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