[krita] libs: Fix delays in the end of Instant Preview rendering stroke

Dmitry Kazakov null at kde.org
Thu Sep 20 18:34:05 BST 2018


Git commit b5c33311b1f70f3bd249461a31a7333f964e5fb4 by Dmitry Kazakov.
Committed on 20/09/2018 at 17:08.
Pushed by dkazakov into branch 'master'.

Fix delays in the end of Instant Preview rendering stroke

Needs your testing!

Test Plan:

1) Paint with complex brushes on Float32 images
2) Try painting many repetitive strokes, there should be no
   flockering and no delays.
3) Try cancel your srokes before they are finished with Esc key.
   Please note that the most dangerous moment is when Krita writes
   "Updating..." right before background stroke calculation is
   finished. Try pressing exactly at this moment! :)

Technical Details:

There was a short delay in the end of every Instant Preview
regeneration stroke. It was mostly seen on bigger canvases and
Float32-bitdepth mode.

The cause of this delay was the "update resume" stroke that
tried to upload the 100%-zoom image data to the canvas. Sometimes
such uploading could take up to 1.5 seconds, which could interfere
into the painter's workflow.

This patch does multiple things to mitigate this problem:

1) KisSuspendProjectionUpdatesStrokeStrategy is now suspendable
   (again). It means that Krita will suspend the uploading process
   if the user desides to paint further instead of waiting for the
   background rendering to complete. This is the main part of this
   patch.

2) On resuming Krita will not upload the entire image to the canvas,
   but only a changed part. This is achieved by collecting dirty
   requests in KisImage::enableUIUpdates(). This method itself doesn't
   solve the initial problem, but it makes uploading a bit more efficient.

3) While the resume stroke is suspended, KisOpenGLCanvas2 blocks all the
   synchronizations of tiles' mipmaps. It means that normal lodN strokes
   will run with the mipmaps blocked. It is a bit dangerous approach, but
   it works until KisSuspendProjectionUpdatesStrokeStrategy is the only
   user of sigRequestLodPlanesSyncBlocked() signal.

CC:kimageshop at kde.org
BUG:361448
Fixes T2145

M  +3    -3    libs/global/kis_pointer_utils.h
M  +1    -0    libs/image/CMakeLists.txt
M  +2    -2    libs/image/KisFakeRunnableStrokeJobsExecutor.cpp
M  +1    -1    libs/image/KisFakeRunnableStrokeJobsExecutor.h
M  +3    -3    libs/image/KisRunnableBasedStrokeStrategy.cpp
M  +1    -1    libs/image/KisRunnableBasedStrokeStrategy.h
M  +2    -2    libs/image/KisRunnableStrokeJobData.cpp
M  +3    -3    libs/image/KisRunnableStrokeJobData.h
C  +4    -11   libs/image/KisRunnableStrokeJobDataBase.cpp [from: libs/image/KisRunnableStrokeJobsInterface.cpp - 070% similarity]
C  +10   -13   libs/image/KisRunnableStrokeJobDataBase.h [from: libs/image/KisRunnableStrokeJobsInterface.h - 058% similarity]
M  +8    -8    libs/image/KisRunnableStrokeJobUtils.h
M  +1    -1    libs/image/KisRunnableStrokeJobsInterface.cpp
M  +9    -3    libs/image/KisRunnableStrokeJobsInterface.h
M  +15   -1    libs/image/kis_image.cc
M  +4    -21   libs/image/kis_image.h
M  +1    -1    libs/image/kis_image_interfaces.h
M  +8    -10   libs/image/kis_image_signal_router.cpp
M  +34   -2    libs/image/kis_image_signal_router.h
M  +0    -20   libs/image/kis_strokes_queue.cpp
M  +292  -86   libs/image/kis_suspend_projection_updates_stroke_strategy.cpp
M  +7    -2    libs/image/kis_suspend_projection_updates_stroke_strategy.h
M  +37   -17   libs/ui/canvas/kis_canvas2.cpp
M  +3    -2    libs/ui/canvas/kis_canvas2.h
M  +5    -5    libs/ui/canvas/kis_update_info.cpp
M  +6    -4    libs/ui/canvas/kis_update_info.h
M  +1    -1    libs/ui/kis_animation_frame_cache.cpp
M  +1    -1    libs/ui/opengl/kis_opengl_canvas2.cpp
M  +3    -3    libs/ui/opengl/kis_opengl_image_textures.cpp
M  +1    -1    libs/ui/opengl/kis_opengl_image_textures.h
M  +3    -2    libs/ui/opengl/kis_texture_tile.cpp
M  +1    -1    libs/ui/opengl/kis_texture_tile.h

https://commits.kde.org/krita/b5c33311b1f70f3bd249461a31a7333f964e5fb4

diff --git a/libs/global/kis_pointer_utils.h b/libs/global/kis_pointer_utils.h
index e5d2350fb71..ae1c14b9ffb 100644
--- a/libs/global/kis_pointer_utils.h
+++ b/libs/global/kis_pointer_utils.h
@@ -88,10 +88,10 @@ template <template <class> class Container, class T>
  * support it. The main usage case is conversion of pointers in "descendant-
  * to-parent" way.
  */
