Krita Source Code Documentation
Loading...
Searching...
No Matches
KisAsyncAnimationRenderDialogBase.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2017 Dmitry Kazakov <dimula73@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
9#include <QEventLoop>
10#include <QProgressDialog>
11#include <QElapsedTimer>
12#include <QApplication>
13#include <QThread>
14#include <QTime>
15#include <QList>
16#include <QtMath>
18
19#include <klocalizedstring.h>
20
23#include "KisViewManager.h"
25#include "kis_time_span.h"
26#include "kis_image.h"
27#include "kis_image_config.h"
30#include <boost/optional.hpp>
31
32#include <vector>
33#include <memory>
34
35namespace {
36struct RendererPair {
37 std::unique_ptr<KisAsyncAnimationRendererBase> renderer;
38 KisImageSP image;
39
40 RendererPair() {}
41 RendererPair(KisAsyncAnimationRendererBase *_renderer, KisImageSP _image)
42 : renderer(_renderer),
43 image(_image)
44 {
45 }
46 RendererPair(RendererPair &&rhs)
47 : renderer(std::move(rhs.renderer)),
48 image(rhs.image)
49 {
50 }
51};
52
53int calculateNumberMemoryAllowedClones(KisImageSP image)
54{
57 ->fetchMemoryStatistics(image);
58
59 const qint64 allowedMemory = 0.8 * stats.tilesHardLimit - stats.realMemorySize;
60 const qint64 cloneSize = stats.projectionsSize;
61
62 if (cloneSize > 0 && allowedMemory > 0) {
63 return allowedMemory / cloneSize;
64 }
65
66 return 0; // will become 1; either when the cloneSize = 0 or the allowedMemory is 0 or below
67}
68
69}
70
71
73{
74 Private(const QString &_actionTitle, KisImageSP _image, int _busyWait)
75 : actionTitle(_actionTitle),
76 image(_image),
77 busyWait(_busyWait),
79 {
80 }
81
82 QString actionTitle;
85 bool isBatchMode = false;
86
87 std::vector<RendererPair> asyncRenderers;
88 bool memoryLimitReached = false;
89
90 QElapsedTimer processingTime;
91 QScopedPointer<QProgressDialog> progressDialog;
92 QEventLoop waitLoop;
93
99
101 using ProgressData = QPair<int, QString>;
102 boost::optional<ProgressData> progressData;
104
105
106 int numDirtyFramesLeft() const {
107 return stillDirtyFrames.size() + framesInProgress.size();
108 }
109
110};
111
113 : m_d(new Private(actionTitle, image, busyWait))
114{
115 connect(&m_d->progressDialogCompressor, SIGNAL(timeout()),
116 SLOT(slotUpdateCompressedProgressData()), Qt::QueuedConnection);
117}
118
122
125{
126 KisBlockBackgroundFrameGenerationLock populatorBlock(m_d->image->animationInterface());
127
128 {
135 bool imageIsIdle = true;
136
137 if (viewManager) {
138 imageIsIdle = viewManager->blockUntilOperationsFinished(m_d->image);
139 } else {
140 imageIsIdle = false;
141 if (m_d->image->tryBarrierLock(true)) {
142 m_d->image->unlock();
143 imageIsIdle = true;
144 }
145 }
146
147 if (!imageIsIdle) {
148 return RenderCancelled;
149 }
150 }
151
152
153 if (!m_d->isBatchMode) {
154 QWidget *parentWidget = viewManager ? viewManager->mainWindowAsQWidget() : 0;
155 KisLockFrameGenerationLockAdapter adapter(m_d->image->animationInterface());
156 KisAsyncActionFeedback feedback(i18n("Wait for existing frame generation process to complete..."), parentWidget);
157 feedback.waitForMutex(adapter);
158 }
159
160 m_d->stillDirtyFrames = calcDirtyFrames();
161 m_d->framesInProgress.clear();
162 m_d->result = RenderComplete;
163 m_d->dirtyFramesCount = m_d->stillDirtyFrames.size();
164
165 if (!m_d->isBatchMode) {
166 QWidget *parentWidget = viewManager ? viewManager->mainWindowAsQWidget() : 0;
167 m_d->progressDialog.reset(new QProgressDialog(m_d->actionTitle, i18n("Cancel"), 0, 0, parentWidget));
168 m_d->progressDialog->setWindowModality(Qt::ApplicationModal);
169 m_d->progressDialog->setMinimum(0);
170 m_d->progressDialog->setMaximum(m_d->dirtyFramesCount);
171 m_d->progressDialog->setMinimumDuration(m_d->busyWait);
172 connect(m_d->progressDialog.data(), SIGNAL(canceled()), SLOT(slotCancelRegeneration()));
173 }
174
175 if (m_d->dirtyFramesCount <= 0) return m_d->result;
176
177 m_d->processingTime.start();
178
179 KisImageConfig cfg(true);
180
181 const int maxThreads = cfg.maxNumberOfThreads();
182 const int numAllowedWorker = 1 + calculateNumberMemoryAllowedClones(m_d->image);
183 const int proposedNumWorkers = qMin(m_d->dirtyFramesCount, cfg.frameRenderingClones());
184 const int numWorkers = qMin(proposedNumWorkers, numAllowedWorker);
185 const int numThreadsPerWorker = qMax(1, qCeil(qreal(maxThreads) / numWorkers));
186
187 m_d->memoryLimitReached = numWorkers < proposedNumWorkers;
188
189 const int oldWorkingThreadsLimit = m_d->image->workingThreadsLimit();
190
191 for (int i = 0; i < numWorkers; i++) {
192 // reuse the image for one of the workers
193 const bool lastWorker = (i == numWorkers - 1);
194 KisImageSP image = m_d->image;
195
196 if (!lastWorker) {
197 //Only the last-most worker should try to use the source image pointer. Others need a copy.
198
199 if (m_d->asyncRenderers.size() == 0) {
200 // Copy source image when no renderers (aka no copies) exist, requires lock as image memory can be actively modified.
201 m_d->image->barrierLock(true);
202 image = m_d->image->clone(true);
203 m_d->image->unlock();
204 } else {
205 // Copy a previous copy, shouldn't require lock since image is "fresh" and untouchable by other krita systems.
206 image = m_d->asyncRenderers[m_d->asyncRenderers.size() - 1].image->clone(true);
207 }
208 }
209
210
211 image->setWorkingThreadsLimit(numThreadsPerWorker);
213
214 connect(renderer, SIGNAL(sigFrameCompleted(int)), SLOT(slotFrameCompleted(int)));
216
217 m_d->asyncRenderers.push_back(RendererPair(renderer, image));
218 }
219
220
223
224 if (m_d->numDirtyFramesLeft() > 0) {
225 m_d->waitLoop.exec();
226 }
227
228 for (auto &pair : m_d->asyncRenderers) {
229 KIS_SAFE_ASSERT_RECOVER_NOOP(!pair.renderer->isActive());
230 if (viewManager) {
231 viewManager->blockUntilOperationsFinishedForced(pair.image);
232 } else {
233 pair.image->barrierLock(true);
234 pair.image->unlock();
235 }
236
237 }
238 m_d->asyncRenderers.clear();
239
240 if (viewManager) {
241 viewManager->blockUntilOperationsFinishedForced(m_d->image);
242 } else {
243 m_d->image->barrierLock(true);
244 m_d->image->unlock();
245 }
246
247 m_d->image->setWorkingThreadsLimit(oldWorkingThreadsLimit);
248
249 m_d->progressDialog.reset();
250
251 return m_d->result;
252}
253
255{
256 m_d->regionOfInterest = roi;
257}
258
260{
261 return m_d->regionOfInterest;
262}
263
265{
266 Q_UNUSED(frame);
267
268 m_d->framesInProgress.removeOne(frame);
269
272}
273
275{
276 Q_UNUSED(frame);
277
278 cancelProcessingImpl(cancelReason);
279}
280
285
287{
288 for (auto &pair : m_d->asyncRenderers) {
289 if (pair.renderer->isActive()) {
290 pair.renderer->cancelCurrentFrameRendering(cancelReason);
291 }
292 KIS_SAFE_ASSERT_RECOVER_NOOP(!pair.renderer->isActive());
293 }
294
295 m_d->stillDirtyFrames.clear();
296 m_d->framesInProgress.clear();
297 m_d->result =
302}
303
304
306{
307 bool hadWorkOnPreviousCycle = false;
308
309 while (!m_d->stillDirtyFrames.isEmpty()) {
310 for (auto &pair : m_d->asyncRenderers) {
311 if (!pair.renderer->isActive()) {
312 const int currentDirtyFrame = m_d->stillDirtyFrames.takeFirst();
313
314 KisLockFrameGenerationLock lock(pair.image->animationInterface());
315
316 initializeRendererForFrame(pair.renderer.get(), pair.image, currentDirtyFrame);
317 pair.renderer->startFrameRegeneration(pair.image, currentDirtyFrame, m_d->regionOfInterest,
318 KisAsyncAnimationRendererBase::None, std::move(lock));
319 hadWorkOnPreviousCycle = true;
320 m_d->framesInProgress.append(currentDirtyFrame);
321 break;
322 }
323 }
324
325 if (!hadWorkOnPreviousCycle) break;
326 hadWorkOnPreviousCycle = false;
327 }
328}
329
331{
332 const int processedFramesCount = m_d->dirtyFramesCount - m_d->numDirtyFramesLeft();
333
334 const qint64 elapsedMSec = m_d->processingTime.elapsed();
335 const qint64 estimatedMSec =
336 !processedFramesCount ? 0 :
337 elapsedMSec * m_d->dirtyFramesCount / processedFramesCount;
338
339 const QTime elapsedTime = QTime::fromMSecsSinceStartOfDay(elapsedMSec);
340 const QTime estimatedTime = QTime::fromMSecsSinceStartOfDay(estimatedMSec);
341
342 const QString timeFormat = estimatedTime.hour() > 0 ? "HH:mm:ss" : "mm:ss";
343
344 const QString elapsedTimeString = elapsedTime.toString(timeFormat);
345 const QString estimatedTimeString = estimatedTime.toString(timeFormat);
346
347 const QString memoryLimitMessage(
348 i18n("\n\nThe memory limit has been reached.\nThe number of frames saved simultaneously is limited to %1\n\n",
349 m_d->asyncRenderers.size()));
350
351
352 const QString progressLabel(i18n("%1\n\nElapsed: %2\nEstimated: %3\n\n%4",
353 m_d->actionTitle,
354 elapsedTimeString,
355 estimatedTimeString,
356 m_d->memoryLimitReached ? memoryLimitMessage : QString()));
357 if (m_d->progressDialog) {
363 m_d->progressData = Private::ProgressData(processedFramesCount, progressLabel);
364 m_d->progressDialogCompressor.start();
365 }
366
367 if (!m_d->numDirtyFramesLeft()) {
368 m_d->waitLoop.quit();
369 }
370}
371
373{
383 if (m_d->progressDialogReentrancyCounter > 0) {
384 m_d->progressDialogCompressor.start();
385 return;
386 }
387
388 if (m_d->progressDialog && m_d->progressData) {
389 m_d->progressDialogReentrancyCounter++;
390
391 m_d->progressDialog->setLabelText(m_d->progressData->second);
392 m_d->progressDialog->setValue(m_d->progressData->first);
393 m_d->progressData = boost::none;
394
395 m_d->progressDialogReentrancyCounter--;
396 }
397}
398
400{
401 m_d->isBatchMode = value;
402}
403
405{
406 return m_d->isBatchMode;
407}
float value(const T *src, size_t ch)
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
void slotFrameCancelled(int frame, KisAsyncAnimationRendererBase::CancelReason cancelReason)
virtual KisAsyncAnimationRendererBase * createRenderer(KisImageSP image)=0
create a renderer object linked to image
virtual Result regenerateRange(KisViewManager *viewManager)
start generation of frames and (if not in batch mode) show the dialog
virtual void initializeRendererForFrame(KisAsyncAnimationRendererBase *renderer, KisImageSP image, int frame)=0
void cancelProcessingImpl(KisAsyncAnimationRendererBase::CancelReason cancelReason)
KisAsyncAnimationRenderDialogBase(const QString &actionTitle, KisImageSP image, int busyWait=200)
construct and initialize the dialog
virtual QList< int > calcDirtyFrames() const =0
returns a list of frames that should be regenerated by the dialog
void setBatchMode(bool value)
setting batch mode to true will prevent any dialogs or message boxes from showing on screen....
int frameRenderingClones(bool defaultValue=false) const
int maxNumberOfThreads(bool defaultValue=false) const
void setWorkingThreadsLimit(int value)
void unlock()
Definition kis_image.cc:805
void barrierLock(bool readOnly=false)
Wait until all the queued background jobs are completed and lock the image.
Definition kis_image.cc:756
KisImage * clone(bool exactCopy=false)
Definition kis_image.cc:405
bool blockUntilOperationsFinished(KisImageSP image)
blockUntilOperationsFinished blocks the GUI of the application until execution of actions on image is...
void blockUntilOperationsFinishedForced(KisImageSP image)
blockUntilOperationsFinished blocks the GUI of the application until execution of actions on image is...
QWidget * mainWindowAsQWidget() const
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
Private(const QString &_actionTitle, KisImageSP _image, int _busyWait)
Statistics fetchMemoryStatistics(KisImageSP image) const
static KisMemoryStatisticsServer * instance()