diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index 0f648884e..b328e3135 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -232,7 +232,7 @@ void EncodingParams::Save(QXmlStreamWriter *writer) const Encoder* Encoder::CreateFromID(const QString &id, const EncodingParams& params) { Q_UNUSED(id) - + return new FFmpegEncoder(params); } diff --git a/app/codec/encoder.h b/app/codec/encoder.h index 4acf38bde..2d4e2d245 100644 --- a/app/codec/encoder.h +++ b/app/codec/encoder.h @@ -122,6 +122,11 @@ public: virtual void Close() = 0; + virtual PixelFormat::Format GetDesiredPixelFormat() const + { + return PixelFormat::PIX_FMT_INVALID; + } + private: EncodingParams params_; diff --git a/app/codec/ffmpeg/ffmpegencoder.h b/app/codec/ffmpeg/ffmpegencoder.h index 4efc76754..e42d8137a 100644 --- a/app/codec/ffmpeg/ffmpegencoder.h +++ b/app/codec/ffmpeg/ffmpegencoder.h @@ -47,6 +47,11 @@ public: virtual void Close() override; + virtual PixelFormat::Format GetDesiredPixelFormat() const override + { + return video_conversion_fmt_; + } + private: /** * @brief Handle an error diff --git a/app/common/filefunctions.cpp b/app/common/filefunctions.cpp index acb9ee8c3..513d781ea 100644 --- a/app/common/filefunctions.cpp +++ b/app/common/filefunctions.cpp @@ -75,7 +75,7 @@ QString FileFunctions::GetTempFilePath() { QString temp_path = QDir(QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)) .filePath(QCoreApplication::organizationName())) - .filePath(QCoreApplication::applicationName()); + .filePath(QCoreApplication::applicationName()); // Ensure it exists QDir(temp_path).mkpath("."); @@ -199,4 +199,45 @@ QString FileFunctions::ReadFileAsString(const QString &filename) return file_data; } +QString FileFunctions::GetSafeTemporaryFilename(const QString &original) +{ + int counter = 0; + + QFileInfo original_info(original); + QString basename = original_info.baseName(); + QString complete_suffix = original_info.completeSuffix(); + + // If we have a complete suffix, make sure there's a period in it + if (!complete_suffix.isEmpty()) { + complete_suffix.prepend('.'); + } + + QString temp_abs_path; + do { + temp_abs_path = original_info.dir().filePath( + QStringLiteral("%1.tmp%2%3").arg(basename, + QString::number(counter), + complete_suffix)); + counter++; + } while (QFileInfo::exists(temp_abs_path)); + + return temp_abs_path; +} + +bool FileFunctions::RenameFileAllowOverwrite(const QString &from, const QString &to) +{ + if (QFileInfo::exists(to) && !QFile::remove(to)) { + qCritical() << "Couldn't remove existing file" << to << "for overwrite"; + return false; + } + + // By this point, we can assume `to` either never existed or has now been deleted + if (!QFile::rename(from, to)) { + qCritical() << "Failed to rename file" << from << "to" << to; + return false; + } + + return true; +} + OLIVE_NAMESPACE_EXIT diff --git a/app/common/filefunctions.h b/app/common/filefunctions.h index 1251dfad6..f19aac9b9 100644 --- a/app/common/filefunctions.h +++ b/app/common/filefunctions.h @@ -67,6 +67,23 @@ public: static QString ReadFileAsString(const QString& filename); + /** + * @brief Returns a temporary filename that can be used while writing rather than the original + * + * If overwriting a file, it's safest to write to a new file first and then only replace it at + * the end so that if the program crashes or the user cancels the save half way through, the + * original file is still intact. + * + * This function returns a slight variant of the filename provided that's guaranteed to not exist + * and therefore won't overwrite anything important. + */ + static QString GetSafeTemporaryFilename(const QString& original); + + /** + * @brief Renames a file from `from` to `to`, deleting `to` if such a file already exists first + */ + static bool RenameFileAllowOverwrite(const QString& from, const QString& to); + }; diff --git a/app/config/config.cpp b/app/config/config.cpp index 40f53240f..2feb40578 100644 --- a/app/config/config.cpp +++ b/app/config/config.cpp @@ -204,7 +204,10 @@ void Config::Load() void Config::Save() { - QFile config_file(GetConfigFilePath()); + QString real_filename = GetConfigFilePath(); + QString temp_filename = FileFunctions::GetSafeTemporaryFilename(real_filename); + + QFile config_file(temp_filename); if (!config_file.open(QFile::WriteOnly)) { QMessageBox::critical(Core::instance()->main_window(), @@ -243,6 +246,11 @@ void Config::Save() writer.writeEndDocument(); config_file.close(); + + if (!FileFunctions::RenameFileAllowOverwrite(temp_filename, real_filename)) { + qWarning() << QStringLiteral("Failed to overwrite \"%1\". Config has been saved as \"%2\" instead.") + .arg(real_filename, temp_filename); + } } QVariant Config::operator[](const QString &key) const diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index 2d3250be9..c14aff1f3 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -179,6 +179,7 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : video_tab_->height_slider()->SetDefaultValue(viewer_node_->video_params().height()); video_tab_->frame_rate_combobox()->SetFrameRate(viewer_node_->video_params().time_base().flipped()); video_tab_->pixel_aspect_combobox()->SetPixelAspectRatio(viewer_node_->video_params().pixel_aspect_ratio()); + video_tab_->pixel_format_field()->SetPixelFormat(PixelFormat::instance()->GetConfiguredFormatForMode(RenderMode::kOnline)); video_tab_->interlaced_combobox()->SetInterlaceMode(viewer_node_->video_params().interlacing()); audio_tab_->sample_rate_combobox()->SetSampleRate(viewer_node_->audio_params().sample_rate()); audio_tab_->channel_layout_combobox()->SetChannelLayout(viewer_node_->audio_params().channel_layout()); @@ -285,25 +286,36 @@ void ExportDialog::StartExport() } // Validate video resolution - if (video_enabled_->isChecked()) { - if (video_tab_->width_slider()->GetValue() % 2 != 0 - || video_tab_->height_slider()->GetValue() % 2 != 0) { - QMessageBox b(this); - b.setIcon(QMessageBox::Critical); - b.setWindowModality(Qt::WindowModal); - b.setWindowTitle(tr("Invalid parameters")); - b.setText(tr("Width and height must be multiples of 2.")); - b.exec(); - return; - } + if (video_enabled_->isChecked() + && video_tab_->GetSelectedCodec() == ExportCodec::kCodecH264 + && (video_tab_->width_slider()->GetValue()%2 != 0 || video_tab_->height_slider()->GetValue()%2 != 0)) { + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Invalid Parameters")); + b.setText(tr("Width and height must be multiples of 2.")); + b.exec(); + return; } ExportTask* task = new ExportTask(viewer_node_, color_manager_, GenerateParams()); TaskDialog* td = new TaskDialog(task, tr("Export"), this); - connect(td, &TaskDialog::TaskSucceeded, this, &QDialog::accept); + connect(td, &TaskDialog::TaskSucceeded, this, &ExportDialog::ExportFinished); td->open(); } +void ExportDialog::ExportFinished() +{ + TaskDialog* td = static_cast(sender()); + + if (td->GetTask()->IsCancelled()) { + // If this task was cancelled, we stay open so the user can potentially queue another export + } else { + // Accept this dialog and close + this->accept(); + } +} + void ExportDialog::closeEvent(QCloseEvent *e) { preview_viewer_->ConnectViewerNode(nullptr); @@ -373,7 +385,7 @@ void ExportDialog::ResolutionChanged() new_width *= video_aspect_ratio_; // Align to even number and set - video_tab_->width_slider()->SetValue(AlignEvenNumber(new_width)); + video_tab_->width_slider()->SetValue(new_width); } else { @@ -384,7 +396,7 @@ void ExportDialog::ResolutionChanged() new_height /= video_aspect_ratio_; // Align to even number and set - video_tab_->height_slider()->SetValue(AlignEvenNumber(new_height)); + video_tab_->height_slider()->SetValue(new_height); } } @@ -414,17 +426,12 @@ void ExportDialog::SetDefaultFilename() filename_edit_->setText(file_location); } -int ExportDialog::AlignEvenNumber(double d) -{ - return qCeil(d * 0.5) * 2; -} - ExportParams ExportDialog::GenerateParams() const { VideoParams video_render_params(static_cast(video_tab_->width_slider()->GetValue()), static_cast(video_tab_->height_slider()->GetValue()), video_tab_->frame_rate_combobox()->GetFrameRate().flipped(), - PixelFormat::instance()->GetConfiguredFormatForMode(RenderMode::kOnline), + video_tab_->pixel_format_field()->GetPixelFormat(), video_tab_->pixel_aspect_combobox()->GetPixelAspectRatio(), video_tab_->interlaced_combobox()->GetInterlaceMode(), 1); diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index 8313e59cf..d92eaba3e 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -49,8 +49,6 @@ private: void LoadPresets(); void SetDefaultFilename(); - static int AlignEvenNumber(double d); - ExportParams GenerateParams() const; ViewerOutput* viewer_node_; @@ -85,6 +83,8 @@ private slots: void StartExport(); + void ExportFinished(); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/export/exportvideotab.cpp b/app/dialog/export/exportvideotab.cpp index 9d62c360f..617f67269 100644 --- a/app/dialog/export/exportvideotab.cpp +++ b/app/dialog/export/exportvideotab.cpp @@ -115,6 +115,13 @@ QWidget* ExportVideoTab::SetupResolutionSection() interlaced_combobox_ = new InterlacedComboBox(); layout->addWidget(interlaced_combobox_, row, 1); + row++; + + layout->addWidget(new QLabel(tr("Quality:")), row, 0); + + pixel_format_field_ = new PixelFormatComboBox(true); + layout->addWidget(pixel_format_field_, row, 1); + return resolution_group; } diff --git a/app/dialog/export/exportvideotab.h b/app/dialog/export/exportvideotab.h index fe35d884b..ffbac3e19 100644 --- a/app/dialog/export/exportvideotab.h +++ b/app/dialog/export/exportvideotab.h @@ -111,6 +111,11 @@ public: return pixel_aspect_combobox_; } + PixelFormatComboBox* pixel_format_field() const + { + return pixel_format_field_; + } + const int& threads() const { return threads_; @@ -149,6 +154,7 @@ private: InterlacedComboBox* interlaced_combobox_; PixelAspectRatioComboBox* pixel_aspect_combobox_; + PixelFormatComboBox* pixel_format_field_; int threads_; diff --git a/app/dialog/sequence/sequencedialogparametertab.cpp b/app/dialog/sequence/sequencedialogparametertab.cpp index 7487ba294..cd744eefc 100644 --- a/app/dialog/sequence/sequencedialogparametertab.cpp +++ b/app/dialog/sequence/sequencedialogparametertab.cpp @@ -74,7 +74,7 @@ SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence* sequence, QWidg preview_resolution_label_ = new QLabel(); preview_layout->addWidget(preview_resolution_label_, row, 2); row++; - preview_layout->addWidget(new QLabel(tr("Format:")), row, 0); + preview_layout->addWidget(new QLabel(tr("Quality:")), row, 0); preview_format_field_ = new PixelFormatComboBox(true); preview_layout->addWidget(preview_format_field_, row, 1, 1, 2); layout->addWidget(preview_group); diff --git a/app/render/colorprocessor.h b/app/render/colorprocessor.h index 5d0b9a8f1..531f0ce3f 100644 --- a/app/render/colorprocessor.h +++ b/app/render/colorprocessor.h @@ -70,8 +70,10 @@ private: }; -using ColorProcessorChain = QList; +using ColorProcessorChain = QVector; OLIVE_NAMESPACE_EXIT +Q_DECLARE_METATYPE(OLIVE_NAMESPACE::ColorProcessorPtr); + #endif // COLORPROCESSOR_H diff --git a/app/render/job/shaderjob.h b/app/render/job/shaderjob.h index c6fc66cd9..72b8c9270 100644 --- a/app/render/job/shaderjob.h +++ b/app/render/job/shaderjob.h @@ -36,16 +36,6 @@ public: iterative_input_ = nullptr; } - const QMatrix4x4& GetMatrix() const - { - return matrix_; - } - - void SetMatrix(const QMatrix4x4& matrix) - { - matrix_ = matrix; - } - const QString& GetShaderID() const { return shader_id_; @@ -101,8 +91,6 @@ private: QHash interpolation_; - QMatrix4x4 matrix_; - }; OLIVE_NAMESPACE_EXIT diff --git a/app/render/opengl/openglrenderer.cpp b/app/render/opengl/openglrenderer.cpp index 25a1c9fb0..e6e304099 100644 --- a/app/render/opengl/openglrenderer.cpp +++ b/app/render/opengl/openglrenderer.cpp @@ -431,8 +431,9 @@ void OpenGLRenderer::Blit(QVariant s, ShaderJob job, Texture *destination, Video static_cast(destination_params.width()), static_cast(destination_params.height())); - // Set matrix to identity - shader->setUniformValue("ove_mvpmat", job.GetMatrix()); + // Ensure matrix is set, at least to identity + shader->setUniformValue("ove_mvpmat", + job.GetValue(QStringLiteral("ove_mvpmat")).data.value()); // Set the viewport to the "physical" resolution of the destination functions_->glViewport(0, 0, diff --git a/app/render/pixelformat.cpp b/app/render/pixelformat.cpp index 9aee69783..92c0ebd46 100644 --- a/app/render/pixelformat.cpp +++ b/app/render/pixelformat.cpp @@ -127,7 +127,8 @@ PixelFormat *PixelFormat::instance() PixelFormat::Format PixelFormat::GetConfiguredFormatForMode(RenderMode::Mode mode) { - return static_cast(Core::GetPreferenceForRenderMode(mode, QStringLiteral("PixelFormat")).toInt()); + return static_cast( + Core::GetPreferenceForRenderMode(mode, QStringLiteral("PixelFormat")).toInt()); } void PixelFormat::SetConfiguredFormatForMode(RenderMode::Mode mode, PixelFormat::Format format) diff --git a/app/render/renderer.cpp b/app/render/renderer.cpp index ffebfa072..acd9885eb 100644 --- a/app/render/renderer.cpp +++ b/app/render/renderer.cpp @@ -23,7 +23,6 @@ #include #include "common/ocioutils.h" -#include "render/colormanager.h" OLIVE_NAMESPACE_ENTER @@ -57,14 +56,14 @@ TexturePtr Renderer::CreateTexture(const VideoParams ¶ms, const void *data, return CreateTexture(params, Texture::k2D, Texture::kRGBA, data, linesize); } -void Renderer::BlitColorManaged(ColorProcessorPtr color_processor, TexturePtr source, Texture *destination, bool flipped) +void Renderer::BlitColorManaged(ColorProcessorPtr color_processor, TexturePtr source, Texture *destination, const QMatrix4x4 &matrix) { - BlitColorManagedInternal(color_processor, source, destination, destination->params(), flipped); + BlitColorManagedInternal(color_processor, source, destination, destination->params(), matrix); } -void Renderer::BlitColorManaged(ColorProcessorPtr color_processor, TexturePtr source, VideoParams params, bool flipped) +void Renderer::BlitColorManaged(ColorProcessorPtr color_processor, TexturePtr source, VideoParams params, const QMatrix4x4& matrix) { - BlitColorManagedInternal(color_processor, source, nullptr, params, flipped); + BlitColorManagedInternal(color_processor, source, nullptr, params, matrix); } void Renderer::Destroy() @@ -195,7 +194,7 @@ bool Renderer::GetColorContext(ColorProcessorPtr color_processor, Renderer::Colo } } -void Renderer::BlitColorManagedInternal(ColorProcessorPtr color_processor, TexturePtr source, Texture *destination, VideoParams params, bool flipped) +void Renderer::BlitColorManagedInternal(ColorProcessorPtr color_processor, TexturePtr source, Texture *destination, VideoParams params, const QMatrix4x4& matrix) { ColorContext color_ctx; if (!GetColorContext(color_processor, &color_ctx)) { @@ -203,7 +202,10 @@ void Renderer::BlitColorManagedInternal(ColorProcessorPtr color_processor, Textu } ShaderJob job; + job.InsertValue(QStringLiteral("ove_maintex"), ShaderValue(QVariant::fromValue(source), NodeParam::kTexture)); + job.InsertValue(QStringLiteral("ove_mvpmat"), ShaderValue(matrix, NodeParam::kMatrix)); + foreach (const ColorContext::LUT& l, color_ctx.lut3d_textures) { job.InsertValue(l.name, ShaderValue(QVariant::fromValue(l.texture), NodeParam::kTexture)); job.SetInterpolation(l.name, l.interpolation); @@ -213,12 +215,6 @@ void Renderer::BlitColorManagedInternal(ColorProcessorPtr color_processor, Textu job.SetInterpolation(l.name, l.interpolation); } - if (flipped) { - QMatrix4x4 mat; - mat.scale(1, -1, 1); - job.SetMatrix(mat); - } - if (destination) { BlitToTexture(color_ctx.compiled_shader, job, destination); } else { diff --git a/app/render/renderer.h b/app/render/renderer.h index 2da550fa1..bee5dc21b 100644 --- a/app/render/renderer.h +++ b/app/render/renderer.h @@ -60,8 +60,8 @@ public: Blit(shader, job, nullptr, params); } - void BlitColorManaged(ColorProcessorPtr color_processor, TexturePtr source, Texture* destination, bool flipped = false); - void BlitColorManaged(ColorProcessorPtr color_processor, TexturePtr source, VideoParams params, bool flipped = false); + void BlitColorManaged(ColorProcessorPtr color_processor, TexturePtr source, Texture* destination, const QMatrix4x4& matrix = QMatrix4x4()); + void BlitColorManaged(ColorProcessorPtr color_processor, TexturePtr source, VideoParams params, const QMatrix4x4& matrix = QMatrix4x4()); void Destroy(); @@ -108,7 +108,7 @@ private: bool GetColorContext(ColorProcessorPtr color_processor, ColorContext* ctx); void BlitColorManagedInternal(ColorProcessorPtr color_processor, TexturePtr source, - Texture* destination, VideoParams params, bool flipped); + Texture* destination, VideoParams params, const QMatrix4x4 &matrix); QHash color_cache_; diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp index de9b4b408..2f6401a06 100644 --- a/app/render/rendermanager.cpp +++ b/app/render/rendermanager.cpp @@ -56,8 +56,10 @@ RenderManager::RenderManager(QObject *parent) : still_cache_ = new StillImageCache(); decoder_cache_ = new DecoderCache(); shader_cache_ = new ShaderCache(); + default_shader_ = context_->CreateNativeShader(ShaderCode(QString(), QString())); } else { qCritical() << "Tried to initialize unknown graphics backend"; + context_ = nullptr; still_cache_ = nullptr; decoder_cache_ = nullptr; } @@ -65,12 +67,16 @@ RenderManager::RenderManager(QObject *parent) : RenderManager::~RenderManager() { - delete shader_cache_; - delete decoder_cache_; - delete still_cache_; + if (context_) { + context_->DestroyNativeShader(default_shader_); - context_->Destroy(); - delete context_; + delete shader_cache_; + delete decoder_cache_; + delete still_cache_; + + context_->Destroy(); + delete context_; + } } QByteArray RenderManager::Hash(const Node *n, const VideoParams ¶ms, const rational &time) @@ -93,19 +99,31 @@ QByteArray RenderManager::Hash(const Node *n, const VideoParams ¶ms, const r return hasher.result(); } -RenderTicketPtr RenderManager::RenderFrame(ViewerOutput *viewer, ColorManager *color_manager, const rational &time, RenderMode::Mode mode, FrameHashCache *cache, bool prioritize) +RenderTicketPtr RenderManager::RenderFrame(ViewerOutput* viewer, ColorManager* color_manager, + const rational& time, RenderMode::Mode mode, + FrameHashCache* cache, bool prioritize) { return RenderFrame(viewer, color_manager, time, mode, + viewer->video_params(), + viewer->audio_params(), QSize(0, 0), QMatrix4x4(), + PixelFormat::PIX_FMT_INVALID, + nullptr, cache, prioritize); } -RenderTicketPtr RenderManager::RenderFrame(ViewerOutput* viewer, ColorManager* color_manager, const rational &time, RenderMode::Mode mode, const QSize &force_size, const QMatrix4x4 &matrix, FrameHashCache *cache, bool prioritize) +RenderTicketPtr RenderManager::RenderFrame(ViewerOutput* viewer, ColorManager* color_manager, + const rational& time, RenderMode::Mode mode, + const VideoParams &video_params, const AudioParams &audio_params, + const QSize& force_size, + const QMatrix4x4& force_matrix, PixelFormat::Format force_format, + ColorProcessorPtr force_color_output, + FrameHashCache* cache, bool prioritize) { // Create ticket RenderTicketPtr ticket = std::make_shared(); @@ -113,11 +131,18 @@ RenderTicketPtr RenderManager::RenderFrame(ViewerOutput* viewer, ColorManager* c ticket->setProperty("viewer", Node::PtrToValue(viewer)); ticket->setProperty("time", QVariant::fromValue(time)); ticket->setProperty("size", force_size); - ticket->setProperty("matrix", matrix); + ticket->setProperty("matrix", force_matrix); + ticket->setProperty("format", force_format); ticket->setProperty("mode", mode); ticket->setProperty("type", kTypeVideo); - ticket->setProperty("cache", cache->GetCacheDirectory()); ticket->setProperty("colormanager", Node::PtrToValue(color_manager)); + ticket->setProperty("coloroutput", QVariant::fromValue(force_color_output)); + ticket->setProperty("vparam", QVariant::fromValue(video_params)); + ticket->setProperty("aparam", QVariant::fromValue(audio_params)); + + if (cache) { + ticket->setProperty("cache", cache->GetCacheDirectory()); + } // Queue appending the ticket and running the next job on our thread to make this function thread-safe QMetaObject::invokeMethod(this, "AddTicket", Qt::AutoConnection, @@ -127,7 +152,12 @@ RenderTicketPtr RenderManager::RenderFrame(ViewerOutput* viewer, ColorManager* c return ticket; } -RenderTicketPtr RenderManager::RenderAudio(ViewerOutput* viewer, const TimeRange &r, bool generate_waveforms, bool prioritize) +RenderTicketPtr RenderManager::RenderAudio(ViewerOutput* viewer, const TimeRange& r, bool generate_waveforms, bool prioritize) +{ + return RenderAudio(viewer, r, viewer->audio_params(), generate_waveforms, prioritize); +} + +RenderTicketPtr RenderManager::RenderAudio(ViewerOutput* viewer, const TimeRange &r, const AudioParams ¶ms, bool generate_waveforms, bool prioritize) { // Create ticket RenderTicketPtr ticket = std::make_shared(); @@ -136,6 +166,7 @@ RenderTicketPtr RenderManager::RenderAudio(ViewerOutput* viewer, const TimeRange ticket->setProperty("time", QVariant::fromValue(r)); ticket->setProperty("type", kTypeAudio); ticket->setProperty("waveforms", generate_waveforms); + ticket->setProperty("aparam", QVariant::fromValue(params)); // Queue appending the ticket and running the next job on our thread to make this function thread-safe QMetaObject::invokeMethod(this, "AddTicket", Qt::AutoConnection, @@ -165,7 +196,7 @@ RenderTicketPtr RenderManager::SaveFrameToCache(FrameHashCache *cache, FramePtr void RenderManager::RunTicket(RenderTicketPtr ticket) const { - RenderProcessor::Process(ticket, context_, still_cache_, decoder_cache_, shader_cache_); + RenderProcessor::Process(ticket, context_, still_cache_, decoder_cache_, shader_cache_, default_shader_); } OLIVE_NAMESPACE_EXIT diff --git a/app/render/rendermanager.h b/app/render/rendermanager.h index 0f73ca53d..702356023 100644 --- a/app/render/rendermanager.h +++ b/app/render/rendermanager.h @@ -80,8 +80,16 @@ public: * * This function is thread-safe. */ - RenderTicketPtr RenderFrame(ViewerOutput* viewer, ColorManager* color_manager, const rational& time, RenderMode::Mode mode, FrameHashCache* cache = nullptr, bool prioritize = false); - RenderTicketPtr RenderFrame(ViewerOutput* viewer, ColorManager* color_manager, const rational& time, RenderMode::Mode mode, const QSize& force_size, const QMatrix4x4& matrix, FrameHashCache* cache = nullptr, bool prioritize = false); + RenderTicketPtr RenderFrame(ViewerOutput* viewer, ColorManager* color_manager, + const rational& time, RenderMode::Mode mode, + FrameHashCache* cache = nullptr, bool prioritize = false); + RenderTicketPtr RenderFrame(ViewerOutput* viewer, ColorManager* color_manager, + const rational& time, RenderMode::Mode mode, + const VideoParams& video_params, const AudioParams& audio_params, + const QSize& force_size, + const QMatrix4x4& force_matrix, PixelFormat::Format force_format, + ColorProcessorPtr force_color_output, + FrameHashCache* cache = nullptr, bool prioritize = false); /** * @brief Asynchronously generate a chunk of audio @@ -93,6 +101,7 @@ public: * * This function is thread-safe. */ + RenderTicketPtr RenderAudio(ViewerOutput* viewer, const TimeRange& r, const AudioParams& params, bool generate_waveforms, bool prioritize = false); RenderTicketPtr RenderAudio(ViewerOutput* viewer, const TimeRange& r, bool generate_waveforms, bool prioritize = false); RenderTicketPtr SaveFrameToCache(FrameHashCache* cache, FramePtr frame, const QByteArray& hash, bool prioritize = false); @@ -129,6 +138,8 @@ private: ShaderCache* shader_cache_; + QVariant default_shader_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 6587c6d0c..146cb5d7a 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -29,12 +29,13 @@ OLIVE_NAMESPACE_ENTER -RenderProcessor::RenderProcessor(RenderTicketPtr ticket, Renderer *render_ctx, StillImageCache* still_image_cache, DecoderCache* decoder_cache, ShaderCache *shader_cache) : +RenderProcessor::RenderProcessor(RenderTicketPtr ticket, Renderer *render_ctx, StillImageCache* still_image_cache, DecoderCache* decoder_cache, ShaderCache *shader_cache, QVariant default_shader) : ticket_(ticket), render_ctx_(render_ctx), still_image_cache_(still_image_cache), decoder_cache_(decoder_cache), - shader_cache_(shader_cache) + shader_cache_(shader_cache), + default_shader_(default_shader) { } @@ -49,14 +50,16 @@ void RenderProcessor::Run() case RenderManager::kTypeVideo: { ViewerOutput* viewer = Node::ValueToPtr(ticket_->property("viewer")); + const VideoParams& video_params = ticket_->property("vparam").value(); rational time = ticket_->property("time").value(); NodeValueTable table = ProcessInput(viewer->texture_input(), - TimeRange(time, time + viewer->video_params().time_base())); + TimeRange(time, time + video_params.time_base())); TexturePtr texture = table.Get(NodeParam::kTexture).value(); - VideoParams frame_params = viewer->video_params(); + // Set up output frame parameters + VideoParams frame_params = ticket_->property("vparam").value(); QSize frame_size = ticket_->property("size").value(); if (!frame_size.isNull()) { @@ -64,6 +67,11 @@ void RenderProcessor::Run() frame_params.set_height(frame_size.height()); } + PixelFormat::Format frame_format = static_cast(ticket_->property("format").toInt()); + if (frame_format != PixelFormat::PIX_FMT_INVALID) { + frame_params.set_format(frame_format); + } + FramePtr frame = Frame::Create(); frame->set_timestamp(time); frame->set_video_params(frame_params); @@ -74,10 +82,31 @@ void RenderProcessor::Run() memset(frame->data(), 0, frame->allocated_size()); } else { // Dump texture contents to frame + ColorProcessorPtr output_color_transform = ticket_->property("coloroutput").value(); const VideoParams& tex_params = texture->params(); - if (tex_params.width() != frame->width() || tex_params.height() != frame->height()) { - // FIXME: Blit this shit + if (tex_params.effective_width() != frame_params.effective_width() + || tex_params.effective_height() != frame_params.effective_height() + || tex_params.format() != frame_params.format() + || output_color_transform) { + TexturePtr blit_tex = render_ctx_->CreateTexture(frame_params); + + QMatrix4x4 matrix = ticket_->property("matrix").value(); + + if (output_color_transform) { + // Yes color transform, blit color managed + render_ctx_->BlitColorManaged(output_color_transform, texture, blit_tex.get(), matrix); + } else { + // No color transform, just blit + ShaderJob job; + job.InsertValue(QStringLiteral("ove_maintex"), {QVariant::fromValue(texture), NodeParam::kTexture}); + job.InsertValue(QStringLiteral("ove_mvpmat"), {matrix, NodeParam::kMatrix}); + + render_ctx_->BlitToTexture(default_shader_, job, blit_tex.get()); + } + + // Replace texture that we're going to download in the next step + texture = blit_tex; } render_ctx_->DownloadFromTexture(texture.get(), frame->data(), frame->linesize_pixels()); @@ -138,9 +167,9 @@ DecoderPtr RenderProcessor::ResolveDecoderFromInput(StreamPtr stream) return decoder; } -void RenderProcessor::Process(RenderTicketPtr ticket, Renderer *render_ctx, StillImageCache *still_image_cache, DecoderCache *decoder_cache, ShaderCache *shader_cache) +void RenderProcessor::Process(RenderTicketPtr ticket, Renderer *render_ctx, StillImageCache *still_image_cache, DecoderCache *decoder_cache, ShaderCache *shader_cache, QVariant default_shader) { - RenderProcessor p(ticket, render_ctx, still_image_cache, decoder_cache, shader_cache); + RenderProcessor p(ticket, render_ctx, still_image_cache, decoder_cache, shader_cache, default_shader); p.Run(); } @@ -148,7 +177,7 @@ NodeValueTable RenderProcessor::GenerateBlockTable(const TrackOutput *track, con { if (track->track_type() == Timeline::kTrackTypeAudio) { - const AudioParams& audio_params = Node::ValueToPtr(ticket_->property("viewer"))->audio_params(); + const AudioParams& audio_params = ticket_->property("aparam").value(); QList active_blocks = track->BlocksAtTimeRange(range); @@ -228,11 +257,13 @@ QVariant RenderProcessor::ProcessVideoFootage(StreamPtr stream, const rational & // and color managing them for every frame is a waste of time, so we implement a small cache here // to optimize such a situation VideoStreamPtr video_stream = std::static_pointer_cast(stream); - const VideoParams& video_params = Node::ValueToPtr(ticket_->property("viewer"))->video_params(); + const VideoParams& video_params = ticket_->property("vparam").value(); + + ColorManager* color_manager = Node::ValueToPtr(ticket_->property("colormanager")); StillImageCache::Entry want_entry = {nullptr, stream, - ColorProcessor::GenerateID(Node::ValueToPtr(ticket_->property("colormanager")), video_stream->colorspace(), ColorTransform(OCIO::ROLE_SCENE_LINEAR)), + ColorProcessor::GenerateID(color_manager, video_stream->colorspace(), color_manager->GetReferenceColorSpace()), video_stream->premultiplied_alpha(), video_params.divider(), (video_stream->video_type() == VideoStream::kVideoTypeStill) ? 0 : input_time}; @@ -297,10 +328,9 @@ QVariant RenderProcessor::ProcessVideoFootage(StreamPtr stream, const rational & qDebug() << "FIXME: Accessing video_stream->colorspace() may cause race conditions"; - ColorManager* color_manager = Node::ValueToPtr(ticket_->property("colormanager")); ColorProcessorPtr processor = ColorProcessor::Create(color_manager, video_stream->colorspace(), - ColorTransform(OCIO::ROLE_SCENE_LINEAR)); + color_manager->GetReferenceColorSpace()); render_ctx_->BlitColorManaged(processor, unmanaged_texture, value.get()); @@ -327,7 +357,7 @@ QVariant RenderProcessor::ProcessAudioFootage(StreamPtr stream, const TimeRange DecoderPtr decoder = ResolveDecoderFromInput(stream); if (decoder) { - const AudioParams& audio_params = Node::ValueToPtr(ticket_->property("viewer"))->audio_params(); + const AudioParams& audio_params = ticket_->property("aparam").value(); SampleBufferPtr frame = decoder->RetrieveAudio(input_time, audio_params, &IsCancelled()); @@ -359,7 +389,7 @@ QVariant RenderProcessor::ProcessShader(const Node *node, const TimeRange &range } } - const VideoParams& video_params = Node::ValueToPtr(ticket_->property("viewer"))->video_params(); + const VideoParams& video_params = ticket_->property("vparam").value(); TexturePtr destination = render_ctx_->CreateTexture(video_params); @@ -378,7 +408,7 @@ QVariant RenderProcessor::ProcessSamples(const Node *node, const TimeRange &rang SampleBufferPtr output_buffer = SampleBuffer::CreateAllocated(job.samples()->audio_params(), job.samples()->sample_count()); NodeValueDatabase value_db; - const AudioParams& audio_params = Node::ValueToPtr(ticket_->property("viewer"))->audio_params(); + const AudioParams& audio_params = ticket_->property("aparam").value(); for (int i=0;isample_count();i++) { // Calculate the exact rational time at this sample @@ -416,7 +446,7 @@ QVariant RenderProcessor::ProcessFrameGeneration(const Node *node, const Generat { FramePtr frame = Frame::Create(); - const VideoParams& video_params = Node::ValueToPtr(ticket_->property("viewer"))->video_params(); + const VideoParams& video_params = ticket_->property("vparam").value(); frame->set_video_params(video_params); frame->allocate(); @@ -436,7 +466,7 @@ QVariant RenderProcessor::GetCachedFrame(const Node *node, const rational &time) { if (!ticket_->property("cache").toString().isEmpty() && node->id() == QStringLiteral("org.olivevideoeditor.Olive.videoinput")) { - const VideoParams& video_params = Node::ValueToPtr(ticket_->property("viewer"))->video_params(); + const VideoParams& video_params = ticket_->property("vparam").value(); QByteArray hash = RenderManager::Hash(node, video_params, time); diff --git a/app/render/renderprocessor.h b/app/render/renderprocessor.h index ed5bf38ae..17098179d 100644 --- a/app/render/renderprocessor.h +++ b/app/render/renderprocessor.h @@ -32,7 +32,7 @@ OLIVE_NAMESPACE_ENTER class RenderProcessor : public NodeTraverser { public: - static void Process(RenderTicketPtr ticket, Renderer* render_ctx, StillImageCache* still_image_cache, DecoderCache* decoder_cache, ShaderCache* shader_cache); + static void Process(RenderTicketPtr ticket, Renderer* render_ctx, StillImageCache* still_image_cache, DecoderCache* decoder_cache, ShaderCache* shader_cache, QVariant default_shader); struct RenderedWaveform { const TrackOutput* track; @@ -56,7 +56,7 @@ protected: virtual QVariant GetCachedFrame(const Node *node, const rational &time) override; private: - RenderProcessor(RenderTicketPtr ticket, Renderer* render_ctx, StillImageCache* still_image_cache, DecoderCache* decoder_cache, ShaderCache* shader_cache); + RenderProcessor(RenderTicketPtr ticket, Renderer* render_ctx, StillImageCache* still_image_cache, DecoderCache* decoder_cache, ShaderCache* shader_cache, QVariant default_shader); void Run(); @@ -72,6 +72,8 @@ private: ShaderCache* shader_cache_; + QVariant default_shader_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index d5da32ae5..e7e53b0a1 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -39,7 +39,16 @@ bool ExportTask::Run() { TimeRange range; + // For safety, if we're overwriting, we save to a temporary filename and then only overwrite it + // at the end + QString real_filename = params_.filename(); + if (QFileInfo::exists(params_.filename())) { + // Generate a filename that definitely doesn't exist + params_.SetFilename(FileFunctions::GetSafeTemporaryFilename(real_filename)); + } + encoder_ = Encoder::CreateFromID(params_.encoder(), params_); + if (!encoder_) { SetError(tr("Failed to create encoder")); return false; @@ -61,17 +70,26 @@ bool ExportTask::Run() frame_time_ = Timecode::time_to_timestamp(range.in(), viewer()->video_params().time_base()); + QSize video_force_size; + QMatrix4x4 video_force_matrix; + if (params_.video_enabled()) { // If a transformation matrix is applied to this video, create it here - if (params_.video_scaling_method() != ExportParams::kStretch) { - // FIXME: Re-implement this + if (viewer()->video_params().width() != params_.video_params().width() + || params_.video_params().height() != params_.video_params().height()) { + video_force_size = QSize(params_.video_params().width(), params_.video_params().height()); - /*QMatrix4x4 mat = ExportParams::GenerateMatrix(params_.video_scaling_method(), - viewer()->video_params().width(), - viewer()->video_params().height(), - params_.video_params().width(), - params_.video_params().height());*/ + if (params_.video_scaling_method() != ExportParams::kStretch) { + video_force_matrix = ExportParams::GenerateMatrix(params_.video_scaling_method(), + viewer()->video_params().width(), + viewer()->video_params().height(), + params_.video_params().width(), + params_.video_params().height()); + } + } else { + // Disables forcing size in the renderer + video_force_size = QSize(0, 0); } // Create color processor @@ -96,7 +114,9 @@ bool ExportTask::Run() audio_data_.SetLength(range.length()); } - Render(video_range, audio_range, RenderMode::kOnline, false); + Render(color_manager_, video_range, audio_range, RenderMode::kOnline, nullptr, + video_force_size, video_force_matrix, encoder_->GetDesiredPixelFormat(), + color_processor_); bool success = true; @@ -107,35 +127,28 @@ bool ExportTask::Run() encoder_->Close(); - encoder_->deleteLater(); + delete encoder_; + + // If cancelled, delete the file we made, which is always a file we created since we write to a + // temp file during the actual encoding process + if (IsCancelled()) { + QFile::remove(params_.filename()); + } else if (params_.filename() != real_filename) { + // If we were writing to a temp file, overwrite now + if (!FileFunctions::RenameFileAllowOverwrite(params_.filename(), real_filename)) { + SetError(tr("Failed to overwrite \"%1\". Export has been saved as \"%2\" instead.") + .arg(real_filename, params_.filename())); + success = false; + } + } return success; } -void FrameColorConvert(ColorProcessorPtr processor, FramePtr frame) -{ - // Color conversion must be done with unassociated alpha, and the pipeline is always associated - ColorManager::DisassociateAlpha(frame); - - // Convert color space - processor->ConvertFrame(frame); - - // Re-associate alpha - ColorManager::ReassociateAlpha(frame); -} - -QFuture ExportTask::DownloadFrame(FramePtr frame, const QByteArray &hash) -{ - rendered_frame_.insert(hash, frame); - - return QtConcurrent::run(FrameColorConvert, color_processor_, frame); -} - -void ExportTask::FrameDownloaded(const QByteArray &hash, const std::list ×, qint64 job_time) +void ExportTask::FrameDownloaded(FramePtr f, const QByteArray &hash, const QVector ×, qint64 job_time) { Q_UNUSED(job_time) - - FramePtr f = rendered_frame_.value(hash); + Q_UNUSED(hash) foreach (const rational& t, times) { time_map_.insert(t, f); @@ -154,10 +167,21 @@ void ExportTask::FrameDownloaded(const QByteArray &hash, const std::listWriteFrame(time_map_.take(real_time), real_time); frame_time_++; - } } +void FrameColorConvert(ColorProcessorPtr processor, FramePtr frame) +{ + // Color conversion must be done with unassociated alpha, and the pipeline is always associated + ColorManager::DisassociateAlpha(frame); + + // Convert color space + processor->ConvertFrame(frame); + + // Re-associate alpha + ColorManager::ReassociateAlpha(frame); +} + void ExportTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples, qint64 job_time) { Q_UNUSED(job_time) diff --git a/app/task/export/export.h b/app/task/export/export.h index 58e392ad5..4829f02c3 100644 --- a/app/task/export/export.h +++ b/app/task/export/export.h @@ -38,15 +38,16 @@ public: protected: virtual bool Run() override; - virtual QFuture DownloadFrame(FramePtr frame, const QByteArray &hash) override; - - virtual void FrameDownloaded(const QByteArray& hash, const std::list& times, qint64 job_time) override; + virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times, qint64 job_time) override; virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) override; -private: - QHash rendered_frame_; + virtual bool TwoStepFrameRendering() const override + { + return false; + } +private: QHash time_map_; ColorManager* color_manager_; diff --git a/app/task/precache/precachetask.cpp b/app/task/precache/precachetask.cpp index 8250c2d01..5d96f5978 100644 --- a/app/task/precache/precachetask.cpp +++ b/app/task/precache/precachetask.cpp @@ -20,6 +20,8 @@ #include "precachetask.h" +#include "project/project.h" + OLIVE_NAMESPACE_ENTER PreCacheTask::PreCacheTask(VideoStreamPtr footage, Sequence* sequence) : @@ -61,23 +63,21 @@ bool PreCacheTask::Run() } */ - Render(video_range, TimeRangeList(), RenderMode::kOnline, true); - - download_threads_.waitForDone(); + Render(footage_->footage()->project()->color_manager(), + video_range, + TimeRangeList(), + RenderMode::kOnline, + viewer()->video_frame_cache()); return true; } -QFuture PreCacheTask::DownloadFrame(FramePtr frame, const QByteArray &hash) -{ - return QtConcurrent::run(&download_threads_, viewer()->video_frame_cache(), &FrameHashCache::SaveCacheFrame, hash, frame); -} - -void PreCacheTask::FrameDownloaded(const QByteArray &hash, const std::list ×, qint64 job_time) +void PreCacheTask::FrameDownloaded(FramePtr frame, const QByteArray &hash, const QVector ×, qint64 job_time) { // Do nothing. Pre-cache essentially just creates more frames in the cache, it doesn't need to do // anything else. + Q_UNUSED(frame) Q_UNUSED(hash) Q_UNUSED(times) Q_UNUSED(job_time) diff --git a/app/task/precache/precachetask.h b/app/task/precache/precachetask.h index 090fedd44..960d2fa02 100644 --- a/app/task/precache/precachetask.h +++ b/app/task/precache/precachetask.h @@ -38,9 +38,7 @@ public: protected: virtual bool Run() override; - virtual QFuture DownloadFrame(FramePtr frame, const QByteArray &hash) override; - - virtual void FrameDownloaded(const QByteArray& hash, const std::list& times, qint64 job_time) override; + virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times, qint64 job_time) override; virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) override; @@ -49,8 +47,6 @@ private: VideoInput* video_node_; - QThreadPool download_threads_; - }; OLIVE_NAMESPACE_EXIT diff --git a/app/task/project/save/save.cpp b/app/task/project/save/save.cpp index 4ee1b1fc0..a196d38f7 100644 --- a/app/task/project/save/save.cpp +++ b/app/task/project/save/save.cpp @@ -38,7 +38,7 @@ ProjectSaveTask::ProjectSaveTask(ProjectPtr project) : bool ProjectSaveTask::Run() { // File to temporarily save to (ensures we can't half-write the user's main file and crash) - QString temp_save = QDir(FileFunctions::GetTempFilePath()).filePath(QStringLiteral("tempsv")); + QString temp_save = FileFunctions::GetSafeTemporaryFilename(project_->filename()); QFile project_file(temp_save); @@ -74,16 +74,15 @@ bool ProjectSaveTask::Run() } // Save was successful, we can now rewrite the original file - QFile original(project_->filename()); - if ((!original.exists() || original.remove()) - && QFile::copy(temp_save, project_->filename())) { + if (FileFunctions::RenameFileAllowOverwrite(temp_save, project_->filename())) { return true; } else { - SetError(tr("Failed to write to \"%1\".").arg(project_->filename())); + SetError(tr("Failed to overwrite \"%1\". Project has been saved as \"%2\" instead.") + .arg(project_->filename(), temp_save)); return false; } } else { - SetError(tr("Failed to open file \"%1\" for writing.").arg(project_->filename())); + SetError(tr("Failed to open temporary file \"%1\" for writing.").arg(temp_save)); return false; } } diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index d0b88d0c1..6b90dd1e4 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -22,14 +22,14 @@ #include "common/timecodefunctions.h" #include "render/rendermanager.h" -#include "threading/threadticket.h" OLIVE_NAMESPACE_ENTER RenderTask::RenderTask(ViewerOutput* viewer, const VideoParams &vparams, const AudioParams &aparams) : viewer_(viewer), video_params_(vparams), - audio_params_(aparams) + audio_params_(aparams), + running_tickets_(0) { } @@ -37,207 +37,205 @@ RenderTask::~RenderTask() { } -struct TimeHashFuturePair { - rational time; - RenderTicketPtr hash_future; -}; - -struct HashTimePair { - rational time; - QByteArray hash; -}; - -struct HashFrameFuturePair { - QByteArray hash; - RenderTicketPtr frame_future; -}; - -struct RangeSampleFuturePair { - TimeRange range; - RenderTicketPtr sample_future; -}; - -struct HashDownloadFuturePair { - QByteArray hash; - QFuture download_future; - qint64 job_time; -}; - -void RenderTask::Render(const TimeRangeList& video_range, +bool RenderTask::Render(ColorManager* manager, + const TimeRangeList& video_range, const TimeRangeList &audio_range, RenderMode::Mode mode, - bool use_disk_cache) + FrameHashCache* cache, const QSize &force_size, + const QMatrix4x4 &force_matrix, PixelFormat::Format force_format, + ColorProcessorPtr force_color_output) { - /* + // Run watchers in another thread so they can accept signals even while this thread is blocked + QThread watcher_thread; + watcher_thread.start(); + double progress_counter = 0; double total_length = 0; double video_frame_sz = video_params().time_base().toDouble(); - std::list audio_queue; - std::list audio_lookup_table; - if (!audio_range.isEmpty()) { - foreach (const TimeRange& r, audio_range) { - total_length += r.length().toDouble(); + // Store real time before any rendering takes place + qint64 job_time = QDateTime::currentMSecsSinceEpoch(); - std::list ranges = r.Split(2); - audio_queue.insert(audio_queue.end(), ranges.begin(), ranges.end()); - } + // Queue audio jobs + foreach (const TimeRange& r, audio_range) { + // Don't count audio progress, since it's generally a lot faster than video and is weighted at + // 50%, which makes the progress bar look weird to the uninitiated + //total_length += r.length().toDouble(); + + IncrementRunningTickets(); + + RenderTicketWatcher* watcher = CreateWatcher(&watcher_thread); + watcher->setProperty("range", QVariant::fromValue(r)); + watcher->SetTicket(RenderManager::instance()->RenderAudio(viewer_, r, audio_params_, false)); } - std::list render_lookup_table; - QVector times; - QVector hashes; - std::list frame_queue; - qint64 hash_job_time = 0; + // Look up hashes + QMap > time_map; if (!video_range.isEmpty()) { - times = viewer()->video_frame_cache()->GetFrameListFromTimeRange(video_range); + // Get list of discrete frames from range + QVector times = viewer()->video_frame_cache()->GetFrameListFromTimeRange(video_range); + QVector hashes(times.size()); + // Add to "total progress" total_length += video_frame_sz * times.size(); - RenderTicketPtr hash_future = RenderManager::instance()->Hash(viewer(), times); - hashes = hash_future->Get().value >(); - hash_job_time = hash_future->GetJobTime(); + // Generate hashes + for (int i=0; iWasCancelled()) { - for (int i=0;iHash(viewer(), video_params_, times.at(i)); + } + + // Filter out duplicates + for (int i=0; isetProperty("hash", hash); + + IncrementRunningTickets(); + + watcher->SetTicket(RenderManager::instance()->RenderFrame(viewer_, manager, times.at(i), + mode, video_params_, audio_params_, + force_size, force_matrix, + force_format, force_color_output, + cache)); } } } - // Start downloading frames that have finished - std::list download_futures; + finished_watcher_mutex_.lock(); - // Iterators - std::list::iterator i; - std::list::iterator j; - std::list::iterator k; + while (!IsCancelled()) { + while (!finished_watchers_.empty() && !IsCancelled()) { + RenderTicketWatcher* watcher = finished_watchers_.front(); + finished_watchers_.pop_front(); - std::list running_hashes; - std::list existing_hashes; + finished_watcher_mutex_.unlock(); - while (!IsCancelled() - && (!render_lookup_table.empty() - || !frame_queue.empty() - || !audio_queue.empty() - || !download_futures.empty() - || !audio_lookup_table.empty())) { + // Analyze watcher here + RenderManager::TicketType ticket_type = watcher->GetTicket()->property("type").value(); - while (!IsCancelled() && !frame_queue.empty()) { + if (ticket_type == RenderManager::kTypeAudio) { - // Pop another frame off the frame queue - const HashTimePair& p = frame_queue.front(); + TimeRange range = watcher->property("range").value(); - // Check if we're already rendering this hash - bool rendering_hash = (std::find(running_hashes.begin(), running_hashes.end(), p.hash) != running_hashes.end()); + AudioDownloaded(range, + watcher->Get().value(), + job_time); - // Skip this hash if we're already rendering it - if (!rendering_hash) { - // Check if this frame already exists (has already been rendered previously or during this job) - bool hash_exists = false; + // Don't count audio progress, since it's generally a lot faster than video and is weighted at + // 50%, which makes the progress bar look weird to the uninitiated + //progress_counter += range.length().toDouble(); + //emit ProgressChanged(progress_counter / total_length); - if (use_disk_cache) { - // Check if this hash is in our "existing hashes" list - hash_exists = (std::find(existing_hashes.begin(), existing_hashes.end(), p.hash) != existing_hashes.end()); + } else if (ticket_type == RenderManager::kTypeVideo && TwoStepFrameRendering()) { - // If not, check if it's in the filesystem - if (!hash_exists) { - hash_exists = QFileInfo::exists(viewer()->video_frame_cache()->CachePathName(p.hash)); + DownloadFrame(&watcher_thread, + watcher->Get().value(), + watcher->property("hash").toByteArray()); - // If so, add it to the list so we don't have to check the filesystem again later - if (hash_exists) { - existing_hashes.push_back(p.hash); - } - } - - if (hash_exists) { - // Already exists, no need to render it again - FrameDownloaded(p.hash, {p.time}, hash_job_time); - progress_counter += video_frame_sz; - emit ProgressChanged(progress_counter / total_length); - } - } - - // If no existing disk cache was found, queue it now - if (!hash_exists) { - render_lookup_table.push_back({p.hash, RenderManager::instance()->RenderFrame(viewer(), p.time, mode)}); - running_hashes.push_back(p.hash); - } - } - - // Remove first element - frame_queue.pop_front(); - } - - while (!IsCancelled() && !audio_queue.empty()) { - audio_lookup_table.push_back({audio_queue.front(), RenderManager::instance()->RenderAudio(viewer(), audio_queue.front())}); - audio_queue.pop_front(); - } - - i = render_lookup_table.begin(); - - while (!IsCancelled() && i != render_lookup_table.end()) { - if (i->frame_future->IsFinished()) { - if (!i->frame_future->WasCancelled()) { - FramePtr f = i->frame_future->Get().value(); - - // Start multithreaded download here - download_futures.push_back({i->hash, DownloadFrame(f, i->hash), i->frame_future->GetJobTime()}); - } - - i = render_lookup_table.erase(i); - } else { - i++; - } - } - - j = download_futures.begin(); - - while (!IsCancelled() && j != download_futures.end()) { - if (j->download_future.isFinished()) { - // Place it in the cache - std::list times_with_hash; - - for (int hash_index=0;hash_indexhash) { - times_with_hash.push_back(times.at(hash_index)); - } - } - - FrameDownloaded(j->hash, times_with_hash, j->job_time); - - existing_hashes.push_back(j->hash); - - // Signal process - progress_counter += times_with_hash.size() * video_frame_sz; + progress_counter += video_frame_sz * 0.5; emit ProgressChanged(progress_counter / total_length); - j = download_futures.erase(j); - } else { - j++; - } - } - k = audio_lookup_table.begin(); + // Assume single-step video or video download ticket + QByteArray rendered_hash = watcher->property("hash").toByteArray(); + FrameDownloaded(watcher->Get().value(), rendered_hash, time_map.value(rendered_hash), job_time); - while (!IsCancelled() && k != audio_lookup_table.end()) { - if (k->sample_future->IsFinished()) { - AudioDownloaded(k->range, - k->sample_future->Get().value(), - k->sample_future->GetJobTime()); + double progress_to_add = video_frame_sz; + if (TwoStepFrameRendering()) { + progress_to_add *= 0.5; + } + progress_counter += progress_to_add; - progress_counter += k->range.length().toDouble(); emit ProgressChanged(progress_counter / total_length); - k = audio_lookup_table.erase(k); - } else { - k++; } + + delete watcher; + running_watchers_.removeOne(watcher); + + finished_watcher_mutex_.lock(); + } + + if (IsCancelled()) { + break; + } + + // Run out of finished watchers. If we still have running tickets, wait for the next one to finish. + if (running_tickets_ > 0) { + finished_watcher_wait_cond_.wait(&finished_watcher_mutex_); + } else { + // No more running tickets or finished tickets, wem ust be + break; } } - */ + + finished_watcher_mutex_.unlock(); + + if (IsCancelled()) { + // Cancel every watcher we created + foreach (RenderTicketWatcher* watcher, running_watchers_) { + disconnect(watcher, &RenderTicketWatcher::Finished, this, &RenderTask::TicketDone); + watcher->Cancel(); + } + } + + watcher_thread.quit(); + watcher_thread.wait(); + + return true; +} + +void RenderTask::DownloadFrame(QThread *thread, FramePtr frame, const QByteArray &hash) +{ + RenderTicketWatcher* watcher = CreateWatcher(thread); + + watcher->setProperty("hash", hash); + + IncrementRunningTickets(); + + watcher->SetTicket(RenderManager::instance()->SaveFrameToCache(viewer_->video_frame_cache(), + frame, + hash)); +} + +RenderTicketWatcher *RenderTask::CreateWatcher(QThread *thread) +{ + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + watcher->moveToThread(thread); + connect(watcher, &RenderTicketWatcher::Finished, this, &RenderTask::TicketDone, Qt::DirectConnection); + running_watchers_.append(watcher); + return watcher; +} + +void RenderTask::IncrementRunningTickets() +{ + finished_watcher_mutex_.lock(); + running_tickets_++; + finished_watcher_mutex_.unlock(); +} + +void RenderTask::TicketDone(RenderTicketWatcher* watcher) +{ + finished_watcher_mutex_.lock(); + finished_watchers_.push_back(watcher); + finished_watcher_wait_cond_.wakeAll(); + running_tickets_--; + finished_watcher_mutex_.unlock(); } OLIVE_NAMESPACE_EXIT diff --git a/app/task/render/render.h b/app/task/render/render.h index c70e40468..9f8c1ea17 100644 --- a/app/task/render/render.h +++ b/app/task/render/render.h @@ -24,25 +24,32 @@ #include #include "node/output/viewer/viewer.h" +#include "render/colormanager.h" #include "task/task.h" +#include "threading/threadticket.h" +#include "threading/threadticketwatcher.h" OLIVE_NAMESPACE_ENTER class RenderTask : public Task { + Q_OBJECT public: RenderTask(ViewerOutput* viewer, const VideoParams &vparams, const AudioParams &aparams); virtual ~RenderTask() override; protected: - void Render(const TimeRangeList &video_range, + bool Render(ColorManager *manager, const TimeRangeList &video_range, const TimeRangeList &audio_range, RenderMode::Mode mode, - bool use_disk_cache); + FrameHashCache *cache, const QSize& force_size = QSize(0, 0), + const QMatrix4x4& force_matrix = QMatrix4x4(), + PixelFormat::Format force_format = PixelFormat::PIX_FMT_INVALID, + ColorProcessorPtr force_color_output = nullptr); - virtual QFuture DownloadFrame(FramePtr frame, const QByteArray &hash) = 0; + virtual void DownloadFrame(QThread* thread, FramePtr frame, const QByteArray &hash); - virtual void FrameDownloaded(const QByteArray& hash, const std::list& times, qint64 job_time) = 0; + virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times, qint64 job_time) = 0; virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) = 0; @@ -61,13 +68,38 @@ protected: return audio_params_; } + virtual void CancelEvent() override + { + finished_watcher_mutex_.lock(); + finished_watcher_wait_cond_.wakeAll(); + finished_watcher_mutex_.unlock(); + } + + virtual bool TwoStepFrameRendering() const + { + return true; + } + private: + RenderTicketWatcher* CreateWatcher(QThread *thread); + + void IncrementRunningTickets(); + ViewerOutput* viewer_; VideoParams video_params_; AudioParams audio_params_; + QVector running_watchers_; + std::list finished_watchers_; + int running_tickets_; + QMutex finished_watcher_mutex_; + QWaitCondition finished_watcher_wait_cond_; + +private slots: + void TicketDone(RenderTicketWatcher *watcher); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/threading/threadticketwatcher.cpp b/app/threading/threadticketwatcher.cpp index 804aaef99..247e9b69a 100644 --- a/app/threading/threadticketwatcher.cpp +++ b/app/threading/threadticketwatcher.cpp @@ -46,9 +46,9 @@ void RenderTicketWatcher::SetTicket(RenderTicketPtr ticket) if (ticket_->IsFinished(false)) { locker.unlock(); - emit Finished(); + emit Finished(this); } else { - connect(ticket_.get(), &RenderTicket::Finished, this, &RenderTicketWatcher::Finished); + connect(ticket_.get(), &RenderTicket::Finished, this, &RenderTicketWatcher::TicketFinished); } } @@ -93,4 +93,9 @@ void RenderTicketWatcher::Cancel() } } +void RenderTicketWatcher::TicketFinished() +{ + emit Finished(this); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/threading/threadticketwatcher.h b/app/threading/threadticketwatcher.h index 6bbe979c2..7684f64b6 100644 --- a/app/threading/threadticketwatcher.h +++ b/app/threading/threadticketwatcher.h @@ -49,9 +49,11 @@ public: QVariant Get(); signals: - void Finished(); + void Finished(RenderTicketWatcher* watcher); private: + void TicketFinished(); + RenderTicketPtr ticket_; }; diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index 1e8400ba6..9832f9727 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -84,7 +84,7 @@ QMatrix4x4 ViewerDisplayWidget::GetCompleteMatrixFlippedYTranslation() { QMatrix4x4 mat = combined_matrix_; - mat.data()[13] *= -1.0f; + mat.scale(1, -1, 1); return mat; } @@ -298,7 +298,9 @@ void ViewerDisplayWidget::OnPaint() } // Draw texture through color transform - renderer()->BlitColorManaged(color_service(), texture_, VideoParams(width(), height(), PixelFormat::PIX_FMT_RGBA16F), true); + renderer()->BlitColorManaged(color_service(), texture_, + VideoParams(width(), height(), PixelFormat::PIX_FMT_RGBA16F), + GetCompleteMatrixFlippedYTranslation()); } QTransform world_transform = GenerateWorldTransform();