-template <typename R, typename T>
-inline QList<R> implicitCastList(const QList<T> &list)
+template <typename R, typename T, template <typename U> class Container>
+inline Container<R> implicitCastList(const Container<T> &list)
 {
-    QList<R> result;
+    Container<R> result;
 
     Q_FOREACH(const T &item, list) {
         result.append(item);
diff --git a/libs/image/CMakeLists.txt b/libs/image/CMakeLists.txt
index 23828dfb88a..dcc9e79bb82 100644
--- a/libs/image/CMakeLists.txt
+++ b/libs/image/CMakeLists.txt
@@ -176,6 +176,7 @@ set(kritaimage_LIB_SRCS
    kis_stroke_strategy_undo_command_based.cpp
    kis_simple_stroke_strategy.cpp
    KisRunnableBasedStrokeStrategy.cpp
+   KisRunnableStrokeJobDataBase.cpp
    KisRunnableStrokeJobData.cpp
    KisRunnableStrokeJobsInterface.cpp
    KisFakeRunnableStrokeJobsExecutor.cpp
diff --git a/libs/image/KisFakeRunnableStrokeJobsExecutor.cpp b/libs/image/KisFakeRunnableStrokeJobsExecutor.cpp
index 38ef1c4b59d..f3174fc2beb 100644
--- a/libs/image/KisFakeRunnableStrokeJobsExecutor.cpp
+++ b/libs/image/KisFakeRunnableStrokeJobsExecutor.cpp
@@ -23,9 +23,9 @@
 
 #include <QVector>
 
-void KisFakeRunnableStrokeJobsExecutor::addRunnableJobs(const QVector<KisRunnableStrokeJobData *> &list)
+void KisFakeRunnableStrokeJobsExecutor::addRunnableJobs(const QVector<KisRunnableStrokeJobDataBase *> &list)
 {
-    Q_FOREACH (KisRunnableStrokeJobData *data, list) {
+    Q_FOREACH (KisRunnableStrokeJobDataBase *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");
 
diff --git a/libs/image/KisFakeRunnableStrokeJobsExecutor.h b/libs/image/KisFakeRunnableStrokeJobsExecutor.h
index 5a7fd07af93..9e27dd53205 100644
--- a/libs/image/KisFakeRunnableStrokeJobsExecutor.h
+++ b/libs/image/KisFakeRunnableStrokeJobsExecutor.h
@@ -25,7 +25,7 @@
 class KRITAIMAGE_EXPORT KisFakeRunnableStrokeJobsExecutor : public KisRunnableStrokeJobsInterface
 {
 public:
-    void addRunnableJobs(const QVector<KisRunnableStrokeJobData*> &list);
+    void addRunnableJobs(const QVector<KisRunnableStrokeJobDataBase*> &list);
 };
 
 #endif // KISFAKERUNNABLESTROKEJOBSEXECUTOR_H
diff --git a/libs/image/KisRunnableBasedStrokeStrategy.cpp b/libs/image/KisRunnableBasedStrokeStrategy.cpp
index 45829d73c6e..42e9f9b26d8 100644
--- a/libs/image/KisRunnableBasedStrokeStrategy.cpp
+++ b/libs/image/KisRunnableBasedStrokeStrategy.cpp
@@ -32,10 +32,10 @@ struct KisRunnableBasedStrokeStrategy::JobsInterface : public KisRunnableStrokeJ
     }
 
 
-    void addRunnableJobs(const QVector<KisRunnableStrokeJobData*> &list) {
+    void addRunnableJobs(const QVector<KisRunnableStrokeJobDataBase*> &list) {
         QVector<KisStrokeJobData*> newList;
 
-        Q_FOREACH (KisRunnableStrokeJobData *item, list) {
+        Q_FOREACH (KisRunnableStrokeJobDataBase *item, list) {
             newList.append(item);
         }
 
@@ -67,7 +67,7 @@ void KisRunnableBasedStrokeStrategy::doStrokeCallback(KisStrokeJobData *data)
 {
     if (!data) return;
 
-    KisRunnableStrokeJobData *runnable = dynamic_cast<KisRunnableStrokeJobData*>(data);
+    KisRunnableStrokeJobDataBase *runnable = dynamic_cast<KisRunnableStrokeJobDataBase*>(data);
     if (!runnable) return;
 
     runnable->run();
diff --git a/libs/image/KisRunnableBasedStrokeStrategy.h b/libs/image/KisRunnableBasedStrokeStrategy.h
index 6346ecafe0a..46cd031ae0e 100644
--- a/libs/image/KisRunnableBasedStrokeStrategy.h
+++ b/libs/image/KisRunnableBasedStrokeStrategy.h
@@ -29,7 +29,7 @@ private:
     struct JobsInterface;
 
 public:
-    KisRunnableBasedStrokeStrategy(QString id, const KUndo2MagicString &name);
+    KisRunnableBasedStrokeStrategy(QString id, const KUndo2MagicString &name = KUndo2MagicString());
     KisRunnableBasedStrokeStrategy(const KisRunnableBasedStrokeStrategy &rhs);
     ~KisRunnableBasedStrokeStrategy();
 
diff --git a/libs/image/KisRunnableStrokeJobData.cpp b/libs/image/KisRunnableStrokeJobData.cpp
index a79fd504a01..a55b6be59cb 100644
--- a/libs/image/KisRunnableStrokeJobData.cpp
+++ b/libs/image/KisRunnableStrokeJobData.cpp
@@ -22,13 +22,13 @@
 #include <kis_assert.h>
 
 KisRunnableStrokeJobData::KisRunnableStrokeJobData(QRunnable *runnable, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity)
-    : KisStrokeJobData(sequentiality, exclusivity),
+    : KisRunnableStrokeJobDataBase(sequentiality, exclusivity),
       m_runnable(runnable)
 {
 }
 
 KisRunnableStrokeJobData::KisRunnableStrokeJobData(std::function<void ()> func, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity)
-    : KisStrokeJobData(sequentiality, exclusivity),
+    : KisRunnableStrokeJobDataBase(sequentiality, exclusivity),
       m_func(func)
 {
 }
diff --git a/libs/image/KisRunnableStrokeJobData.h b/libs/image/KisRunnableStrokeJobData.h
index 1afbffed40a..738aa6e524b 100644
--- a/libs/image/KisRunnableStrokeJobData.h
+++ b/libs/image/KisRunnableStrokeJobData.h
@@ -20,12 +20,12 @@
 #define KISRUNNABLESTROKEJOBDATA_H
 
 #include "kritaimage_export.h"
-#include "kis_stroke_job_strategy.h"
+#include "KisRunnableStrokeJobDataBase.h"
 #include <functional>
 
 class QRunnable;
 
-class KRITAIMAGE_EXPORT KisRunnableStrokeJobData : public KisStrokeJobData {
+class KRITAIMAGE_EXPORT KisRunnableStrokeJobData : public KisRunnableStrokeJobDataBase {
 public:
     KisRunnableStrokeJobData(QRunnable *runnable, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL,
                              KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL);
@@ -35,7 +35,7 @@ public:
 
     ~KisRunnableStrokeJobData();
 
-    void run();
+    void run() override;
 
 private:
     QRunnable *m_runnable = 0;
diff --git a/libs/image/KisRunnableStrokeJobsInterface.cpp b/libs/image/KisRunnableStrokeJobDataBase.cpp
similarity index 70%
copy from libs/image/KisRunnableStrokeJobsInterface.cpp
copy to libs/image/KisRunnableStrokeJobDataBase.cpp
index 76c667c89d3..81f9f50c343 100644
--- a/libs/image/KisRunnableStrokeJobsInterface.cpp
+++ b/libs/image/KisRunnableStrokeJobDataBase.cpp
@@ -1,5 +1,5 @@
 /*
- *  Copyright (c) 2017 Dmitry Kazakov <dimula73 at gmail.com>
+ *  Copyright (c) 2018 Dmitry Kazakov <dimula73 at 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
@@ -16,16 +16,9 @@
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  */
 
-#include "KisRunnableStrokeJobsInterface.h"
+#include "KisRunnableStrokeJobDataBase.h"
 
-#include <QVector>
-
-KisRunnableStrokeJobsInterface::~KisRunnableStrokeJobsInterface()
-{
-
-}
-
-void KisRunnableStrokeJobsInterface::addRunnableJob(KisRunnableStrokeJobData *data)
+KisRunnableStrokeJobDataBase::KisRunnableStrokeJobDataBase(KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity)
+    : KisStrokeJobData(sequentiality, exclusivity)
 {
-    addRunnableJobs({data});
 }
diff --git a/libs/image/KisRunnableStrokeJobsInterface.h b/libs/image/KisRunnableStrokeJobDataBase.h
similarity index 58%
copy from libs/image/KisRunnableStrokeJobsInterface.h
copy to libs/image/KisRunnableStrokeJobDataBase.h
index 6f522f36902..35d54d70bfd 100644
--- a/libs/image/KisRunnableStrokeJobsInterface.h
+++ b/libs/image/KisRunnableStrokeJobDataBase.h
@@ -1,5 +1,5 @@
 /*
- *  Copyright (c) 2017 Dmitry Kazakov <dimula73 at gmail.com>
+ *  Copyright (c) 2018 Dmitry Kazakov <dimula73 at 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
@@ -16,22 +16,19 @@
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  */
 
-#ifndef KISRUNNABLESTROKEJOBSINTERFACE_H
-#define KISRUNNABLESTROKEJOBSINTERFACE_H
+#ifndef KISRUNNABLESTROKEJOBDATABASE_H
+#define KISRUNNABLESTROKEJOBDATABASE_H
 
-#include "kritaimage_export.h"
-#include <QtGlobal>
-
-class KisRunnableStrokeJobData;
 
+#include "kritaimage_export.h"
+#include "kis_stroke_job_strategy.h"
+#include "kis_runnable.h"
 
-class KRITAIMAGE_EXPORT KisRunnableStrokeJobsInterface
+class KRITAIMAGE_EXPORT KisRunnableStrokeJobDataBase : public KisStrokeJobData, public KisRunnable
 {
 public:
-    virtual ~KisRunnableStrokeJobsInterface();
-
-    void addRunnableJob(KisRunnableStrokeJobData *data);
-    virtual void addRunnableJobs(const QVector<KisRunnableStrokeJobData*> &list) = 0;
+    KisRunnableStrokeJobDataBase(KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL,
+                                 KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL);
 };
 
-#endif // KISRUNNABLESTROKEJOBSINTERFACE_H
+#endif // KISRUNNABLESTROKEJOBDATABASE_H
diff --git a/libs/image/KisRunnableStrokeJobUtils.h b/libs/image/KisRunnableStrokeJobUtils.h
index f47a75d2fed..bc1dc0722bc 100644
--- a/libs/image/KisRunnableStrokeJobUtils.h
+++ b/libs/image/KisRunnableStrokeJobUtils.h
@@ -27,23 +27,23 @@
 namespace KritaUtils
 {
 
-template <typename Func>
-void addJobSequential(QVector<KisRunnableStrokeJobData*> &jobs, Func func) {
+template <typename Func, typename Job>
+void addJobSequential(QVector<Job*> &jobs, Func func) {
     jobs.append(new KisRunnableStrokeJobData(func, KisStrokeJobData::SEQUENTIAL));
 }
 
-template <typename Func>
-void addJobConcurrent(QVector<KisRunnableStrokeJobData*> &jobs, Func func) {
+template <typename Func, typename Job>
+void addJobConcurrent(QVector<Job*> &jobs, Func func) {
     jobs.append(new KisRunnableStrokeJobData(func, KisStrokeJobData::CONCURRENT));
 }
 
-template <typename Func>
-void addJobBarrier(QVector<KisRunnableStrokeJobData*> &jobs, Func func) {
+template <typename Func, typename Job>
+void addJobBarrier(QVector<Job*> &jobs, Func func) {
     jobs.append(new KisRunnableStrokeJobData(func, KisStrokeJobData::BARRIER));
 }
 
-template <typename Func>
-void addJobUniquelyCuncurrent(QVector<KisRunnableStrokeJobData*> &jobs, Func func) {
+template <typename Func, typename Job>
+void addJobUniquelyCuncurrent(QVector<Job*> &jobs, Func func) {
     jobs.append(new KisRunnableStrokeJobData(func, KisStrokeJobData::UNIQUELY_CONCURRENT));
 }
 
diff --git a/libs/image/KisRunnableStrokeJobsInterface.cpp b/libs/image/KisRunnableStrokeJobsInterface.cpp
index 76c667c89d3..db8b382bc91 100644
--- a/libs/image/KisRunnableStrokeJobsInterface.cpp
+++ b/libs/image/KisRunnableStrokeJobsInterface.cpp
@@ -25,7 +25,7 @@ KisRunnableStrokeJobsInterface::~KisRunnableStrokeJobsInterface()
 
 }
 
-void KisRunnableStrokeJobsInterface::addRunnableJob(KisRunnableStrokeJobData *data)
+void KisRunnableStrokeJobsInterface::addRunnableJob(KisRunnableStrokeJobDataBase *data)
 {
     addRunnableJobs({data});
 }
diff --git a/libs/image/KisRunnableStrokeJobsInterface.h b/libs/image/KisRunnableStrokeJobsInterface.h
index 6f522f36902..b5bf3535917 100644
--- a/libs/image/KisRunnableStrokeJobsInterface.h
+++ b/libs/image/KisRunnableStrokeJobsInterface.h
@@ -21,8 +21,9 @@
 
 #include "kritaimage_export.h"
 #include <QtGlobal>
+#include "kis_pointer_utils.h"
 
-class KisRunnableStrokeJobData;
+class KisRunnableStrokeJobDataBase;
 
 
 class KRITAIMAGE_EXPORT KisRunnableStrokeJobsInterface
@@ -30,8 +31,13 @@ class KRITAIMAGE_EXPORT KisRunnableStrokeJobsInterface
 public:
     virtual ~KisRunnableStrokeJobsInterface();
 
-    void addRunnableJob(KisRunnableStrokeJobData *data);
-    virtual void addRunnableJobs(const QVector<KisRunnableStrokeJobData*> &list) = 0;
+    void addRunnableJob(KisRunnableStrokeJobDataBase *data);
+    virtual void addRunnableJobs(const QVector<KisRunnableStrokeJobDataBase*> &list) = 0;
+
+    template <typename T>
+    void addRunnableJobs(const QVector<T*> &list) {
+        this->addRunnableJobs(implicitCastList<KisRunnableStrokeJobDataBase*>(list));
+    }
 };
 
 #endif // KISRUNNABLESTROKEJOBSINTERFACE_H
diff --git a/libs/image/kis_image.cc b/libs/image/kis_image.cc
index 188af5bb4e0..b9fa4b35f47 100644
--- a/libs/image/kis_image.cc
+++ b/libs/image/kis_image.cc
@@ -96,6 +96,7 @@
 #include "kis_layer_projection_plane.h"
 
 #include "kis_update_time_monitor.h"
+#include "tiles3/kis_lockless_stack.h"
 
 #include <QtCore>
 
@@ -219,6 +220,8 @@ public:
     vKisAnnotationSP annotations;
 
     QAtomicInt disableUIUpdateSignals;
+    KisLocklessStack<QRect> savedDisabledUIUpdates;
+
     KisProjectionUpdatesFilterSP projectionUpdatesFilter;
     KisImageSignalRouter signalRouter;
     KisImageAnimationInterface *animationInterface;
@@ -1631,9 +1634,18 @@ void KisImage::disableUIUpdates()
     m_d->disableUIUpdateSignals.ref();
 }
 
-void KisImage::enableUIUpdates()
+QVector<QRect> KisImage::enableUIUpdates()
 {
     m_d->disableUIUpdateSignals.deref();
+
+    QRect rect;
+    QVector<QRect> postponedUpdates;
+
+    while (m_d->savedDisabledUIUpdates.pop(rect)) {
+        postponedUpdates.append(rect);
+    }
+
+    return postponedUpdates;
 }
 
 void KisImage::notifyProjectionUpdated(const QRect &rc)
@@ -1647,6 +1659,8 @@ void KisImage::notifyProjectionUpdated(const QRect &rc)
         if (dirtyRect.isEmpty()) return;
 
         emit sigImageUpdated(dirtyRect);
+    } else {
+        m_d->savedDisabledUIUpdates.push(rc);
     }
 }
 
diff --git a/libs/image/kis_image.h b/libs/image/kis_image.h
index e1aba4c447d..69e589fdc29 100644
--- a/libs/image/kis_image.h
+++ b/libs/image/kis_image.h
@@ -728,26 +728,6 @@ Q_SIGNALS:
      */
     void sigImageUpdated(const QRect &);
 
-    /**
-     * Emitted whenever the image is going to reset load planes from lodN to
-     * lod0. All the signals emitted with sigImageUpdated() between the calls
-     * to sigBeginLodResetUpdatesBatch() and sigEndLodResetUpdatesBatch() will be
-     * considered as belonging to the same "batch". During processing a "batch"
-     * GUI is not permitted to change lod level of the tile textures. It should
-     * just update internal caches. The call to sigEndLodResetUpdatesBatch()
-     * will start rerendering of the canvas.
-     *
-     * NOTE: this feature is used to avoid flickering when switching
-     * back from lodN plane back to lod0. All the texture tiles should
-     * be loaded with new information before mipmaps can be regenerated.
-     */
-    void sigBeginLodResetUpdatesBatch();
-
-    /**
-     * @see sigBeginLodResetUpdatesBatch()
-     */
-    void sigEndLodResetUpdatesBatch();
-
     /**
        Emitted whenever the image has been modified, so that it
        doesn't match with the version saved on disk.
@@ -955,13 +935,16 @@ public Q_SLOTS:
      * when we change the size of the image. In this case, the whole
      * image will be reloaded into UI by sigSizeChanged(), so there is
      * no need to inform the UI about individual dirty rects.
+     *
+     * The last call to enableUIUpdates() will return the list of udpates
+     * that were requested while they were blocked.
      */
     void disableUIUpdates() override;
 
     /**
      * \see disableUIUpdates
      */
-    void enableUIUpdates() override;
+    QVector<QRect> enableUIUpdates() override;
 
     /**
      * Disables the processing of all the setDirty() requests that
diff --git a/libs/image/kis_image_interfaces.h b/libs/image/kis_image_interfaces.h
index 5635a88da7c..79c59d98192 100644
--- a/libs/image/kis_image_interfaces.h
+++ b/libs/image/kis_image_interfaces.h
@@ -47,7 +47,7 @@ public:
     virtual void unblockUpdates() = 0;
 
     virtual void disableUIUpdates() = 0;
-    virtual void enableUIUpdates() = 0;
+    virtual QVector<QRect> enableUIUpdates() = 0;
 
     virtual void disableDirtyRequests() = 0;
     virtual void enableDirtyRequests() = 0;
diff --git a/libs/image/kis_image_signal_router.cpp b/libs/image/kis_image_signal_router.cpp
index 007f91c5af4..0ba8147b406 100644
--- a/libs/image/kis_image_signal_router.cpp
+++ b/libs/image/kis_image_signal_router.cpp
@@ -115,22 +115,20 @@ void KisImageSignalRouter::emitAboutToRemoveANode(KisNode *parent, int index)
     emit sigRemoveNodeAsync(removedNode);
 }
 
-void KisImageSignalRouter::emitBeginLodResetUpdatesBatch()
+void KisImageSignalRouter::emitRequestLodPlanesSyncBlocked(bool value)
 {
-    KisImageSP image = m_image.toStrongRef();
-    KIS_SAFE_ASSERT_RECOVER_RETURN(image);
-
-    emit image->sigBeginLodResetUpdatesBatch();
+    emit sigRequestLodPlanesSyncBlocked(value);
 }
 
-void KisImageSignalRouter::emitEndLodResetUpdatesBatch()
+void KisImageSignalRouter::emitNotifyBatchUpdateStarted()
 {
-    KisImageSP image = m_image.toStrongRef();
-    KIS_SAFE_ASSERT_RECOVER_RETURN(image);
-
-    emit image->sigEndLodResetUpdatesBatch();
+    emit sigNotifyBatchUpdateStarted();
 }
 
+void KisImageSignalRouter::emitNotifyBatchUpdateEnded()
+{
+    emit sigNotifyBatchUpdateEnded();
+}
 
 void KisImageSignalRouter::slotNotification(KisImageSignalType type)
 {
diff --git a/libs/image/kis_image_signal_router.h b/libs/image/kis_image_signal_router.h
index 10701deab8c..2e196bf4f63 100644
--- a/libs/image/kis_image_signal_router.h
+++ b/libs/image/kis_image_signal_router.h
@@ -149,8 +149,9 @@ public:
     void emitNodeHasBeenAdded(KisNode *parent, int index);
     void emitAboutToRemoveANode(KisNode *parent, int index);
 
-    void emitBeginLodResetUpdatesBatch();
-    void emitEndLodResetUpdatesBatch();
+    void emitRequestLodPlanesSyncBlocked(bool value);
+    void emitNotifyBatchUpdateStarted();
+    void emitNotifyBatchUpdateEnded();
 
 private Q_SLOTS:
     void slotNotification(KisImageSignalType type);
@@ -159,6 +160,37 @@ Q_SIGNALS:
 
     void sigNotification(KisImageSignalType type);
 
+    /**
+     * Emitted whenever the image wants to update Lod0 plane of the canvas. Blocking
+     * synching effectively means that the canvas will not try to read from these planes
+     * until the **all** the data is loaded. Otherwise the user will see weird flickering
+     * because of partially loaded lod0 tiles.
+     *
+     * NOTE: while the sync is blockes, the canvas is considered to use LodN planes
+     *       that are expected to contain valid data.
+     */
+    void sigRequestLodPlanesSyncBlocked(bool value);
+
+    /**
+     * Emitted whenever the image is going to issue a lot of canvas update signals and
+     * it it a good idea to group then and rerender the canvas in one go. The canvas
+     * should initiate new rerenders while the batch is in progress.
+     *
+     * NOTE: even though the batched updates will not initiate a rerender, it does
+     *       **not** guarantee that there will be processed during the batch. The
+     *       updates may come from other sources, e.g. from mouse moves.
+     *
+     * NOTE: this feature is used to avoid flickering when switching
+     *       back from lodN plane back to lod0. All the texture tiles should
+     *       be loaded with new information before mipmaps can be regenerated.
+     */
+    void sigNotifyBatchUpdateStarted();
+
+    /**
+     * \see sigNotifyBatchUpdateStarted()
+     */
+    void sigNotifyBatchUpdateEnded();
+
     // Notifications
     void sigImageModified();
 
diff --git a/libs/image/kis_strokes_queue.cpp b/libs/image/kis_strokes_queue.cpp
index 23520f0104a..f7e08484ffc 100644
--- a/libs/image/kis_strokes_queue.cpp
+++ b/libs/image/kis_strokes_queue.cpp
@@ -193,15 +193,6 @@ bool KisStrokesQueue::Private::shouldWrapInSuspendUpdatesStroke() const
         if (stroke->isCancelled()) continue;
 
         if (stroke->type() == KisStroke::RESUME) {
-
-            /**
-             * Resuming process is long-running and consists of
-             * multiple actions, therefore, if it has already started,
-             * we cannot use it to guard our new stroke, so just skip it.
-             * see https://phabricator.kde.org/T2542
-             */
-            if (stroke->isInitialized()) continue;
-
             return false;
         }
     }
@@ -218,9 +209,6 @@ StrokesQueueIterator KisStrokesQueue::Private::findNewLod0Pos()
         if ((*it)->isCancelled()) continue;
 
         if ((*it)->type() == KisStroke::RESUME) {
-            // \see a comment in shouldWrapInSuspendUpdatesStroke()
-            if ((*it)->isInitialized()) continue;
-
             return it;
         }
     }
@@ -236,14 +224,6 @@ StrokesQueueIterator KisStrokesQueue::Private::findNewLodNPos(KisStrokeSP lodN)
     for (; it != end; ++it) {
         if ((*it)->isCancelled()) continue;
 
-        if (((*it)->type() == KisStroke::SUSPEND ||
-             (*it)->type() == KisStroke::RESUME) &&
-            (*it)->isInitialized()) {
-
-            // \see a comment in shouldWrapInSuspendUpdatesStroke()
-            continue;
-        }
-
         if ((*it)->type() == KisStroke::LOD0 ||
             (*it)->type() == KisStroke::SUSPEND ||
             (*it)->type() == KisStroke::RESUME) {
diff --git a/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp b/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp
index bf6b36bbf9d..4f65953cb35 100644
--- a/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp
+++ b/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp
@@ -23,6 +23,11 @@
 #include <kis_projection_updates_filter.h>
 #include "kis_image_signal_router.h"
 
+#include "kundo2command.h"
+#include "KisRunnableStrokeJobDataBase.h"
+#include "KisRunnableStrokeJobsInterface.h"
+#include "kis_paintop_utils.h"
+
 
 inline uint qHash(const QRect &rc) {
     return rc.x() +
@@ -35,6 +40,13 @@ struct KisSuspendProjectionUpdatesStrokeStrategy::Private
 {
     KisImageWSP image;
     bool suspend;
+    QVector<QRect> accumulatedDirtyRects;
+    bool sanityResumingFinished = false;
+    int updatesEpoch = 0;
+    bool haveDisabledGUILodSync = false;
+
+    void tryFetchUsedUpdatesFilter(KisImageSP image);
+    void tryIssueRecordedDirtyRequests(KisImageSP image);
 
     class SuspendLod0Updates : public KisProjectionUpdatesFilter
     {
@@ -112,50 +124,227 @@ struct KisSuspendProjectionUpdatesStrokeStrategy::Private
         QMutex m_mutex;
     };
 
+    QVector<QSharedPointer<SuspendLod0Updates>> usedFilters;
+
+
+    struct StrokeJobCommand : public KUndo2Command
+    {
+        StrokeJobCommand(KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL,
+                         KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL)
+            : m_sequentiality(sequentiality),
+              m_exclusivity(exclusivity)
+        {}
+
+        KisStrokeJobData::Sequentiality m_sequentiality;
+        KisStrokeJobData::Exclusivity m_exclusivity;
+    };
+
+    struct UndoableData : public KisRunnableStrokeJobDataBase
+    {
+        UndoableData(StrokeJobCommand *command)
+            : KisRunnableStrokeJobDataBase(command->m_sequentiality, command->m_exclusivity),
+              m_command(command)
+        {
+        }
+
+        void run() override {
+            KIS_SAFE_ASSERT_RECOVER_RETURN(m_command);
+            m_command->redo();
+        }
+
+        QScopedPointer<StrokeJobCommand> m_command;
+    };
+
     // Suspend job should be a barrier to ensure all
     // previous lodN strokes reach the GUI. Otherwise,
     // they will be blocked in
     // KisImage::notifyProjectionUpdated()
-    class SuspendData : public KisStrokeJobData {
-    public:
-        SuspendData()
-            : KisStrokeJobData(BARRIER)
-            {}
+    struct SuspendUpdatesCommand : public StrokeJobCommand
+    {
+        SuspendUpdatesCommand(Private *d)
+            : StrokeJobCommand(KisStrokeJobData::BARRIER),
+              m_d(d) {}
+
+        void redo() override {
+            KisImageSP image = m_d->image.toStrongRef();
+            KIS_SAFE_ASSERT_RECOVER_RETURN(image);
+            KIS_SAFE_ASSERT_RECOVER_RETURN(!image->projectionUpdatesFilter());
+
+            image->setProjectionUpdatesFilter(
+                KisProjectionUpdatesFilterSP(new Private::SuspendLod0Updates()));
+        }
+
+
+        void undo() override {
+            KisImageSP image = m_d->image.toStrongRef();
+            KIS_SAFE_ASSERT_RECOVER_RETURN(image);
+            KIS_SAFE_ASSERT_RECOVER_RETURN(image->projectionUpdatesFilter());
+
+            m_d->tryFetchUsedUpdatesFilter(image);
+        }
+
+        Private *m_d;
     };
 
-    class ResumeAndIssueGraphUpdatesData : public KisStrokeJobData {
-    public:
-        ResumeAndIssueGraphUpdatesData()
-            : KisStrokeJobData(SEQUENTIAL)
-            {}
+
+    struct ResumeAndIssueGraphUpdatesCommand : public StrokeJobCommand
+    {
+        ResumeAndIssueGraphUpdatesCommand(Private *d)
+            : StrokeJobCommand(KisStrokeJobData::BARRIER),
+              m_d(d) {}
+
+        void redo() override {
+            KisImageSP image = m_d->image.toStrongRef();
+            KIS_SAFE_ASSERT_RECOVER_RETURN(image);
+            KIS_SAFE_ASSERT_RECOVER_RETURN(image->projectionUpdatesFilter());
+
+            image->disableUIUpdates();
+            m_d->tryFetchUsedUpdatesFilter(image);
+            m_d->tryIssueRecordedDirtyRequests(image);
+        }
+
+        void undo() override {
+            KisImageSP image = m_d->image.toStrongRef();
+            KIS_SAFE_ASSERT_RECOVER_RETURN(image);
+            KIS_SAFE_ASSERT_RECOVER_RETURN(!image->projectionUpdatesFilter());
+
+            image->setProjectionUpdatesFilter(
+                KisProjectionUpdatesFilterSP(new Private::SuspendLod0Updates()));
+            image->enableUIUpdates();
+        }
+
+        Private *m_d;
     };
 
-    class StartBatchUpdate : public KisStrokeJobData {
-    public:
-        StartBatchUpdate()
-            : KisStrokeJobData(BARRIER)
-            {}
+    struct UploadDataToUIData : public KisRunnableStrokeJobDataBase
+    {
+        UploadDataToUIData(const QRect &rc, int updateEpoch, KisSuspendProjectionUpdatesStrokeStrategy *strategy)
+            : KisRunnableStrokeJobDataBase(KisStrokeJobData::CONCURRENT),
+              m_strategy(strategy),
+              m_rc(rc),
+              m_updateEpoch(updateEpoch)
+        {
+        }
+
+        void run() override {
+            // check if we've already started stinking...
+            if (m_strategy->m_d->updatesEpoch > m_updateEpoch) {
+                return;
+            }
+
+            KisImageSP image = m_strategy->m_d->image.toStrongRef();
+            KIS_SAFE_ASSERT_RECOVER_RETURN(image);
+
+            image->notifyProjectionUpdated(m_rc);
+        }
+
+        KisSuspendProjectionUpdatesStrokeStrategy *m_strategy;
+        QRect m_rc;
+        int m_updateEpoch;
     };
 
-    class EndBatchUpdate : public KisStrokeJobData {
-    public:
-        EndBatchUpdate()
-            : KisStrokeJobData(BARRIER)
-            {}
+    struct BlockUILodSync : public KisRunnableStrokeJobDataBase
+    {
+        BlockUILodSync(bool block, KisSuspendProjectionUpdatesStrokeStrategy *strategy)
+            : KisRunnableStrokeJobDataBase(KisStrokeJobData::BARRIER),
+              m_strategy(strategy),
+              m_block(block)
+        {}
+
+        void run() override {
+            KisImageSP image = m_strategy->m_d->image.toStrongRef();
+            KIS_SAFE_ASSERT_RECOVER_RETURN(image);
+
+            image->signalRouter()->emitRequestLodPlanesSyncBlocked(m_block);
+            m_strategy->m_d->haveDisabledGUILodSync = m_block;
+        }
+
+        KisSuspendProjectionUpdatesStrokeStrategy *m_strategy;
+        bool m_block;
     };
 
-    class IssueCanvasUpdatesData : public KisStrokeJobData {
-    public:
-        IssueCanvasUpdatesData(QRect _updateRect)
-            : KisStrokeJobData(CONCURRENT),
-              updateRect(_updateRect)
-            {}
-        QRect updateRect;
+    struct StartBatchUIUpdatesCommand : public StrokeJobCommand
+    {
+        StartBatchUIUpdatesCommand(KisSuspendProjectionUpdatesStrokeStrategy *strategy)
+            : StrokeJobCommand(KisStrokeJobData::BARRIER),
+              m_strategy(strategy) {}
+
+        void redo() override {
+            KisImageSP image = m_strategy->m_d->image.toStrongRef();
+            KIS_SAFE_ASSERT_RECOVER_RETURN(image);
+
+            /**
+             * We accumulate dirty rects from all(!) epochs, because some updates of the
+             * previous epochs might have been cancelled without doing any real work.
+             */
+            const QVector<QRect> totalDirtyRects =
+                image->enableUIUpdates() + m_strategy->m_d->accumulatedDirtyRects;
+
+            const QRect totalRect =
+                image->bounds() &
+                std::accumulate(totalDirtyRects.begin(), totalDirtyRects.end(), QRect(), std::bit_or<QRect>());
+
+            m_strategy->m_d->accumulatedDirtyRects =
+                KisPaintOpUtils::splitAndFilterDabRect(totalRect,
+                                                       totalDirtyRects,
+                                                       KritaUtils::optimalPatchSize().width());
+
+            image->signalRouter()->emitNotifyBatchUpdateStarted();
+
+            QVector<KisRunnableStrokeJobDataBase*> jobsData;
+            Q_FOREACH (const QRect &rc, m_strategy->m_d->accumulatedDirtyRects) {
+                jobsData << new Private::UploadDataToUIData(rc, m_strategy->m_d->updatesEpoch, m_strategy);
+            }
+
+            m_strategy->runnableJobsInterface()->addRunnableJobs(jobsData);
+
+        }
+
+        void undo() override {
+            KisImageSP image = m_strategy->m_d->image.toStrongRef();
+            KIS_SAFE_ASSERT_RECOVER_RETURN(image);
+
+            image->signalRouter()->emitNotifyBatchUpdateEnded();
+            image->disableUIUpdates();
+        }
+
+        KisSuspendProjectionUpdatesStrokeStrategy *m_strategy;
     };
+
+    struct EndBatchUIUpdatesCommand : public StrokeJobCommand
+    {
+        EndBatchUIUpdatesCommand(KisSuspendProjectionUpdatesStrokeStrategy *strategy)
+            : StrokeJobCommand(KisStrokeJobData::BARRIER),
+              m_strategy(strategy) {}
+
+        void redo() override {
+            KisImageSP image = m_strategy->m_d->image.toStrongRef();
+            KIS_SAFE_ASSERT_RECOVER_RETURN(image);
+
+            image->signalRouter()->emitNotifyBatchUpdateEnded();
+            m_strategy->m_d->sanityResumingFinished = true;
+        }
+
+        void undo() override {
+            KIS_SAFE_ASSERT_RECOVER_NOOP(0 && "why the heck we are undoing the last job of the stroke?!");
+
+            m_strategy->m_d->sanityResumingFinished = false;
+
+            KisImageSP image = m_strategy->m_d->image.toStrongRef();
+            KIS_SAFE_ASSERT_RECOVER_RETURN(image);
+
+            image->signalRouter()->emitNotifyBatchUpdateStarted();
+        }
+
+        KisSuspendProjectionUpdatesStrokeStrategy *m_strategy;
+    };
+
+
+    QVector<StrokeJobCommand*> executedCommands;
 };
 
 KisSuspendProjectionUpdatesStrokeStrategy::KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP image, bool suspend)
-    : KisSimpleStrokeStrategy(suspend ? "suspend_stroke_strategy" : "resume_stroke_strategy"),
+    : KisRunnableBasedStrokeStrategy(suspend ? "suspend_stroke_strategy" : "resume_stroke_strategy"),
       m_d(new Private)
 {
     m_d->image = image;
@@ -174,11 +363,32 @@ KisSuspendProjectionUpdatesStrokeStrategy::KisSuspendProjectionUpdatesStrokeStra
     enableJob(JOB_DOSTROKE, true);
     enableJob(JOB_CANCEL, true);
 
+    enableJob(JOB_SUSPEND, true, KisStrokeJobData::BARRIER);
+    enableJob(JOB_RESUME, true, KisStrokeJobData::BARRIER);
+
     setNeedsExplicitCancel(true);
 }
 
 KisSuspendProjectionUpdatesStrokeStrategy::~KisSuspendProjectionUpdatesStrokeStrategy()
 {
+    qDeleteAll(m_d->executedCommands);
+}
+
+void KisSuspendProjectionUpdatesStrokeStrategy::initStrokeCallback()
+{
+    QVector<KisRunnableStrokeJobDataBase*> jobs;
+
+    if (m_d->suspend) {
+        jobs << new Private::UndoableData(new Private::SuspendUpdatesCommand(m_d.data()));
+    } else {
+        jobs << new Private::UndoableData(new Private::ResumeAndIssueGraphUpdatesCommand(m_d.data()));
+        jobs << new Private::BlockUILodSync(true, this);
+        jobs << new Private::UndoableData(new Private::StartBatchUIUpdatesCommand(this));
+        jobs << new Private::UndoableData(new Private::EndBatchUIUpdatesCommand(this));
+        jobs << new Private::BlockUILodSync(false, this);
+    }
+
+    runnableJobsInterface()->addRunnableJobs(jobs);
 }
 
 /**
@@ -218,85 +428,49 @@ KisSuspendProjectionUpdatesStrokeStrategy::~KisSuspendProjectionUpdatesStrokeStr
  *          entire image. Multithreaded way is used to conform the
  *          double-stage update principle of KisCanvas2.
  */
-
 void KisSuspendProjectionUpdatesStrokeStrategy::doStrokeCallback(KisStrokeJobData *data)
 {
-    Private::SuspendData *suspendData = dynamic_cast<Private::SuspendData*>(data);
-    Private::ResumeAndIssueGraphUpdatesData *resumeData = dynamic_cast<Private::ResumeAndIssueGraphUpdatesData*>(data);
-    Private::StartBatchUpdate *startBatchData = dynamic_cast<Private::StartBatchUpdate*>(data);
-    Private::EndBatchUpdate *endBatchData = dynamic_cast<Private::EndBatchUpdate*>(data);
-    Private::IssueCanvasUpdatesData *canvasUpdates = dynamic_cast<Private::IssueCanvasUpdatesData*>(data);
-
-    KisImageSP image = m_d->image.toStrongRef();
-    if (!image) {
-        return;
-    }
+    KisRunnableStrokeJobDataBase *runnable = dynamic_cast<KisRunnableStrokeJobDataBase*>(data);
+    if (runnable) {
+        runnable->run();
 
-    if (suspendData) {
-        image->setProjectionUpdatesFilter(
-            KisProjectionUpdatesFilterSP(new Private::SuspendLod0Updates()));
-    } else if (resumeData) {
-        image->disableUIUpdates();
-        resumeAndIssueUpdates(false);
-    } else if (startBatchData) {
-        image->enableUIUpdates();
-        image->signalRouter()->emitBeginLodResetUpdatesBatch();
-    } else if (canvasUpdates) {
-        image->notifyProjectionUpdated(canvasUpdates->updateRect);
-    } else if (endBatchData) {
-        image->signalRouter()->emitEndLodResetUpdatesBatch();
+        if (Private::UndoableData *undoable = dynamic_cast<Private::UndoableData*>(data)) {
+            Private::StrokeJobCommand *command =  undoable->m_command.take();
+            m_d->executedCommands.append(command);
+        }
     }
 }
 
 QList<KisStrokeJobData*> KisSuspendProjectionUpdatesStrokeStrategy::createSuspendJobsData(KisImageWSP /*image*/)
 {
-    QList<KisStrokeJobData*> jobsData;
-    jobsData << new Private::SuspendData();
-    return jobsData;
+    return QList<KisStrokeJobData*>();
 }
 
-QList<KisStrokeJobData*> KisSuspendProjectionUpdatesStrokeStrategy::createResumeJobsData(KisImageWSP _image)
+QList<KisStrokeJobData*> KisSuspendProjectionUpdatesStrokeStrategy::createResumeJobsData(KisImageWSP /*_image*/)
 {
-    QList<KisStrokeJobData*> jobsData;
-
-    jobsData << new Private::ResumeAndIssueGraphUpdatesData();
-    jobsData << new Private::StartBatchUpdate();
-
-    using KritaUtils::splitRectIntoPatches;
-    using KritaUtils::optimalPatchSize;
-    KisImageSP image = _image;
-
-    QVector<QRect> rects = splitRectIntoPatches(image->bounds(), optimalPatchSize());
-    Q_FOREACH (const QRect &rc, rects) {
-        jobsData << new Private::IssueCanvasUpdatesData(rc);
-    }
-
-    jobsData << new Private::EndBatchUpdate();
-
-    return jobsData;
+    return QList<KisStrokeJobData*>();
 }
 
-void KisSuspendProjectionUpdatesStrokeStrategy::resumeAndIssueUpdates(bool dropUpdates)
+void KisSuspendProjectionUpdatesStrokeStrategy::Private::tryFetchUsedUpdatesFilter(KisImageSP image)
 {
-    KisImageSP image = m_d->image.toStrongRef();
-    if (!image) {
-        return;
-    }
-
     KisProjectionUpdatesFilterSP filter =
         image->projectionUpdatesFilter();
 
     if (!filter) return;
 
-    Private::SuspendLod0Updates *localFilter =
-        dynamic_cast<Private::SuspendLod0Updates*>(filter.data());
+    QSharedPointer<Private::SuspendLod0Updates> localFilter =
+        filter.dynamicCast<Private::SuspendLod0Updates>();
 
     if (localFilter) {
         image->setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP());
+        this->usedFilters.append(localFilter);
+    }
+}
 
-        if (!dropUpdates) {
-            localFilter->notifyUpdates(image.data());
-        }
+void KisSuspendProjectionUpdatesStrokeStrategy::Private::tryIssueRecordedDirtyRequests(KisImageSP image)
+{
+    Q_FOREACH (QSharedPointer<Private::SuspendLod0Updates> filter, usedFilters) {
+        filter->notifyUpdates(image.data());
     }
 }
 
@@ -307,6 +481,16 @@ void KisSuspendProjectionUpdatesStrokeStrategy::cancelStrokeCallback()
         return;
     }
 
+    for (auto it = m_d->executedCommands.rbegin(); it != m_d->executedCommands.rend(); ++it) {
+        (*it)->undo();
+    }
+
+    m_d->tryFetchUsedUpdatesFilter(image);
+
+    if (m_d->haveDisabledGUILodSync) {
+        image->signalRouter()->emitRequestLodPlanesSyncBlocked(false);
+    }
+
     /**
      * We shouldn't emit any ad-hoc updates when cancelling the
      * stroke.  It generates weird temporary holes on the canvas,
@@ -314,10 +498,32 @@ void KisSuspendProjectionUpdatesStrokeStrategy::cancelStrokeCallback()
      * corrupted. We will just emit a common refreshGraphAsync() that
      * will do all the work in a beautiful way
      */
-    resumeAndIssueUpdates(true);
-
     if (!m_d->suspend) {
         // FIXME: optimize
         image->refreshGraphAsync();
     }
 }
+
+void KisSuspendProjectionUpdatesStrokeStrategy::suspendStrokeCallback()
+{
+    KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->suspend || !m_d->sanityResumingFinished);
+
+    for (auto it = m_d->executedCommands.rbegin(); it != m_d->executedCommands.rend(); ++it) {
+        (*it)->undo();
+    }
+
+    // reset all the issued updates
+    m_d->updatesEpoch++;
+}
+
+void KisSuspendProjectionUpdatesStrokeStrategy::resumeStrokeCallback()
+{
+    QVector<KisRunnableStrokeJobDataBase*> jobs;
+
+    Q_FOREACH (Private::StrokeJobCommand *command, m_d->executedCommands) {
+        jobs << new Private::UndoableData(command);
+    }
+    m_d->executedCommands.clear();
+
+    runnableJobsInterface()->addRunnableJobs(jobs);
+}
diff --git a/libs/image/kis_suspend_projection_updates_stroke_strategy.h b/libs/image/kis_suspend_projection_updates_stroke_strategy.h
index 1cc30bc0b33..be82a2a5ebb 100644
--- a/libs/image/kis_suspend_projection_updates_stroke_strategy.h
+++ b/libs/image/kis_suspend_projection_updates_stroke_strategy.h
@@ -19,11 +19,11 @@
 #ifndef __KIS_SUSPEND_PROJECTION_UPDATES_STROKE_STRATEGY_H
 #define __KIS_SUSPEND_PROJECTION_UPDATES_STROKE_STRATEGY_H
 
-#include <kis_simple_stroke_strategy.h>
+#include <KisRunnableBasedStrokeStrategy.h>
 
 #include <QScopedPointer>
 
-class KisSuspendProjectionUpdatesStrokeStrategy : public KisSimpleStrokeStrategy
+class KisSuspendProjectionUpdatesStrokeStrategy : public KisRunnableBasedStrokeStrategy
 {
 public:
     KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP image, bool suspend);
@@ -33,9 +33,14 @@ public:
     static QList<KisStrokeJobData*> createResumeJobsData(KisImageWSP image);
 
 private:
+    void initStrokeCallback() override;
     void doStrokeCallback(KisStrokeJobData *data) override;
     void cancelStrokeCallback() override;
 
+    void suspendStrokeCallback() override;
+    void resumeStrokeCallback() override;
+
+
     void resumeAndIssueUpdates(bool dropUpdates);
 
 private:
diff --git a/libs/ui/canvas/kis_canvas2.cpp b/libs/ui/canvas/kis_canvas2.cpp
index 883c373843d..53cdf604211 100644
--- a/libs/ui/canvas/kis_canvas2.cpp
+++ b/libs/ui/canvas/kis_canvas2.cpp
@@ -95,6 +95,8 @@
 #include "opengl/kis_opengl_canvas_debugger.h"
 
 #include "kis_algebra_2d.h"
+#include "kis_image_signal_router.h"
+
 
 class Q_DECL_HIDDEN KisCanvas2::KisCanvas2Private
 {
@@ -149,7 +151,7 @@ public:
     QRect regionOfInterest;
 
     QRect renderingLimit;
-    int lodResetBatchActive = 0;
+    int isBatchUpdateActive = 0;
 
     bool effectiveLodAllowedInImage() {
         return lodAllowedInImage && !bootstrapLodBlocked;
@@ -562,8 +564,9 @@ void KisCanvas2::initializeImage()
     m_d->toolProxy.initializeImage(image);
 
     connect(image, SIGNAL(sigImageUpdated(QRect)), SLOT(startUpdateCanvasProjection(QRect)), Qt::DirectConnection);
-    connect(image, SIGNAL(sigBeginLodResetUpdatesBatch()), SLOT(slotBeginLodResetUpdatesBatch()), Qt::DirectConnection);
-    connect(image, SIGNAL(sigEndLodResetUpdatesBatch()), SLOT(slotEndLodResetUpdatesBatch()), Qt::DirectConnection);
+    connect(image->signalRouter(), SIGNAL(sigNotifyBatchUpdateStarted()), SLOT(slotBeginUpdatesBatch()), Qt::DirectConnection);
+    connect(image->signalRouter(), SIGNAL(sigNotifyBatchUpdateEnded()), SLOT(slotEndUpdatesBatch()), Qt::DirectConnection);
+    connect(image->signalRouter(), SIGNAL(sigRequestLodPlanesSyncBlocked(bool)), SLOT(slotSetLodUpdatesBlocked(bool)), Qt::DirectConnection);
 
     connect(image, SIGNAL(sigProofingConfigChanged()), SLOT(slotChangeProofingConfig()));
     connect(image, SIGNAL(sigSizeChanged(const QPointF&, const QPointF&)), SLOT(startResizingImage()), Qt::DirectConnection);
@@ -758,7 +761,7 @@ void KisCanvas2::startUpdateCanvasProjection(const QRect & rc)
 void KisCanvas2::updateCanvasProjection()
 {
     auto tryIssueCanvasUpdates = [this](const QRect &vRect) {
-        if (!m_d->lodResetBatchActive) {
+        if (!m_d->isBatchUpdateActive) {
             // TODO: Implement info->dirtyViewportRect() for KisOpenGLCanvas2 to avoid updating whole canvas
             if (m_d->currentCanvasIsOpenGL) {
                 m_d->savedUpdateRect = QRect();
@@ -782,26 +785,30 @@ void KisCanvas2::updateCanvasProjection()
         tryIssueCanvasUpdates(vRect);
     };
 
+    bool shouldExplicitlyIssueUpdates = false;
 
     QVector<KisUpdateInfoSP> infoObjects;
     while (KisUpdateInfoSP info = m_d->projectionUpdatesCompressor.takeUpdateInfo()) {
-        const KisBatchControlUpdateInfo *batchInfo = dynamic_cast<const KisBatchControlUpdateInfo*>(info.data());
+        const KisMarkerUpdateInfo *batchInfo = dynamic_cast<const KisMarkerUpdateInfo*>(info.data());
         if (batchInfo) {
             if (!infoObjects.isEmpty()) {
                 uploadData(infoObjects);
                 infoObjects.clear();
             }
 
-            if (batchInfo->type() == KisBatchControlUpdateInfo::StartBatch) {
-                m_d->lodResetBatchActive++;
+            if (batchInfo->type() == KisMarkerUpdateInfo::StartBatch) {
+                m_d->isBatchUpdateActive++;
+            } else if (batchInfo->type() == KisMarkerUpdateInfo::EndBatch) {
+                m_d->isBatchUpdateActive--;
+                KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->isBatchUpdateActive >= 0);
+                if (m_d->isBatchUpdateActive == 0) {
+                    shouldExplicitlyIssueUpdates = true;
+                }
+            } else if (batchInfo->type() == KisMarkerUpdateInfo::BlockLodUpdates) {
                 m_d->canvasWidget->setLodResetInProgress(true);
-            } else if (batchInfo->type() == KisBatchControlUpdateInfo::EndBatch) {
+            } else if (batchInfo->type() == KisMarkerUpdateInfo::UnblockLodUpdates) {
                 m_d->canvasWidget->setLodResetInProgress(false);
-                m_d->lodResetBatchActive--;
-                KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->lodResetBatchActive >= 0);
-                if (m_d->lodResetBatchActive == 0) {
-                    tryIssueCanvasUpdates(m_d->coordinatesConverter->imageRectInImagePixels());
-                }
+                shouldExplicitlyIssueUpdates = true;
             }
         } else {
             infoObjects << info;
@@ -810,27 +817,40 @@ void KisCanvas2::updateCanvasProjection()
 
     if (!infoObjects.isEmpty()) {
         uploadData(infoObjects);
+    } else if (shouldExplicitlyIssueUpdates) {
+        tryIssueCanvasUpdates(m_d->coordinatesConverter->imageRectInImagePixels());
     }
 }
 
-void KisCanvas2::slotBeginLodResetUpdatesBatch()
+void KisCanvas2::slotBeginUpdatesBatch()
 {
     KisUpdateInfoSP info =
-        new KisBatchControlUpdateInfo(KisBatchControlUpdateInfo::StartBatch,
+        new KisMarkerUpdateInfo(KisMarkerUpdateInfo::StartBatch,
                                       m_d->coordinatesConverter->imageRectInImagePixels());
     m_d->projectionUpdatesCompressor.putUpdateInfo(info);
     emit sigCanvasCacheUpdated();
 }
 
-void KisCanvas2::slotEndLodResetUpdatesBatch()
+void KisCanvas2::slotEndUpdatesBatch()
 {
     KisUpdateInfoSP info =
-        new KisBatchControlUpdateInfo(KisBatchControlUpdateInfo::EndBatch,
+        new KisMarkerUpdateInfo(KisMarkerUpdateInfo::EndBatch,
                                       m_d->coordinatesConverter->imageRectInImagePixels());
     m_d->projectionUpdatesCompressor.putUpdateInfo(info);
     emit sigCanvasCacheUpdated();
 }
 
+void KisCanvas2::slotSetLodUpdatesBlocked(bool value)
+{
+    KisUpdateInfoSP info =
+        new KisMarkerUpdateInfo(value ?
+                                KisMarkerUpdateInfo::BlockLodUpdates :
+                                KisMarkerUpdateInfo::UnblockLodUpdates,
+                                m_d->coordinatesConverter->imageRectInImagePixels());
+    m_d->projectionUpdatesCompressor.putUpdateInfo(info);
+    emit sigCanvasCacheUpdated();
+}
+
 void KisCanvas2::slotDoCanvasUpdate()
 {
     /**
diff --git a/libs/ui/canvas/kis_canvas2.h b/libs/ui/canvas/kis_canvas2.h
index 6bcf7d20199..639ace19303 100644
--- a/libs/ui/canvas/kis_canvas2.h
+++ b/libs/ui/canvas/kis_canvas2.h
@@ -286,8 +286,9 @@ private Q_SLOTS:
     void startUpdateCanvasProjection(const QRect & rc);
     void updateCanvasProjection();
 
-    void slotBeginLodResetUpdatesBatch();
-    void slotEndLodResetUpdatesBatch();
+    void slotBeginUpdatesBatch();
+    void slotEndUpdatesBatch();
+    void slotSetLodUpdatesBlocked(bool value);
 
     /**
      * Called whenever the view widget needs to show a different part of
diff --git a/libs/ui/canvas/kis_update_info.cpp b/libs/ui/canvas/kis_update_info.cpp
index 9330a4c2b57..11a23b0669a 100644
--- a/libs/ui/canvas/kis_update_info.cpp
+++ b/libs/ui/canvas/kis_update_info.cpp
@@ -97,25 +97,25 @@ bool KisOpenGLUpdateInfo::tryMergeWith(const KisOpenGLUpdateInfo &rhs)
     return true;
 }
 
-KisBatchControlUpdateInfo::KisBatchControlUpdateInfo(KisBatchControlUpdateInfo::Type type, const QRect &dirtyImageRect)
+KisMarkerUpdateInfo::KisMarkerUpdateInfo(KisMarkerUpdateInfo::Type type, const QRect &dirtyImageRect)
     : m_type(type),
       m_dirtyImageRect(dirtyImageRect)
 {
 }
 
-KisBatchControlUpdateInfo::Type KisBatchControlUpdateInfo::type() const
+KisMarkerUpdateInfo::Type KisMarkerUpdateInfo::type() const
 {
     return m_type;
 }
 
-QRect KisBatchControlUpdateInfo::dirtyImageRect() const
+QRect KisMarkerUpdateInfo::dirtyImageRect() const
 {
     return m_dirtyImageRect;
 }
 
-int KisBatchControlUpdateInfo::levelOfDetail() const
+int KisMarkerUpdateInfo::levelOfDetail() const
 {
     // return invalid level of detail to avoid merging the update info
     // with other updates
-    return m_type == StartBatch ? -1 : -2;
+    return -1 - (int)m_type;
 }
diff --git a/libs/ui/canvas/kis_update_info.h b/libs/ui/canvas/kis_update_info.h
index 76ecb7e19e3..69dcd03fe87 100644
--- a/libs/ui/canvas/kis_update_info.h
+++ b/libs/ui/canvas/kis_update_info.h
@@ -142,16 +142,18 @@ public:
     KisImagePatch patch;
 };
 
-class KisBatchControlUpdateInfo : public KisUpdateInfo
+class KisMarkerUpdateInfo : public KisUpdateInfo
 {
 public:
     enum Type {
-        StartBatch,
-        EndBatch
+        StartBatch = 0,
+        EndBatch,
+        BlockLodUpdates,
+        UnblockLodUpdates,
     };
 
 public:
-    KisBatchControlUpdateInfo(Type type, const QRect &dirtyImageRect);
+    KisMarkerUpdateInfo(Type type, const QRect &dirtyImageRect);
 
     Type type() const;
 
diff --git a/libs/ui/kis_animation_frame_cache.cpp b/libs/ui/kis_animation_frame_cache.cpp
index 829cf3249ee..d41d21b1e25 100644
--- a/libs/ui/kis_animation_frame_cache.cpp
+++ b/libs/ui/kis_animation_frame_cache.cpp
@@ -237,7 +237,7 @@ bool KisAnimationFrameCache::uploadFrame(int time)
         // Previously we were trying to start cache regeneration in this point,
         // but it caused even bigger slowdowns when scrubbing
     } else {
-        m_d->textures->recalculateCache(info);
+        m_d->textures->recalculateCache(info, false);
     }
 
     return bool(info);
diff --git a/libs/ui/opengl/kis_opengl_canvas2.cpp b/libs/ui/opengl/kis_opengl_canvas2.cpp
index 7e1dc65017b..076b4955e88 100644
--- a/libs/ui/opengl/kis_opengl_canvas2.cpp
+++ b/libs/ui/opengl/kis_opengl_canvas2.cpp
@@ -914,7 +914,7 @@ QRect KisOpenGLCanvas2::updateCanvasProjection(KisUpdateInfoSP info)
     // See KisQPainterCanvas::updateCanvasProjection for more info
     bool isOpenGLUpdateInfo = dynamic_cast<KisOpenGLUpdateInfo*>(info.data());
     if (isOpenGLUpdateInfo) {
-        d->openGLImageTextures->recalculateCache(info);
+        d->openGLImageTextures->recalculateCache(info, d->lodSwitchInProgress);
     }
     return QRect(); // FIXME: Implement dirty rect for OpenGL
 }
diff --git a/libs/ui/opengl/kis_opengl_image_textures.cpp b/libs/ui/opengl/kis_opengl_image_textures.cpp
index beeb2f29eb1..7668aa59f6c 100644
--- a/libs/ui/opengl/kis_opengl_image_textures.cpp
+++ b/libs/ui/opengl/kis_opengl_image_textures.cpp
@@ -112,7 +112,7 @@ void KisOpenGLImageTextures::initGL(QOpenGLFunctions *f)
     createImageTextureTiles();
 
     KisOpenGLUpdateInfoSP info = updateCache(m_image->bounds(), m_image);
-    recalculateCache(info);
+    recalculateCache(info, false);
 }
 
 KisOpenGLImageTextures::~KisOpenGLImageTextures()
@@ -256,7 +256,7 @@ KisOpenGLUpdateInfoSP KisOpenGLImageTextures::updateCacheImpl(const QRect& rect,
     return m_updateInfoBuilder.buildUpdateInfo(rect, srcImage, convertColorSpace);
 }
 
-void KisOpenGLImageTextures::recalculateCache(KisUpdateInfoSP info)
+void KisOpenGLImageTextures::recalculateCache(KisUpdateInfoSP info, bool blockMipmapRegeneration)
 {
     if (!m_initialized) {
         dbgUI << "OpenGL: Tried to edit image texture cache before it was initialized.";
@@ -271,7 +271,7 @@ void KisOpenGLImageTextures::recalculateCache(KisUpdateInfoSP info)
         KisTextureTile *tile = getTextureTileCR(tileInfo->tileCol(), tileInfo->tileRow());
         KIS_ASSERT_RECOVER_RETURN(tile);
 
-        tile->update(*tileInfo);
+        tile->update(*tileInfo, blockMipmapRegeneration);
     }
 }
 
diff --git a/libs/ui/opengl/kis_opengl_image_textures.h b/libs/ui/opengl/kis_opengl_image_textures.h
index 83032e6c1b1..32a915536e0 100644
--- a/libs/ui/opengl/kis_opengl_image_textures.h
+++ b/libs/ui/opengl/kis_opengl_image_textures.h
@@ -135,7 +135,7 @@ public:
     KisOpenGLUpdateInfoSP updateCache(const QRect& rect, KisImageSP srcImage);
     KisOpenGLUpdateInfoSP updateCacheNoConversion(const QRect& rect);
 
-    void recalculateCache(KisUpdateInfoSP info);
+    void recalculateCache(KisUpdateInfoSP info, bool blockMipmapRegeneration);
 
     void slotImageSizeChanged(qint32 w, qint32 h);
 
diff --git a/libs/ui/opengl/kis_texture_tile.cpp b/libs/ui/opengl/kis_texture_tile.cpp
index bc7e8960881..99fa080d6e4 100644
--- a/libs/ui/opengl/kis_texture_tile.cpp
+++ b/libs/ui/opengl/kis_texture_tile.cpp
@@ -150,7 +150,7 @@ void KisTextureTile::setPreparedLodPlane(int lod)
     m_needsMipmapRegeneration = false;
 }
 
-void KisTextureTile::update(const KisTextureTileUpdateInfo &updateInfo)
+void KisTextureTile::update(const KisTextureTileUpdateInfo &updateInfo, bool blockMipmapRegeneration)
 {
     f->initializeOpenGLFunctions();
     f->glBindTexture(GL_TEXTURE_2D, m_textureId);
@@ -188,7 +188,8 @@ void KisTextureTile::update(const KisTextureTileUpdateInfo &updateInfo)
      * To avoid this issue, we should regenerate the dirty mipmap
      * *before* doing anything with the low-resolution plane.
      */
-    if (patchLevelOfDetail > 0 &&
+    if (!blockMipmapRegeneration &&
+        patchLevelOfDetail > 0 &&
         m_needsMipmapRegeneration &&
         !updateInfo.isEntireTileUpdated()) {
 
diff --git a/libs/ui/opengl/kis_texture_tile.h b/libs/ui/opengl/kis_texture_tile.h
index 2fd77d52dbc..9229add0684 100644
--- a/libs/ui/opengl/kis_texture_tile.h
+++ b/libs/ui/opengl/kis_texture_tile.h
@@ -75,7 +75,7 @@ public:
         m_numMipmapLevels = num;
     }
 
-    void update(const KisTextureTileUpdateInfo &updateInfo);
+    void update(const KisTextureTileUpdateInfo &updateInfo, bool blockMipmapRegeneration);
 
     inline QRect tileRectInImagePixels() {
         return m_tileRectInImagePixels;


More information about the kimageshop mailing list