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 }