File indexing completed on 2025-02-02 04:18:31
0001 /* 0002 * SPDX-FileCopyrightText: 2017 Dmitry Kazakov <dimula73@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "KisAsyncAnimationRenderDialogBase.h" 0008 0009 #include <QEventLoop> 0010 #include <QProgressDialog> 0011 #include <QElapsedTimer> 0012 #include <QApplication> 0013 #include <QThread> 0014 #include <QTime> 0015 #include <QList> 0016 #include <QtMath> 0017 #include <kis_async_action_feedback.h> 0018 0019 #include <klocalizedstring.h> 0020 0021 #include <KisLockFrameGenerationLock.h> 0022 #include <KisBlockBackgroundFrameGenerationLock.h> 0023 #include "KisViewManager.h" 0024 #include "KisAsyncAnimationRendererBase.h" 0025 #include "kis_time_span.h" 0026 #include "kis_image.h" 0027 #include "kis_image_config.h" 0028 #include "kis_memory_statistics_server.h" 0029 #include "kis_signal_compressor.h" 0030 #include <boost/optional.hpp> 0031 0032 #include <vector> 0033 #include <memory> 0034 0035 namespace { 0036 struct RendererPair { 0037 std::unique_ptr<KisAsyncAnimationRendererBase> renderer; 0038 KisImageSP image; 0039 0040 RendererPair() {} 0041 RendererPair(KisAsyncAnimationRendererBase *_renderer, KisImageSP _image) 0042 : renderer(_renderer), 0043 image(_image) 0044 { 0045 } 0046 RendererPair(RendererPair &&rhs) 0047 : renderer(std::move(rhs.renderer)), 0048 image(rhs.image) 0049 { 0050 } 0051 }; 0052 0053 int calculateNumberMemoryAllowedClones(KisImageSP image) 0054 { 0055 KisMemoryStatisticsServer::Statistics stats = 0056 KisMemoryStatisticsServer::instance() 0057 ->fetchMemoryStatistics(image); 0058 0059 const qint64 allowedMemory = 0.8 * stats.tilesHardLimit - stats.realMemorySize; 0060 const qint64 cloneSize = stats.projectionsSize; 0061 0062 if (cloneSize > 0 && allowedMemory > 0) { 0063 return allowedMemory / cloneSize; 0064 } 0065 0066 return 0; // will become 1; either when the cloneSize = 0 or the allowedMemory is 0 or below 0067 } 0068 0069 } 0070 0071 0072 struct KisAsyncAnimationRenderDialogBase::Private 0073 { 0074 Private(const QString &_actionTitle, KisImageSP _image, int _busyWait) 0075 : actionTitle(_actionTitle), 0076 image(_image), 0077 busyWait(_busyWait), 0078 progressDialogCompressor(40, KisSignalCompressor::FIRST_ACTIVE) 0079 { 0080 } 0081 0082 QString actionTitle; 0083 KisImageSP image; 0084 int busyWait; 0085 bool isBatchMode = false; 0086 0087 std::vector<RendererPair> asyncRenderers; 0088 bool memoryLimitReached = false; 0089 0090 QElapsedTimer processingTime; 0091 QScopedPointer<QProgressDialog> progressDialog; 0092 QEventLoop waitLoop; 0093 0094 QList<int> stillDirtyFrames; 0095 QList<int> framesInProgress; 0096 int dirtyFramesCount = 0; 0097 Result result = RenderComplete; 0098 KisRegion regionOfInterest; 0099 0100 KisSignalCompressor progressDialogCompressor; 0101 using ProgressData = QPair<int, QString>; 0102 boost::optional<ProgressData> progressData; 0103 int progressDialogReentrancyCounter = 0; 0104 0105 0106 int numDirtyFramesLeft() const { 0107 return stillDirtyFrames.size() + framesInProgress.size(); 0108 } 0109 0110 }; 0111 0112 KisAsyncAnimationRenderDialogBase::KisAsyncAnimationRenderDialogBase(const QString &actionTitle, KisImageSP image, int busyWait) 0113 : m_d(new Private(actionTitle, image, busyWait)) 0114 { 0115 connect(&m_d->progressDialogCompressor, SIGNAL(timeout()), 0116 SLOT(slotUpdateCompressedProgressData()), Qt::QueuedConnection); 0117 } 0118 0119 KisAsyncAnimationRenderDialogBase::~KisAsyncAnimationRenderDialogBase() 0120 { 0121 } 0122 0123 KisAsyncAnimationRenderDialogBase::Result 0124 KisAsyncAnimationRenderDialogBase::regenerateRange(KisViewManager *viewManager) 0125 { 0126 KisBlockBackgroundFrameGenerationLock populatorBlock(m_d->image->animationInterface()); 0127 0128 { 0129 /** 0130 * Since this method can be called from the places where no 0131 * view manager is available, we need this manually crafted 0132 * ugly construction to "try-lock-cancel" the image. 0133 */ 0134 0135 bool imageIsIdle = true; 0136 0137 if (viewManager) { 0138 imageIsIdle = viewManager->blockUntilOperationsFinished(m_d->image); 0139 } else { 0140 imageIsIdle = false; 0141 if (m_d->image->tryBarrierLock(true)) { 0142 m_d->image->unlock(); 0143 imageIsIdle = true; 0144 } 0145 } 0146 0147 if (!imageIsIdle) { 0148 return RenderCancelled; 0149 } 0150 } 0151 0152 0153 if (!m_d->isBatchMode) { 0154 QWidget *parentWidget = viewManager ? viewManager->mainWindowAsQWidget() : 0; 0155 KisLockFrameGenerationLockAdapter adapter(m_d->image->animationInterface()); 0156 KisAsyncActionFeedback feedback(i18n("Wait for existing frame generation process to complete..."), parentWidget); 0157 feedback.waitForMutex(adapter); 0158 } 0159 0160 m_d->stillDirtyFrames = calcDirtyFrames(); 0161 m_d->framesInProgress.clear(); 0162 m_d->result = RenderComplete; 0163 m_d->dirtyFramesCount = m_d->stillDirtyFrames.size(); 0164 0165 if (!m_d->isBatchMode) { 0166 QWidget *parentWidget = viewManager ? viewManager->mainWindowAsQWidget() : 0; 0167 m_d->progressDialog.reset(new QProgressDialog(m_d->actionTitle, i18n("Cancel"), 0, 0, parentWidget)); 0168 m_d->progressDialog->setWindowModality(Qt::ApplicationModal); 0169 m_d->progressDialog->setMinimum(0); 0170 m_d->progressDialog->setMaximum(m_d->dirtyFramesCount); 0171 m_d->progressDialog->setMinimumDuration(m_d->busyWait); 0172 connect(m_d->progressDialog.data(), SIGNAL(canceled()), SLOT(slotCancelRegeneration())); 0173 } 0174 0175 if (m_d->dirtyFramesCount <= 0) return m_d->result; 0176 0177 m_d->processingTime.start(); 0178 0179 KisImageConfig cfg(true); 0180 0181 const int maxThreads = cfg.maxNumberOfThreads(); 0182 const int numAllowedWorker = 1 + calculateNumberMemoryAllowedClones(m_d->image); 0183 const int proposedNumWorkers = qMin(m_d->dirtyFramesCount, cfg.frameRenderingClones()); 0184 const int numWorkers = qMin(proposedNumWorkers, numAllowedWorker); 0185 const int numThreadsPerWorker = qMax(1, qCeil(qreal(maxThreads) / numWorkers)); 0186 0187 m_d->memoryLimitReached = numWorkers < proposedNumWorkers; 0188 0189 const int oldWorkingThreadsLimit = m_d->image->workingThreadsLimit(); 0190 0191 for (int i = 0; i < numWorkers; i++) { 0192 // reuse the image for one of the workers 0193 const bool lastWorker = (i == numWorkers - 1); 0194 KisImageSP image = m_d->image; 0195 0196 if (!lastWorker) { 0197 //Only the last-most worker should try to use the source image pointer. Others need a copy. 0198 0199 if (m_d->asyncRenderers.size() == 0) { 0200 // Copy source image when no renderers (aka no copies) exist, requires lock as image memory can be actively modified. 0201 m_d->image->barrierLock(true); 0202 image = m_d->image->clone(true); 0203 m_d->image->unlock(); 0204 } else { 0205 // Copy a previous copy, shouldn't require lock since image is "fresh" and untouchable by other krita systems. 0206 image = m_d->asyncRenderers[m_d->asyncRenderers.size() - 1].image->clone(true); 0207 } 0208 } 0209 0210 0211 image->setWorkingThreadsLimit(numThreadsPerWorker); 0212 KisAsyncAnimationRendererBase *renderer = createRenderer(image); 0213 0214 connect(renderer, SIGNAL(sigFrameCompleted(int)), SLOT(slotFrameCompleted(int))); 0215 connect(renderer, SIGNAL(sigFrameCancelled(int, KisAsyncAnimationRendererBase::CancelReason)), SLOT(slotFrameCancelled(int, KisAsyncAnimationRendererBase::CancelReason))); 0216 0217 m_d->asyncRenderers.push_back(RendererPair(renderer, image)); 0218 } 0219 0220 0221 tryInitiateFrameRegeneration(); 0222 updateProgressLabel(); 0223 0224 if (m_d->numDirtyFramesLeft() > 0) { 0225 m_d->waitLoop.exec(); 0226 } 0227 0228 for (auto &pair : m_d->asyncRenderers) { 0229 KIS_SAFE_ASSERT_RECOVER_NOOP(!pair.renderer->isActive()); 0230 if (viewManager) { 0231 viewManager->blockUntilOperationsFinishedForced(pair.image); 0232 } else { 0233 pair.image->barrierLock(true); 0234 pair.image->unlock(); 0235 } 0236 0237 } 0238 m_d->asyncRenderers.clear(); 0239 0240 if (viewManager) { 0241 viewManager->blockUntilOperationsFinishedForced(m_d->image); 0242 } else { 0243 m_d->image->barrierLock(true); 0244 m_d->image->unlock(); 0245 } 0246 0247 m_d->image->setWorkingThreadsLimit(oldWorkingThreadsLimit); 0248 0249 m_d->progressDialog.reset(); 0250 0251 return m_d->result; 0252 } 0253 0254 void KisAsyncAnimationRenderDialogBase::setRegionOfInterest(const KisRegion &roi) 0255 { 0256 m_d->regionOfInterest = roi; 0257 } 0258 0259 KisRegion KisAsyncAnimationRenderDialogBase::regionOfInterest() const 0260 { 0261 return m_d->regionOfInterest; 0262 } 0263 0264 void KisAsyncAnimationRenderDialogBase::slotFrameCompleted(int frame) 0265 { 0266 Q_UNUSED(frame); 0267 0268 m_d->framesInProgress.removeOne(frame); 0269 0270 tryInitiateFrameRegeneration(); 0271 updateProgressLabel(); 0272 } 0273 0274 void KisAsyncAnimationRenderDialogBase::slotFrameCancelled(int frame, KisAsyncAnimationRendererBase::CancelReason cancelReason) 0275 { 0276 Q_UNUSED(frame); 0277 0278 cancelProcessingImpl(cancelReason); 0279 } 0280 0281 void KisAsyncAnimationRenderDialogBase::slotCancelRegeneration() 0282 { 0283 cancelProcessingImpl(KisAsyncAnimationRendererBase::UserCancelled); 0284 } 0285 0286 void KisAsyncAnimationRenderDialogBase::cancelProcessingImpl(KisAsyncAnimationRendererBase::CancelReason cancelReason) 0287 { 0288 for (auto &pair : m_d->asyncRenderers) { 0289 if (pair.renderer->isActive()) { 0290 pair.renderer->cancelCurrentFrameRendering(cancelReason); 0291 } 0292 KIS_SAFE_ASSERT_RECOVER_NOOP(!pair.renderer->isActive()); 0293 } 0294 0295 m_d->stillDirtyFrames.clear(); 0296 m_d->framesInProgress.clear(); 0297 m_d->result = 0298 cancelReason == KisAsyncAnimationRendererBase::UserCancelled ? RenderCancelled : 0299 cancelReason == KisAsyncAnimationRendererBase::RenderingFailed ? RenderFailed : 0300 RenderTimedOut; 0301 updateProgressLabel(); 0302 } 0303 0304 0305 void KisAsyncAnimationRenderDialogBase::tryInitiateFrameRegeneration() 0306 { 0307 bool hadWorkOnPreviousCycle = false; 0308 0309 while (!m_d->stillDirtyFrames.isEmpty()) { 0310 for (auto &pair : m_d->asyncRenderers) { 0311 if (!pair.renderer->isActive()) { 0312 const int currentDirtyFrame = m_d->stillDirtyFrames.takeFirst(); 0313 0314 KisLockFrameGenerationLock lock(pair.image->animationInterface()); 0315 0316 initializeRendererForFrame(pair.renderer.get(), pair.image, currentDirtyFrame); 0317 pair.renderer->startFrameRegeneration(pair.image, currentDirtyFrame, m_d->regionOfInterest, 0318 KisAsyncAnimationRendererBase::None, std::move(lock)); 0319 hadWorkOnPreviousCycle = true; 0320 m_d->framesInProgress.append(currentDirtyFrame); 0321 break; 0322 } 0323 } 0324 0325 if (!hadWorkOnPreviousCycle) break; 0326 hadWorkOnPreviousCycle = false; 0327 } 0328 } 0329 0330 void KisAsyncAnimationRenderDialogBase::updateProgressLabel() 0331 { 0332 const int processedFramesCount = m_d->dirtyFramesCount - m_d->numDirtyFramesLeft(); 0333 0334 const qint64 elapsedMSec = m_d->processingTime.elapsed(); 0335 const qint64 estimatedMSec = 0336 !processedFramesCount ? 0 : 0337 elapsedMSec * m_d->dirtyFramesCount / processedFramesCount; 0338 0339 const QTime elapsedTime = QTime::fromMSecsSinceStartOfDay(elapsedMSec); 0340 const QTime estimatedTime = QTime::fromMSecsSinceStartOfDay(estimatedMSec); 0341 0342 const QString timeFormat = estimatedTime.hour() > 0 ? "HH:mm:ss" : "mm:ss"; 0343 0344 const QString elapsedTimeString = elapsedTime.toString(timeFormat); 0345 const QString estimatedTimeString = estimatedTime.toString(timeFormat); 0346 0347 const QString memoryLimitMessage( 0348 i18n("\n\nThe memory limit has been reached.\nThe number of frames saved simultaneously is limited to %1\n\n", 0349 m_d->asyncRenderers.size())); 0350 0351 0352 const QString progressLabel(i18n("%1\n\nElapsed: %2\nEstimated: %3\n\n%4", 0353 m_d->actionTitle, 0354 elapsedTimeString, 0355 estimatedTimeString, 0356 m_d->memoryLimitReached ? memoryLimitMessage : QString())); 0357 if (m_d->progressDialog) { 0358 /** 0359 * We should avoid reentrancy caused by explicit 0360 * QApplication::processEvents() in QProgressDialog::setValue(), so use 0361 * a compressor instead 0362 */ 0363 m_d->progressData = Private::ProgressData(processedFramesCount, progressLabel); 0364 m_d->progressDialogCompressor.start(); 0365 } 0366 0367 if (!m_d->numDirtyFramesLeft()) { 0368 m_d->waitLoop.quit(); 0369 } 0370 } 0371 0372 void KisAsyncAnimationRenderDialogBase::slotUpdateCompressedProgressData() 0373 { 0374 /** 0375 * Qt's implementation of QProgressDialog is a bit weird: it calls 0376 * QApplication::processEvents() from inside setValue(), which means 0377 * that our update method may reenter multiple times. 0378 * 0379 * This code avoids reentering by using a compressor and an explicit 0380 * entrance counter. 0381 */ 0382 0383 if (m_d->progressDialogReentrancyCounter > 0) { 0384 m_d->progressDialogCompressor.start(); 0385 return; 0386 } 0387 0388 if (m_d->progressDialog && m_d->progressData) { 0389 m_d->progressDialogReentrancyCounter++; 0390 0391 m_d->progressDialog->setLabelText(m_d->progressData->second); 0392 m_d->progressDialog->setValue(m_d->progressData->first); 0393 m_d->progressData = boost::none; 0394 0395 m_d->progressDialogReentrancyCounter--; 0396 } 0397 } 0398 0399 void KisAsyncAnimationRenderDialogBase::setBatchMode(bool value) 0400 { 0401 m_d->isBatchMode = value; 0402 } 0403 0404 bool KisAsyncAnimationRenderDialogBase::batchMode() const 0405 { 0406 return m_d->isBatchMode; 0407 }