From 69bbafb62e79a275632e216a83fa48f6697737be Mon Sep 17 00:00:00 2001 From: justburner Date: Jan 03 2022 19:51:45 +0000 Subject: Multi-Thread FFMPEG and responsive finalizing window --- diff --git a/toonz/sources/image/ffmpeg/tiio_ffmpeg.cpp b/toonz/sources/image/ffmpeg/tiio_ffmpeg.cpp index 8061153..e70d0f3 100644 --- a/toonz/sources/image/ffmpeg/tiio_ffmpeg.cpp +++ b/toonz/sources/image/ffmpeg/tiio_ffmpeg.cpp @@ -4,6 +4,8 @@ #include "tsound.h" #include +#include +#include #include #include #include @@ -166,25 +168,21 @@ void Ffmpeg::runFfmpeg(QStringList preIArgs, QStringList postIArgs, // write the file QProcess ffmpeg; ffmpeg.start(m_ffmpegPath + "/ffmpeg", args); - if (ffmpeg.waitForFinished(m_ffmpegTimeout)) { + if (waitFfmpeg(ffmpeg)) { QString results = ffmpeg.readAllStandardError(); results += ffmpeg.readAllStandardOutput(); int exitCode = ffmpeg.exitCode(); - ffmpeg.close(); std::string strResults = results.toStdString(); - } else { - DVGui::warning( - QObject::tr("FFmpeg timed out.\n" - "Please check the file for errors.\n" - "If the file doesn't play or is incomplete, \n" - "Please try raising the FFmpeg timeout in Preferences.")); } + ffmpeg.close(); } QString Ffmpeg::runFfprobe(QStringList args) { QProcess ffmpeg; ffmpeg.start(m_ffmpegPath + "/ffprobe", args); - ffmpeg.waitForFinished(m_ffmpegTimeout); + if (!waitFfmpeg(ffmpeg)) { + throw TImageException(m_path, "error accessing ffprobe."); + } QString results = ffmpeg.readAllStandardError(); results += ffmpeg.readAllStandardOutput(); int exitCode = ffmpeg.exitCode(); @@ -196,6 +194,34 @@ QString Ffmpeg::runFfprobe(QStringList args) { return results; } +bool Ffmpeg::waitFfmpeg(const QProcess &ffmpeg) { + QEventLoop eloop; + QTimer timer; + timer.connect(&timer, &QTimer::timeout, &eloop, [&eloop] { eloop.exit(-2); }); + ffmpeg.connect(&ffmpeg, &QProcess::errorOccurred, &eloop, + [&eloop] { eloop.exit(-1); }); + ffmpeg.connect(&ffmpeg, + static_cast( + &QProcess::finished), + &eloop, &QEventLoop::quit); + timer.start(m_ffmpegTimeout); + + int exitCode = eloop.exec(); + if (exitCode == 0) return true; + if (exitCode == -1) { + DVGui::warning( + QObject::tr("FFmpeg returned error-code: %1").arg(ffmpeg.exitCode())); + } + if (exitCode == -2) { + DVGui::warning( + QObject::tr("FFmpeg timed out.\n" + "Please check the file for errors.\n" + "If the file doesn't play or is incomplete, \n" + "Please try raising the FFmpeg timeout in Preferences.")); + } + return false; +} + void Ffmpeg::saveSoundTrack(TSoundTrack *st) { m_sampleRate = st->getSampleRate(); m_channelCount = st->getChannelCount(); diff --git a/toonz/sources/image/ffmpeg/tiio_ffmpeg.h b/toonz/sources/image/ffmpeg/tiio_ffmpeg.h index 88747db..6ec415a 100644 --- a/toonz/sources/image/ffmpeg/tiio_ffmpeg.h +++ b/toonz/sources/image/ffmpeg/tiio_ffmpeg.h @@ -8,6 +8,7 @@ #include "trasterimage.h" #include #include +#include struct ffmpegFileInfo { int m_lx, m_ly, m_frameCount; @@ -54,6 +55,7 @@ private: QStringList m_audioArgs; TUINT32 m_sampleRate; QString cleanPathSymbols(); + bool waitFfmpeg(const QProcess &ffmpeg); }; #endif diff --git a/toonz/sources/include/tlevel_io.h b/toonz/sources/include/tlevel_io.h index cfd76e1..056364f 100644 --- a/toonz/sources/include/tlevel_io.h +++ b/toonz/sources/include/tlevel_io.h @@ -239,6 +239,19 @@ inline bool isMovieType(const TFilePath &fp) { //----------------------------------------------------------- +inline bool isSequencialRequired(std::string type) { + return (type == "mov" || type == "avi" || type == "3gp"); +} + +//----------------------------------------------------------- + +inline bool isSequencialRequired(const TFilePath &fp) { + std::string type(fp.getType()); + return isSequencialRequired(type); +} + +//----------------------------------------------------------- + inline bool doesSupportRandomAccess(const TFilePath &fp, bool isToonzOutput = false) { return (fp.getDots() == "..") || (isToonzOutput && fp.getType() == "mov"); diff --git a/toonz/sources/include/toonz/preferences.h b/toonz/sources/include/toonz/preferences.h index b91a942..365948d 100644 --- a/toonz/sources/include/toonz/preferences.h +++ b/toonz/sources/include/toonz/preferences.h @@ -283,6 +283,7 @@ public: QString getFfmpegPath() const { return getStringValue(ffmpegPath); } int getFfmpegTimeout() { return getIntValue(ffmpegTimeout); } QString getFastRenderPath() const { return getStringValue(fastRenderPath); } + bool getFfmpegMultiThread() const { return getBoolValue(ffmpegMultiThread); } // Drawing tab QString getScanLevelType() const { return getStringValue(scanLevelType); } diff --git a/toonz/sources/include/toonz/preferencesitemids.h b/toonz/sources/include/toonz/preferencesitemids.h index 55adaf4..ada431e 100644 --- a/toonz/sources/include/toonz/preferencesitemids.h +++ b/toonz/sources/include/toonz/preferencesitemids.h @@ -75,6 +75,7 @@ enum PreferencesItemId { ffmpegPath, ffmpegTimeout, fastRenderPath, + ffmpegMultiThread, //---------- // Drawing diff --git a/toonz/sources/toonz/outputsettingspopup.cpp b/toonz/sources/toonz/outputsettingspopup.cpp index b930b49..5412485 100644 --- a/toonz/sources/toonz/outputsettingspopup.cpp +++ b/toonz/sources/toonz/outputsettingspopup.cpp @@ -172,6 +172,7 @@ OutputSettingsPopup::OutputSettingsPopup(bool isPreview) , m_applyShrinkChk(nullptr) , m_outputCameraOm(nullptr) , m_isPreviewSettings(isPreview) + , m_allowMT(Preferences::instance()->getFfmpegMultiThread()) , m_presetCombo(nullptr) { setWindowTitle(isPreview ? tr("Preview Settings") : tr("Output Settings")); if (!isPreview) setObjectName("OutputSettingsPopup"); @@ -843,6 +844,11 @@ void OutputSettingsPopup::updateField() { m_multimediaOm->setCurrentIndex(prop->getMultimediaRendering()); } + // Refresh format if allow-multithread was toggled + if (m_allowMT != Preferences::instance()->getFfmpegMultiThread()) { + onFormatChanged(m_fileFormat->currentText()); + } + // camera if (m_outputCameraOm) { m_outputCameraOm->blockSignals(true); @@ -1046,19 +1052,21 @@ void OutputSettingsPopup::onNameChanged() { /*! Set current scene output format to new format set in popup field. */ void OutputSettingsPopup::onFormatChanged(const QString &str) { - auto isMultiRenderInvalid = [](std::string ext) -> bool { - return ext == "mp4" || ext == "gif" || ext == "webm" || + auto isMultiRenderInvalid = [](std::string ext, bool allowMT) -> bool { + return (!allowMT && (ext == "mp4" || ext == "gif" || ext == "webm")) || ext == "spritesheet"; }; TOutputProperties *prop = getProperties(); - bool wasMultiRenderInvalid = isMultiRenderInvalid(prop->getPath().getType()); - TFilePath fp = prop->getPath().withType(str.toStdString()); + bool wasMultiRenderInvalid = + isMultiRenderInvalid(prop->getPath().getType(), m_allowMT); + TFilePath fp = prop->getPath().withType(str.toStdString()); prop->setPath(fp); TApp::instance()->getCurrentScene()->setDirtyFlag(true); + m_allowMT = Preferences::instance()->getFfmpegMultiThread(); if (m_presetCombo) m_presetCombo->setCurrentIndex(0); - if (isMultiRenderInvalid(str.toStdString())) { + if (isMultiRenderInvalid(str.toStdString(), m_allowMT)) { m_threadsComboOm->setDisabled(true); m_threadsComboOm->setCurrentIndex(0); } else { diff --git a/toonz/sources/toonz/outputsettingspopup.h b/toonz/sources/toonz/outputsettingspopup.h index edb4acc..2aab697 100644 --- a/toonz/sources/toonz/outputsettingspopup.h +++ b/toonz/sources/toonz/outputsettingspopup.h @@ -67,6 +67,7 @@ class OutputSettingsPopup : public DVGui::Dialog { DVGui::DoubleLineEdit *m_stereoShift; QComboBox *m_rasterGranularityOm; QComboBox *m_threadsComboOm; + bool m_allowMT; DVGui::DoubleLineEdit *m_frameRateFld; QPushButton *m_fileFormatButton; diff --git a/toonz/sources/toonz/preferencespopup.cpp b/toonz/sources/toonz/preferencespopup.cpp index 2227146..dda75ac 100644 --- a/toonz/sources/toonz/preferencespopup.cpp +++ b/toonz/sources/toonz/preferencespopup.cpp @@ -1173,6 +1173,7 @@ QString PreferencesPopup::getUIString(PreferencesItemId id) { {ffmpegPath, tr("FFmpeg Path:")}, {ffmpegTimeout, tr("FFmpeg Timeout:")}, {fastRenderPath, tr("Fast Render Path:")}, + {ffmpegMultiThread, tr("Allow Multi-Thread in FFMPEG Rendering (UNSTABLE)")}, // Drawing {scanLevelType, tr("Scan File Format:")}, @@ -1787,6 +1788,13 @@ QWidget* PreferencesPopup::createImportExportPage() { lay); insertUI(fastRenderPath, lay); + putLabel("", lay); + putLabel( + tr("Enabling multi-thread rendering will render significantly faster " + "but a random crash might occur, use at your own risk."), + lay); + insertUI(ffmpegMultiThread, lay); + lay->setRowStretch(lay->rowCount(), 1); insertFootNote(lay); widget->setLayout(lay); diff --git a/toonz/sources/toonz/rendercommand.cpp b/toonz/sources/toonz/rendercommand.cpp index 83fb805..aae42e9 100644 --- a/toonz/sources/toonz/rendercommand.cpp +++ b/toonz/sources/toonz/rendercommand.cpp @@ -364,7 +364,14 @@ class RenderListener final : public DVGui::ProgressDialog, , m_labelText(labelText) {} TThread::Message *clone() const override { return new Message(*this); } void onDeliver() override { - if (m_frame == -1) + if (m_frame == -2) { + m_pb->setLabelText( + QObject::tr("Finalizing render, please wait.", "RenderListener")); + // Set busy indicator to progress bar + m_pb->setMinimum(0); + m_pb->setMaximum(0); + m_pb->setValue(0); + } else if (m_frame == -1) m_pb->hide(); else { m_pb->setLabelText( @@ -411,8 +418,7 @@ public: if (m_frameCounter + 1 < m_totalFrames) Message(this, ret ? -1 : ++m_frameCounter, m_progressBarString).send(); else - setLabelText( - QObject::tr("Finalizing render, please wait.", "RenderListener")); + Message(this, -2, "").send(); return !ret; } bool onFrameFailed(int frame, TException &) override { diff --git a/toonz/sources/toonzlib/movierenderer.cpp b/toonz/sources/toonzlib/movierenderer.cpp index 30d4d6a..c864ab1 100644 --- a/toonz/sources/toonzlib/movierenderer.cpp +++ b/toonz/sources/toonzlib/movierenderer.cpp @@ -27,6 +27,7 @@ // Qt includes #include +#include #include "toonz/movierenderer.h" @@ -131,6 +132,8 @@ public: bool m_cacheResults; bool m_preview; bool m_movieType; + bool m_seqRequired; + bool m_waitAfterFinish; public: Imp(ToonzScene *scene, const TFilePath &moviePath, int threadCount, @@ -185,13 +188,15 @@ MovieRenderer::Imp::Imp(ToonzScene *scene, const TFilePath &moviePath, , m_failure(false) // AFTER the first completed raster gets processed , m_cacheResults(cacheResults) , m_preview(moviePath.isEmpty()) - , m_movieType(isMovieType(moviePath)) { + , m_movieType(isMovieType(moviePath)) + , m_seqRequired(isSequencialRequired(moviePath)) { m_renderCacheId = m_fp.withName(m_fp.getName() + "#RENDERID" + QString::number(m_renderSessionId).toStdString()) .getLevelName(); m_renderer.addPort(this); + m_waitAfterFinish = m_movieType && !m_seqRequired && threadCount > 1; } //--------------------------------------------------------- @@ -470,6 +475,9 @@ void MovieRenderer::Imp::doRenderRasterCompleted(const RenderData &renderData) { QMutexLocker locker(&m_mutex); + bool allowMT = Preferences::instance()->getFfmpegMultiThread(); + bool requireSeq = allowMT ? m_seqRequired : m_movieType; + // Build soundtrack at the first time a frame is completed - and the filetype // is that of a movie. if (m_firstCompletedRaster && m_movieType && !m_st) { @@ -523,7 +531,7 @@ void MovieRenderer::Imp::doRenderRasterCompleted(const RenderData &renderData) { // In the *movie type* case, frames must be saved sequentially. // If the frame is not the next one in the sequence, wait until *that* frame // is available. - if (m_movieType && + if (requireSeq && (ft->first != m_framesToBeRendered[m_nextFrameIdxToSave].first)) break; @@ -557,8 +565,8 @@ void MovieRenderer::Imp::doRenderRasterCompleted(const RenderData &renderData) { ~MutexUnlocker() { if (m_locker) m_locker->relock(); } - } unlocker = {m_movieType ? (QMutexLocker *)0 - : (locker.unlock(), &locker)}; + } unlocker = {requireSeq ? (QMutexLocker *)0 + : (locker.unlock(), &locker)}; savedFrame = saveFrame(frame, rasters); } @@ -683,6 +691,9 @@ void MovieRenderer::Imp::onRenderFailure(const RenderData &renderData, // No sense making it later in this case! m_failure = true; + bool allowMT = Preferences::instance()->getFfmpegMultiThread(); + bool requireSeq = allowMT ? m_seqRequired : m_movieType; + // If the saver object has already been destroyed - or it was never // created to begin with, nothing to be done if (!m_levelUpdaterA.get()) return; // The preview case would fall here @@ -694,7 +705,7 @@ void MovieRenderer::Imp::onRenderFailure(const RenderData &renderData, std::map>::iterator it = m_toBeSaved.begin(); while (it != m_toBeSaved.end()) { - if (m_movieType && + if (requireSeq && (it->first != m_framesToBeRendered[m_nextFrameIdxToSave].first)) break; @@ -735,6 +746,15 @@ void MovieRenderer::Imp::onRenderFinished(bool isCanceled) { ? m_fp : TFilePath(getPreviewName(m_renderSessionId).toStdWString())); + if (m_waitAfterFinish) { + // Wait half a second to add some stability before finalizing + QEventLoop eloop; + QTimer timer; + timer.connect(&timer, &QTimer::timeout, &eloop, &QEventLoop::quit); + timer.start(500); + eloop.exec(); + } + // Close updaters. After this, the output levels should be finalized on disk. m_levelUpdaterA.reset(); m_levelUpdaterB.reset(); diff --git a/toonz/sources/toonzlib/preferences.cpp b/toonz/sources/toonzlib/preferences.cpp index bd02174..79aff9d 100644 --- a/toonz/sources/toonzlib/preferences.cpp +++ b/toonz/sources/toonzlib/preferences.cpp @@ -498,6 +498,7 @@ void Preferences::definePreferenceItems() { define(ffmpegTimeout, "ffmpegTimeout", QMetaType::Int, 600, 1, std::numeric_limits::max()); define(fastRenderPath, "fastRenderPath", QMetaType::QString, "desktop"); + define(ffmpegMultiThread, "ffmpegMultiThread", QMetaType::Bool, false); // Drawing define(scanLevelType, "scanLevelType", QMetaType::QString, "tif");