1 /*
2  *  Copyright (c) 2017 Dmitry Kazakov <dimula73@gmail.com>
3  *
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation; either version 2 of the License, or
7  *  (at your option) any later version.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program; if not, write to the Free Software
16  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  */
18 
19 #include "KisAsyncAnimationRenderDialogBase.h"
20 
21 #include <QEventLoop>
22 #include <QProgressDialog>
23 #include <QElapsedTimer>
24 #include <QApplication>
25 #include <QThread>
26 #include <QTime>
27 #include <QList>
28 #include <QtMath>
29 
30 #include <klocalizedstring.h>
31 
32 #include "KisViewManager.h"
33 #include "KisAsyncAnimationRendererBase.h"
34 #include "kis_time_range.h"
35 #include "kis_image.h"
36 #include "kis_image_config.h"
37 #include "kis_memory_statistics_server.h"
38 #include "kis_signal_compressor.h"
39 #include <boost/optional.hpp>
40 
41 #include <vector>
42 #include <memory>
43 
44 namespace {
45 struct RendererPair {
46     std::unique_ptr<KisAsyncAnimationRendererBase> renderer;
47     KisImageSP image;
48 
RendererPair__anon824dff600111::RendererPair49     RendererPair() {}
RendererPair__anon824dff600111::RendererPair50     RendererPair(KisAsyncAnimationRendererBase *_renderer, KisImageSP _image)
51         : renderer(_renderer),
52           image(_image)
53     {
54     }
RendererPair__anon824dff600111::RendererPair55     RendererPair(RendererPair &&rhs)
56         : renderer(std::move(rhs.renderer)),
57           image(rhs.image)
58     {
59     }
60 };
61 
calculateNumberMemoryAllowedClones(KisImageSP image)62 int calculateNumberMemoryAllowedClones(KisImageSP image)
63 {
64     KisMemoryStatisticsServer::Statistics stats =
65         KisMemoryStatisticsServer::instance()
66         ->fetchMemoryStatistics(image);
67 
68     const qint64 allowedMemory = 0.8 * stats.tilesHardLimit - stats.realMemorySize;
69     const qint64 cloneSize = stats.projectionsSize;
70 
71     if (cloneSize > 0 && allowedMemory > 0) {
72         return allowedMemory / cloneSize;
73     }
74 
75     return 0; // will become 1; either when the cloneSize = 0 or the allowedMemory is 0 or below
76 }
77 
78 }
79 
80 
81 struct KisAsyncAnimationRenderDialogBase::Private
82 {
PrivateKisAsyncAnimationRenderDialogBase::Private83     Private(const QString &_actionTitle, KisImageSP _image, int _busyWait)
84         : actionTitle(_actionTitle),
85           image(_image),
86           busyWait(_busyWait),
87           progressDialogCompressor(40, KisSignalCompressor::FIRST_ACTIVE)
88     {
89     }
90 
91     QString actionTitle;
92     KisImageSP image;
93     int busyWait;
94     bool isBatchMode = false;
95 
96     std::vector<RendererPair> asyncRenderers;
97     bool memoryLimitReached = false;
98 
99     QElapsedTimer processingTime;
100     QScopedPointer<QProgressDialog> progressDialog;
101     QEventLoop waitLoop;
102 
103     QList<int> stillDirtyFrames;
104     QList<int> framesInProgress;
105     int dirtyFramesCount = 0;
106     Result result = RenderComplete;
107     KisRegion regionOfInterest;
108 
109     KisSignalCompressor progressDialogCompressor;
110     using ProgressData = QPair<int, QString>;
111     boost::optional<ProgressData> progressData;
112     int progressDialogReentrancyCounter = 0;
113 
114 
numDirtyFramesLeftKisAsyncAnimationRenderDialogBase::Private115     int numDirtyFramesLeft() const {
116         return stillDirtyFrames.size() + framesInProgress.size();
117     }
118 
119 };
120 
KisAsyncAnimationRenderDialogBase(const QString & actionTitle,KisImageSP image,int busyWait)121 KisAsyncAnimationRenderDialogBase::KisAsyncAnimationRenderDialogBase(const QString &actionTitle, KisImageSP image, int busyWait)
122     : m_d(new Private(actionTitle, image, busyWait))
123 {
124     connect(&m_d->progressDialogCompressor, SIGNAL(timeout()),
125             SLOT(slotUpdateCompressedProgressData()), Qt::QueuedConnection);
126 }
127 
~KisAsyncAnimationRenderDialogBase()128 KisAsyncAnimationRenderDialogBase::~KisAsyncAnimationRenderDialogBase()
129 {
130 }
131 
132 KisAsyncAnimationRenderDialogBase::Result
regenerateRange(KisViewManager * viewManager)133 KisAsyncAnimationRenderDialogBase::regenerateRange(KisViewManager *viewManager)
134 {
135     {
136         /**
137          * Since this method can be called from the places where no
138          * view manager is available, we need this manually crafted
139          * ugly construction to "try-lock-cancel" the image.
140          */
141 
142         bool imageIsIdle = true;
143 
144         if (viewManager) {
145             imageIsIdle = viewManager->blockUntilOperationsFinished(m_d->image);
146         } else {
147             imageIsIdle = false;
148             if (m_d->image->tryBarrierLock(true)) {
149                 m_d->image->unlock();
150                 imageIsIdle = true;
151             }
152         }
153 
154         if (!imageIsIdle) {
155             return RenderCancelled;
156         }
157     }
158 
159     m_d->stillDirtyFrames = calcDirtyFrames();
160     m_d->framesInProgress.clear();
161     m_d->result = RenderComplete;
162     m_d->dirtyFramesCount = m_d->stillDirtyFrames.size();
163 
164     if (!m_d->isBatchMode) {
165         QWidget *parentWidget = viewManager ? viewManager->mainWindow() : 0;
166         m_d->progressDialog.reset(new QProgressDialog(m_d->actionTitle, i18n("Cancel"), 0, 0, parentWidget));
167         m_d->progressDialog->setWindowModality(Qt::ApplicationModal);
168         m_d->progressDialog->setMinimum(0);
169         m_d->progressDialog->setMaximum(m_d->dirtyFramesCount);
170         m_d->progressDialog->setMinimumDuration(m_d->busyWait);
171         connect(m_d->progressDialog.data(), SIGNAL(canceled()), SLOT(slotCancelRegeneration()));
172     }
173 
174     if (m_d->dirtyFramesCount <= 0) return m_d->result;
175 
176     m_d->processingTime.start();
177 
178     KisImageConfig cfg(true);
179 
180     const int maxThreads = cfg.maxNumberOfThreads();
181     const int numAllowedWorker = 1 + calculateNumberMemoryAllowedClones(m_d->image);
182     const int proposedNumWorkers = qMin(m_d->dirtyFramesCount, cfg.frameRenderingClones());
183     const int numWorkers = qMin(proposedNumWorkers, numAllowedWorker);
184     const int numThreadsPerWorker = qMax(1, qCeil(qreal(maxThreads) / numWorkers));
185 
186     m_d->memoryLimitReached = numWorkers < proposedNumWorkers;
187 
188     const int oldWorkingThreadsLimit = m_d->image->workingThreadsLimit();
189 
190     for (int i = 0; i < numWorkers; i++) {
191         // reuse the image for one of the workers
192         KisImageSP image = i == numWorkers - 1 ? m_d->image : m_d->image->clone(true);
193 
194         image->setWorkingThreadsLimit(numThreadsPerWorker);
195         KisAsyncAnimationRendererBase *renderer = createRenderer(image);
196 
197         connect(renderer, SIGNAL(sigFrameCompleted(int)), SLOT(slotFrameCompleted(int)));
198         connect(renderer, SIGNAL(sigFrameCancelled(int)), SLOT(slotFrameCancelled(int)));
199 
200         m_d->asyncRenderers.push_back(RendererPair(renderer, image));
201     }
202 
203     tryInitiateFrameRegeneration();
204     updateProgressLabel();
205 
206     if (m_d->numDirtyFramesLeft() > 0) {
207         m_d->waitLoop.exec();
208     }
209 
210     for (auto &pair : m_d->asyncRenderers) {
211         KIS_SAFE_ASSERT_RECOVER_NOOP(!pair.renderer->isActive());
212         if (viewManager) {
213             viewManager->blockUntilOperationsFinishedForced(pair.image);
214         } else {
215             pair.image->barrierLock(true);
216             pair.image->unlock();
217         }
218 
219     }
220     m_d->asyncRenderers.clear();
221 
222     if (viewManager) {
223         viewManager->blockUntilOperationsFinishedForced(m_d->image);
224     } else {
225         m_d->image->barrierLock(true);
226         m_d->image->unlock();
227     }
228 
229     m_d->image->setWorkingThreadsLimit(oldWorkingThreadsLimit);
230 
231     m_d->progressDialog.reset();
232 
233     return m_d->result;
234 }
235 
setRegionOfInterest(const KisRegion & roi)236 void KisAsyncAnimationRenderDialogBase::setRegionOfInterest(const KisRegion &roi)
237 {
238     m_d->regionOfInterest = roi;
239 }
240 
regionOfInterest() const241 KisRegion KisAsyncAnimationRenderDialogBase::regionOfInterest() const
242 {
243     return m_d->regionOfInterest;
244 }
245 
slotFrameCompleted(int frame)246 void KisAsyncAnimationRenderDialogBase::slotFrameCompleted(int frame)
247 {
248     Q_UNUSED(frame);
249 
250     m_d->framesInProgress.removeOne(frame);
251 
252     tryInitiateFrameRegeneration();
253     updateProgressLabel();
254 }
255 
slotFrameCancelled(int frame)256 void KisAsyncAnimationRenderDialogBase::slotFrameCancelled(int frame)
257 {
258     Q_UNUSED(frame);
259 
260     cancelProcessingImpl(false);
261 }
262 
slotCancelRegeneration()263 void KisAsyncAnimationRenderDialogBase::slotCancelRegeneration()
264 {
265     cancelProcessingImpl(true);
266 }
267 
cancelProcessingImpl(bool isUserCancelled)268 void KisAsyncAnimationRenderDialogBase::cancelProcessingImpl(bool isUserCancelled)
269 {
270     for (auto &pair : m_d->asyncRenderers) {
271         if (pair.renderer->isActive()) {
272             pair.renderer->cancelCurrentFrameRendering();
273         }
274         KIS_SAFE_ASSERT_RECOVER_NOOP(!pair.renderer->isActive());
275     }
276 
277     m_d->stillDirtyFrames.clear();
278     m_d->framesInProgress.clear();
279     m_d->result = isUserCancelled ? RenderCancelled : RenderFailed;
280     updateProgressLabel();
281 }
282 
283 
tryInitiateFrameRegeneration()284 void KisAsyncAnimationRenderDialogBase::tryInitiateFrameRegeneration()
285 {
286     bool hadWorkOnPreviousCycle = false;
287 
288     while (!m_d->stillDirtyFrames.isEmpty()) {
289         for (auto &pair : m_d->asyncRenderers) {
290             if (!pair.renderer->isActive()) {
291                 const int currentDirtyFrame = m_d->stillDirtyFrames.takeFirst();
292 
293                 initializeRendererForFrame(pair.renderer.get(), pair.image, currentDirtyFrame);
294                 pair.renderer->startFrameRegeneration(pair.image, currentDirtyFrame, m_d->regionOfInterest);
295                 hadWorkOnPreviousCycle = true;
296                 m_d->framesInProgress.append(currentDirtyFrame);
297                 break;
298             }
299         }
300 
301         if (!hadWorkOnPreviousCycle) break;
302         hadWorkOnPreviousCycle = false;
303     }
304 }
305 
updateProgressLabel()306 void KisAsyncAnimationRenderDialogBase::updateProgressLabel()
307 {
308     const int processedFramesCount = m_d->dirtyFramesCount - m_d->numDirtyFramesLeft();
309 
310     const qint64 elapsedMSec = m_d->processingTime.elapsed();
311     const qint64 estimatedMSec =
312         !processedFramesCount ? 0 :
313         elapsedMSec * m_d->dirtyFramesCount / processedFramesCount;
314 
315     const QTime elapsedTime = QTime::fromMSecsSinceStartOfDay(elapsedMSec);
316     const QTime estimatedTime = QTime::fromMSecsSinceStartOfDay(estimatedMSec);
317 
318     const QString timeFormat = estimatedTime.hour() > 0 ? "HH:mm:ss" : "mm:ss";
319 
320     const QString elapsedTimeString = elapsedTime.toString(timeFormat);
321     const QString estimatedTimeString = estimatedTime.toString(timeFormat);
322 
323     const QString memoryLimitMessage(
324         i18n("\n\nThe memory limit has been reached.\nThe number of frames saved simultaneously is limited to %1\n\n",
325              m_d->asyncRenderers.size()));
326 
327 
328     const QString progressLabel(i18n("%1\n\nElapsed: %2\nEstimated: %3\n\n%4",
329                                      m_d->actionTitle,
330                                      elapsedTimeString,
331                                      estimatedTimeString,
332                                      m_d->memoryLimitReached ? memoryLimitMessage : QString()));
333     if (m_d->progressDialog) {
334         /**
335          * We should avoid reentrancy caused by explicit
336          * QApplication::processEvents() in QProgressDialog::setValue(), so use
337          * a compressor instead
338          */
339         m_d->progressData = Private::ProgressData(processedFramesCount, progressLabel);
340         m_d->progressDialogCompressor.start();
341     }
342 
343     if (!m_d->numDirtyFramesLeft()) {
344         m_d->waitLoop.quit();
345     }
346 }
347 
slotUpdateCompressedProgressData()348 void KisAsyncAnimationRenderDialogBase::slotUpdateCompressedProgressData()
349 {
350     /**
351      * Qt's implementation of QProgressDialog is a bit weird: it calls
352      * QApplication::processEvents() from inside setValue(), which means
353      * that our update method may reenter multiple times.
354      *
355      * This code avoids reentering by using a compresson and an explicit
356      * entrance counter.
357      */
358 
359     if (m_d->progressDialogReentrancyCounter > 0) {
360         m_d->progressDialogCompressor.start();
361         return;
362     }
363 
364     if (m_d->progressDialog && m_d->progressData) {
365         m_d->progressDialogReentrancyCounter++;
366 
367         m_d->progressDialog->setLabelText(m_d->progressData->second);
368         m_d->progressDialog->setValue(m_d->progressData->first);
369         m_d->progressData = boost::none;
370 
371         m_d->progressDialogReentrancyCounter--;
372     }
373 }
374 
setBatchMode(bool value)375 void KisAsyncAnimationRenderDialogBase::setBatchMode(bool value)
376 {
377     m_d->isBatchMode = value;
378 }
379 
batchMode() const380 bool KisAsyncAnimationRenderDialogBase::batchMode() const
381 {
382     return m_d->isBatchMode;
383 }
384