From b582843d27b85c1a818aa558205668c8efb56bab Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 29 Oct 2020 01:36:26 +1100 Subject: [PATCH] moved things around and started rewriting render backend --- app/CMakeLists.txt | 1 + app/codec/samplebuffer.cpp | 5 + app/codec/samplebuffer.h | 1 + app/common/timerange.cpp | 16 + app/common/timerange.h | 2 + app/core.cpp | 8 +- app/node/output/viewer/viewer.cpp | 5 + app/project/item/sequence/sequence.cpp | 1 + app/render/CMakeLists.txt | 6 + app/render/backend/CMakeLists.txt | 10 - app/render/backend/opengl/CMakeLists.txt | 4 - app/render/backend/opengl/openglbackend.cpp | 43 - app/render/backend/opengl/openglbackend.h | 43 - app/render/backend/opengl/openglworker.cpp | 90 -- app/render/backend/opengl/openglworker.h | 49 - app/render/backend/renderbackend.cpp | 888 ------------------ app/render/backend/renderbackend.h | 259 ----- app/render/backend/rendercontext.cpp | 6 + app/render/backend/rendercontext.h | 11 + app/render/backend/renderframebuffer.cpp | 6 + app/render/backend/renderframebuffer.h | 11 + app/render/backend/rendershader.cpp | 6 + app/render/backend/rendershader.h | 11 + app/render/backend/rendertexture.cpp | 6 + app/render/backend/rendertexture.h | 12 + app/render/backend/renderworker.cpp | 471 ---------- app/render/backend/renderworker.h | 208 ---- .../{backend => }/colorprocessorcache.h | 0 app/render/{backend => }/decodercache.h | 0 app/render/previewautocacher.cpp | 695 ++++++++++++++ app/render/previewautocacher.h | 198 ++++ app/render/rendermanager.cpp | 169 ++++ app/render/rendermanager.h | 115 +++ app/task/export/export.cpp | 10 +- app/task/precache/precachetask.cpp | 9 +- app/task/render/render.cpp | 26 +- app/task/render/render.h | 24 +- app/threading/CMakeLists.txt | 26 + app/threading/threadpool.cpp | 138 +++ app/threading/threadpool.h | 94 ++ .../threadticket.cpp} | 59 +- .../threadticket.h} | 29 +- .../threadticketwatcher.cpp} | 9 +- .../threadticketwatcher.h} | 4 +- app/widget/viewer/viewer.cpp | 52 +- app/widget/viewer/viewer.h | 13 +- 46 files changed, 1650 insertions(+), 2199 deletions(-) delete mode 100644 app/render/backend/opengl/openglbackend.cpp delete mode 100644 app/render/backend/opengl/openglbackend.h delete mode 100644 app/render/backend/opengl/openglworker.cpp delete mode 100644 app/render/backend/opengl/openglworker.h delete mode 100644 app/render/backend/renderbackend.cpp delete mode 100644 app/render/backend/renderbackend.h create mode 100644 app/render/backend/rendercontext.cpp create mode 100644 app/render/backend/rendercontext.h create mode 100644 app/render/backend/renderframebuffer.cpp create mode 100644 app/render/backend/renderframebuffer.h create mode 100644 app/render/backend/rendershader.cpp create mode 100644 app/render/backend/rendershader.h create mode 100644 app/render/backend/rendertexture.cpp create mode 100644 app/render/backend/rendertexture.h delete mode 100644 app/render/backend/renderworker.cpp delete mode 100644 app/render/backend/renderworker.h rename app/render/{backend => }/colorprocessorcache.h (100%) rename app/render/{backend => }/decodercache.h (100%) create mode 100644 app/render/previewautocacher.cpp create mode 100644 app/render/previewautocacher.h create mode 100644 app/render/rendermanager.cpp create mode 100644 app/render/rendermanager.h create mode 100644 app/threading/CMakeLists.txt create mode 100644 app/threading/threadpool.cpp create mode 100644 app/threading/threadpool.h rename app/{render/backend/renderticket.cpp => threading/threadticket.cpp} (63%) rename app/{render/backend/renderticket.h => threading/threadticket.h} (84%) rename app/{render/backend/renderticketwatcher.cpp => threading/threadticketwatcher.cpp} (93%) rename app/{render/backend/renderticketwatcher.h => threading/threadticketwatcher.h} (96%) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index a5177e705..7e7487ba5 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -41,6 +41,7 @@ add_subdirectory(project) add_subdirectory(render) add_subdirectory(shaders) add_subdirectory(task) +add_subdirectory(threading) add_subdirectory(timeline) add_subdirectory(tool) add_subdirectory(ui) diff --git a/app/codec/samplebuffer.cpp b/app/codec/samplebuffer.cpp index fdd25b825..8c8fdc3b9 100644 --- a/app/codec/samplebuffer.cpp +++ b/app/codec/samplebuffer.cpp @@ -38,6 +38,11 @@ SampleBufferPtr SampleBuffer::Create() return std::make_shared(); } +SampleBufferPtr SampleBuffer::CreateAllocated(const AudioParams &audio_params, const rational &length) +{ + return CreateAllocated(audio_params, audio_params.time_to_samples(length)); +} + SampleBufferPtr SampleBuffer::CreateAllocated(const AudioParams &audio_params, int samples_per_channel) { SampleBufferPtr buffer = Create(); diff --git a/app/codec/samplebuffer.h b/app/codec/samplebuffer.h index 891e9156d..eef51ff8a 100644 --- a/app/codec/samplebuffer.h +++ b/app/codec/samplebuffer.h @@ -46,6 +46,7 @@ public: virtual ~SampleBuffer(); static SampleBufferPtr Create(); + static SampleBufferPtr CreateAllocated(const AudioParams& audio_params, const rational& length); static SampleBufferPtr CreateAllocated(const AudioParams& audio_params, int samples_per_channel); static SampleBufferPtr CreateFromPackedData(const AudioParams& audio_params, const QByteArray& bytes); diff --git a/app/common/timerange.cpp b/app/common/timerange.cpp index d8b2a5b6b..fde65bbf3 100644 --- a/app/common/timerange.cpp +++ b/app/common/timerange.cpp @@ -20,6 +20,7 @@ #include "timerange.h" +#include #include OLIVE_NAMESPACE_ENTER @@ -148,6 +149,21 @@ const TimeRange &TimeRange::operator-=(const rational &rhs) return *this; } +std::list TimeRange::Split(const int &chunk_size) const +{ + std::list split_ranges; + + int start_time = qFloor(this->in().toDouble() / static_cast(chunk_size)) * chunk_size; + int end_time = qCeil(this->out().toDouble() / static_cast(chunk_size)) * chunk_size; + + for (int i=start_time; iin(), rational(i)), + qMin(this->out(), rational(i + chunk_size)))); + } + + return split_ranges; +} + void TimeRange::normalize() { // If `out` is earlier than `in`, swap them diff --git a/app/common/timerange.h b/app/common/timerange.h index de5785b5f..109d9361b 100644 --- a/app/common/timerange.h +++ b/app/common/timerange.h @@ -56,6 +56,8 @@ public: const TimeRange& operator+=(const rational &rhs); const TimeRange& operator-=(const rational &rhs); + std::list Split(const int &chunk_size) const; + private: void normalize(); diff --git a/app/core.cpp b/app/core.cpp index 8db737dbd..449197683 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -53,6 +53,7 @@ #include "render/colormanager.h" #include "render/diskmanager.h" #include "render/pixelformat.h" +#include "render/rendermanager.h" #include "render/shaderinfo.h" #ifdef USE_OTIO #include "task/project/loadotio/loadotio.h" @@ -135,8 +136,8 @@ void Core::Start() // Initialize task manager TaskManager::CreateInstance(); - // Initialize OpenGL service - OpenGLProxy::CreateInstance(); + // Initialize RenderManager + RenderManager::CreateInstance(); // // Start application @@ -180,7 +181,7 @@ void Core::Stop() } } - OpenGLProxy::DestroyInstance(); + RenderManager::DestroyInstance(); MenuShared::DestroyInstance(); @@ -197,6 +198,7 @@ void Core::Stop() NodeFactory::Destroy(); delete main_window_; + main_window_ = nullptr; } MainWindow *Core::main_window() diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index ca99b0837..ae5788929 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -164,6 +164,8 @@ void ViewerOutput::set_video_params(const VideoParams &video) } emit VideoParamsChanged(); + + video_frame_cache_.InvalidateAll(); } void ViewerOutput::set_audio_params(const AudioParams &audio) @@ -171,6 +173,9 @@ void ViewerOutput::set_audio_params(const AudioParams &audio) audio_params_ = audio; emit AudioParamsChanged(); + + // This will automatically InvalidateAll + audio_playback_cache_.SetParameters(audio_params()); } rational ViewerOutput::GetLength() diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index ab61093a1..91042e555 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -21,6 +21,7 @@ #include "sequence.h" #include +#include #include "config/config.h" #include "common/channellayout.h" diff --git a/app/render/CMakeLists.txt b/app/render/CMakeLists.txt index 709a8c5e5..44b590ee6 100644 --- a/app/render/CMakeLists.txt +++ b/app/render/CMakeLists.txt @@ -28,7 +28,9 @@ set(OLIVE_SOURCES render/colormanager.h render/colormanager.cpp render/colorprocessor.h + render/colorprocessorcache.h render/colorprocessor.cpp + render/decodercache.h render/diskmanager.h render/diskmanager.cpp render/framehashcache.h @@ -39,6 +41,10 @@ set(OLIVE_SOURCES render/pixelformat.cpp render/playbackcache.h render/playbackcache.cpp + render/previewautocacher.h + render/previewautocacher.cpp + render/rendermanager.h + render/rendermanager.cpp render/rendermodes.h render/shaderinfo.h render/videoparams.h diff --git a/app/render/backend/CMakeLists.txt b/app/render/backend/CMakeLists.txt index 79650bd7b..8dc59590c 100644 --- a/app/render/backend/CMakeLists.txt +++ b/app/render/backend/CMakeLists.txt @@ -18,15 +18,5 @@ add_subdirectory(opengl) set(OLIVE_SOURCES ${OLIVE_SOURCES} - render/backend/colorprocessorcache.h - render/backend/decodercache.h - render/backend/renderbackend.h - render/backend/renderbackend.cpp - render/backend/renderticket.h - render/backend/renderticket.cpp - render/backend/renderticketwatcher.h - render/backend/renderticketwatcher.cpp - render/backend/renderworker.h - render/backend/renderworker.cpp PARENT_SCOPE ) diff --git a/app/render/backend/opengl/CMakeLists.txt b/app/render/backend/opengl/CMakeLists.txt index f1a46cbd7..cb8f1b674 100644 --- a/app/render/backend/opengl/CMakeLists.txt +++ b/app/render/backend/opengl/CMakeLists.txt @@ -16,8 +16,6 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - render/backend/opengl/openglbackend.h - render/backend/opengl/openglbackend.cpp render/backend/opengl/openglcolorprocessor.h render/backend/opengl/openglcolorprocessor.cpp render/backend/opengl/openglframebuffer.h @@ -32,7 +30,5 @@ set(OLIVE_SOURCES render/backend/opengl/opengltexture.cpp render/backend/opengl/opengltexturecache.h render/backend/opengl/opengltexturecache.cpp - render/backend/opengl/openglworker.h - render/backend/opengl/openglworker.cpp PARENT_SCOPE ) diff --git a/app/render/backend/opengl/openglbackend.cpp b/app/render/backend/opengl/openglbackend.cpp deleted file mode 100644 index 60e81f914..000000000 --- a/app/render/backend/opengl/openglbackend.cpp +++ /dev/null @@ -1,43 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include "openglbackend.h" - -#include "openglworker.h" - -OLIVE_NAMESPACE_ENTER - -OpenGLBackend::OpenGLBackend(QObject* parent) : - RenderBackend(parent) -{ - -} - -OpenGLBackend::~OpenGLBackend() -{ - Close(); -} - -RenderWorker *OpenGLBackend::CreateNewWorker() -{ - return new OpenGLWorker(this); -} - -OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/opengl/openglbackend.h b/app/render/backend/opengl/openglbackend.h deleted file mode 100644 index 7d88611f3..000000000 --- a/app/render/backend/opengl/openglbackend.h +++ /dev/null @@ -1,43 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef OPENGLBACKEND_H -#define OPENGLBACKEND_H - -#include "openglproxy.h" -#include "render/backend/renderbackend.h" - -OLIVE_NAMESPACE_ENTER - -class OpenGLBackend : public RenderBackend -{ -public: - OpenGLBackend(QObject* parent = nullptr); - - virtual ~OpenGLBackend() override; - -protected: - virtual RenderWorker* CreateNewWorker() override; - -}; - -OLIVE_NAMESPACE_EXIT - -#endif // OPENGLBACKEND_H diff --git a/app/render/backend/opengl/openglworker.cpp b/app/render/backend/opengl/openglworker.cpp deleted file mode 100644 index ec518ffc7..000000000 --- a/app/render/backend/opengl/openglworker.cpp +++ /dev/null @@ -1,90 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include "openglworker.h" - -OLIVE_NAMESPACE_ENTER - -OpenGLWorker::OpenGLWorker(RenderBackend *parent) : - RenderWorker(parent) -{ -} - -void OpenGLWorker::TextureToFrame(const QVariant &texture, FramePtr frame, const QMatrix4x4& mat) const -{ - QMetaObject::invokeMethod(OpenGLProxy::instance(), - "TextureToBuffer", - Qt::BlockingQueuedConnection, - Q_ARG(const QVariant&, texture), - OLIVE_NS_ARG(FramePtr, frame), - Q_ARG(const QMatrix4x4&, mat)); -} - -QVariant OpenGLWorker::FootageFrameToTexture(StreamPtr stream, FramePtr frame) const -{ - QVariant value; - - QMetaObject::invokeMethod(OpenGLProxy::instance(), - "FrameToValue", - Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QVariant, value), - OLIVE_NS_ARG(FramePtr, frame), - OLIVE_NS_ARG(StreamPtr, stream), - OLIVE_NS_CONST_ARG(VideoParams&, video_params()), - OLIVE_NS_CONST_ARG(RenderMode::Mode&, render_mode())); - - return value; -} - -QVariant OpenGLWorker::CachedFrameToTexture(FramePtr frame) const -{ - QVariant value; - - QMetaObject::invokeMethod(OpenGLProxy::instance(), - "PreCachedFrameToValue", - Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QVariant, value), - OLIVE_NS_ARG(FramePtr, frame)); - - return value; -} - -QVariant OpenGLWorker::ProcessShader(const Node *node, const TimeRange &range, const ShaderJob &job) -{ - QVariant value; - - QMetaObject::invokeMethod(OpenGLProxy::instance(), - "RunNodeAccelerated", - Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QVariant, value), - OLIVE_NS_CONST_ARG(Node*, node), - OLIVE_NS_CONST_ARG(TimeRange&, range), - OLIVE_NS_CONST_ARG(ShaderJob&, job), - OLIVE_NS_CONST_ARG(VideoParams&, video_params())); - - return value; -} - -bool OpenGLWorker::TextureHasAlpha(const QVariant &v) const -{ - return PixelFormat::FormatHasAlphaChannel(v.value()->texture()->format()); -} - -OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/opengl/openglworker.h b/app/render/backend/opengl/openglworker.h deleted file mode 100644 index 75eed65f8..000000000 --- a/app/render/backend/opengl/openglworker.h +++ /dev/null @@ -1,49 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef OPENGLWORKER_H -#define OPENGLWORKER_H - -#include "openglproxy.h" -#include "render/backend/renderworker.h" - -OLIVE_NAMESPACE_ENTER - -class OpenGLWorker : public RenderWorker -{ -public: - OpenGLWorker(RenderBackend* parent); - -protected: - virtual void TextureToFrame(const QVariant& texture, FramePtr frame, const QMatrix4x4 &mat) const override; - - virtual QVariant FootageFrameToTexture(StreamPtr stream, FramePtr frame) const override; - - virtual QVariant CachedFrameToTexture(FramePtr frame) const override; - - virtual QVariant ProcessShader(const Node *node, const TimeRange &range, const ShaderJob& job) override; - - virtual bool TextureHasAlpha(const QVariant& v) const override; - -}; - -OLIVE_NAMESPACE_EXIT - -#endif // OPENGLWORKER_H diff --git a/app/render/backend/renderbackend.cpp b/app/render/backend/renderbackend.cpp deleted file mode 100644 index 6e469e07e..000000000 --- a/app/render/backend/renderbackend.cpp +++ /dev/null @@ -1,888 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include "renderbackend.h" - -#include -#include -#include - -#include "config/config.h" -#include "core.h" -#include "task/conform/conform.h" -#include "task/taskmanager.h" -#include "window/mainwindow/mainwindow.h" - -OLIVE_NAMESPACE_ENTER - -QVector RenderBackend::instances_; -QMutex RenderBackend::instance_lock_; -RenderBackend* RenderBackend::active_instance_ = nullptr; -QThreadPool RenderBackend::thread_pool_; - -RenderBackend::RenderBackend(QObject *parent) : - QObject(parent), - viewer_node_(nullptr), - video_force_download_resolution_(false), - autocache_enabled_(false), - autocache_paused_(false), - generate_audio_previews_(false), - render_mode_(RenderMode::kOnline), - autocache_has_changed_(false), - use_custom_autocache_range_(false), - ignore_next_mouse_button_(false) -{ - instance_lock_.lock(); - instances_.append(this); - instance_lock_.unlock(); - - // Set default autocache range - SetAutoCachePlayhead(rational()); -} - -RenderBackend::~RenderBackend() -{ - Close(); -} - -void RenderBackend::SetViewerNode(ViewerOutput *viewer_node) -{ - if (viewer_node_ == viewer_node) { - return; - } - - ViewerOutput* old_viewer = viewer_node_; - if (!viewer_node) { - // If setting to null, set it here before we wait for jobs to finish to prevent WorkerFinished() - // from calling RunNextJob() again and preventing us from finishing - viewer_node_ = nullptr; - } - - if (old_viewer) { - // Cancel any remaining tickets - ClearQueue(); - - // Wait for any currently running jobs to finish - foreach (RenderTicketPtr ticket, running_tickets_) { - ticket->WaitForFinished(); - } - - // Clear autocache lists - { - // This can be cleared normally (hashes will be discarded and need to be calculated again) - autocache_hash_tasks_.clear(); - - // We need to wait for these since they work directly on the FrameHashCache. Most of the time - // this is fine, but not if the FrameHashCache gets deleted after this function. - foreach (QFutureWatcher* watcher, autocache_hash_process_tasks_) { - watcher->waitForFinished(); - } - autocache_hash_process_tasks_.clear(); - - // This can be cleared normally (frames will be discarded and need to be rendered again) - autocache_video_tasks_.clear(); - - // This can be cleared normally (PCM data will be discarded and need to be rendered again) - autocache_audio_tasks_.clear(); - - // We'll need to wait for these since they work directly on the FrameHashCache. Frames will - // be in the cache for later use. - { - QMap*, QByteArray>::const_iterator i; - for (i=autocache_video_download_tasks_.constBegin(); i!=autocache_video_download_tasks_.constEnd(); i++) { - i.key()->waitForFinished(); - } - autocache_video_download_tasks_.clear(); - } - - // No longer caching any hashes - autocache_currently_caching_hashes_.clear(); - } - - // Delete all of our copied nodes - foreach (Node* c, copy_map_) { - c->deleteLater(); - } - copy_map_.clear(); - copied_viewer_node_ = nullptr; - graph_update_queue_.clear(); - - // Disconnect signal (will be a no-op if the signal was never connected) - disconnect(old_viewer, - &ViewerOutput::GraphChangedFrom, - this, - &RenderBackend::NodeGraphChanged); - - disconnect(old_viewer->video_frame_cache(), - &PlaybackCache::Invalidated, - this, - &RenderBackend::AutoCacheVideoInvalidated); - - disconnect(old_viewer->audio_playback_cache(), - &PlaybackCache::Invalidated, - this, - &RenderBackend::AutoCacheAudioInvalidated); - - foreach (const WorkerData& worker, workers_) { - worker.worker->ClearDecoders(); - } - } - - if (viewer_node) { - // If setting to non-null, set it now - viewer_node_ = viewer_node; - - // Copy graph - copied_viewer_node_ = static_cast(viewer_node_->copy()); - copy_map_.insert(viewer_node_, copied_viewer_node_); - - // We begin an operation and never end it which prevents the copy from unnecessarily - // invalidating its own cache - copied_viewer_node_->BeginOperation(); - - NodeGraphChanged(viewer_node_->texture_input()); - NodeGraphChanged(viewer_node_->samples_input()); - ProcessUpdateQueue(); - - if (autocache_enabled_) { - connect(viewer_node_, - &ViewerOutput::GraphChangedFrom, - this, - &RenderBackend::NodeGraphChanged); - - connect(viewer_node_->video_frame_cache(), - &PlaybackCache::Invalidated, - this, - &RenderBackend::AutoCacheVideoInvalidated); - - connect(viewer_node_->audio_playback_cache(), - &PlaybackCache::Invalidated, - this, - &RenderBackend::AutoCacheAudioInvalidated); - } - } -} - -void RenderBackend::AutoCacheRange(const TimeRange &range) -{ - Q_ASSERT(autocache_enabled_); - - autocache_has_changed_ = true; - use_custom_autocache_range_ = true; - custom_autocache_range_ = range; - - AutoCacheRequeueFrames(); -} - -RenderTicketPtr RenderBackend::Hash(const QVector ×, bool prioritize) -{ - Q_ASSERT(viewer_node_); - - SetActiveInstance(); - - RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeHash, - QVariant::fromValue(times)); - - if (prioritize) { - render_queue_.push_front(ticket); - } else { - render_queue_.push_back(ticket); - } - - QMetaObject::invokeMethod(this, "RunNextJob", Qt::QueuedConnection); - - return ticket; -} - -RenderTicketPtr RenderBackend::RenderFrame(const rational &time, bool prioritize, const QByteArray& hash) -{ - Q_ASSERT(viewer_node_); - - SetActiveInstance(); - - RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeVideo, - QVariant::fromValue(time)); - - ticket->setProperty("hash", hash); - - if (prioritize) { - render_queue_.push_front(ticket); - } else { - render_queue_.push_back(ticket); - } - - QMetaObject::invokeMethod(this, "RunNextJob", Qt::QueuedConnection); - - return ticket; -} - -RenderTicketPtr RenderBackend::RenderAudio(const TimeRange &r, bool prioritize) -{ - Q_ASSERT(viewer_node_); - - SetActiveInstance(); - - RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeAudio, - QVariant::fromValue(r)); - - if (prioritize) { - render_queue_.push_front(ticket); - } else { - render_queue_.push_back(ticket); - } - - QMetaObject::invokeMethod(this, "RunNextJob", Qt::QueuedConnection); - - return ticket; -} - -void RenderBackend::SetVideoParams(const VideoParams ¶ms) -{ - video_params_ = params; -} - -void RenderBackend::SetAudioParams(const AudioParams ¶ms) -{ - audio_params_ = params; -} - -void RenderBackend::IgnoreNextMouseButton() -{ - ignore_next_mouse_button_ = true; -} - -std::list RenderBackend::SplitRangeIntoChunks(const TimeRange &r) -{ - // FIXME: Magic number - const int chunk_size = 2; - - std::list split_ranges; - - int start_time = qFloor(r.in().toDouble() / static_cast(chunk_size)) * chunk_size; - int end_time = qCeil(r.out().toDouble() / static_cast(chunk_size)) * chunk_size; - - for (int i=start_time; iCancel(); - } - render_queue_.clear(); -} - -void RenderBackend::NodeGraphChanged(NodeInput *source) -{ - // We need to determine: - // - If we don't have this input, assume that it's coming soon and ignore it - // - If we do, is this input a child of another input we're already copying? - // - Or are any of the queued inputs children of this one? - - // First we need to find our copy of the input being queued - Node* our_copy_node = copy_map_.value(source->parentNode()); - - // If we don't have this node yet, assume it's coming in a later copy in which case it'll be - // copied then - if (!our_copy_node) { - // Assert that there are updates coming - Q_ASSERT(!graph_update_queue_.isEmpty()); - return; - } - - // If we're here, we must have this node. Determine if we're already copying a "parent" of this - for (int i=0; iIsArray() && static_cast(source)->sub_params().contains(queued_input)) - || queued_input->parentNode()->OutputsTo(source, true, true)) { - // In which case, we don't need to queue it and can queue our own - graph_update_queue_.removeAt(i); - disconnect(queued_input, &NodeInput::destroyed, this, &RenderBackend::QueuedInputRemoved); - i--; - } - - // Check if the source is a member of this array, in which case it'll be copied eventually anyway - if (queued_input->IsArray() - && static_cast(queued_input)->sub_params().contains(source)) { - return; - } - - // Check if this dependency graph is already queued - if (source->parentNode()->OutputsTo(queued_input, true, true)) { - // In which case, no further copy is necessary - return; - } - } - - graph_update_queue_.append(source); - connect(source, &NodeInput::destroyed, this, &RenderBackend::QueuedInputRemoved); -} - -void RenderBackend::Close() -{ - SetViewerNode(nullptr); - - for (int i=0;ideleteLater(); - } - workers_.clear(); -} - -void RenderBackend::RunNextJob() -{ - // If queue is empty, nothing to be done - if (render_queue_.empty()) { - - // If we're the active instance, unset it - instance_lock_.lock(); - if (active_instance_ == this) { - active_instance_ = nullptr; - } - instance_lock_.unlock(); - - return; - } - - // If we have a value update queued, check if all workers are available and proceed from there - if (autocache_enabled_ && !graph_update_queue_.isEmpty()) { - bool all_workers_available = true; - - foreach (const WorkerData& data, workers_) { - if (data.busy) { - all_workers_available = false; - break; - } - } - - if (all_workers_available) { - // Process queue - ProcessUpdateQueue(); - } else { - return; - } - } - - // If we have no workers allocated, allocate them now - if (workers_.isEmpty()) { - // Allocate workers here - workers_.resize(thread_pool_.maxThreadCount()); - - for (int i=0;iSetVideoParams(video_params_); - worker->SetAudioParams(audio_params_); - worker->SetForceDownloadResolution(video_force_download_resolution_); - worker->SetVideoDownloadMatrix(video_download_matrix_); - worker->SetRenderMode(render_mode_); - worker->SetPreviewGenerationEnabled(generate_audio_previews_); - worker->SetCopyMap(©_map_); - worker->SetCachePath(viewer_node_->video_frame_cache()->GetCacheDirectory()); - - // Move ticket from queue to running list - RenderTicketPtr ticket = render_queue_.front(); - render_queue_.pop_front(); - running_tickets_.push_back(ticket); - - // Create watcher to remove from running list - RenderTicketWatcher* watcher = new RenderTicketWatcher(); - connect(watcher, &RenderTicketWatcher::Finished, this, &RenderBackend::TicketFinished); - watcher->SetTicket(ticket); - - // Set job time to now - ticket->SetJobTime(); - - switch (ticket->GetType()) { - case RenderTicket::kTypeHash: - Q_ASSERT(video_params_.is_valid()); - - QtConcurrent::run(&thread_pool_, - worker, - &RenderWorker::Hash, - ticket, - copied_viewer_node_, - ticket->GetTime().value >()); - break; - case RenderTicket::kTypeVideo: - { - Q_ASSERT(video_params_.is_valid()); - - rational frame = ticket->GetTime().value(); - - QtConcurrent::run(&thread_pool_, - worker, - &RenderWorker::RenderFrame, - ticket, - copied_viewer_node_, - frame); - - QByteArray frame_hash = ticket->property("hash").toByteArray(); - if (!frame_hash.isEmpty()) { - autocache_currently_caching_hashes_.append(frame_hash); - } - break; - } - case RenderTicket::kTypeAudio: - Q_ASSERT(audio_params_.is_valid()); - - QtConcurrent::run(&thread_pool_, - worker, - &RenderWorker::RenderAudio, - ticket, - copied_viewer_node_, - ticket->GetTime().value()); - break; - } - - if (render_queue_.empty()) { - // No more jobs, can exit here - break; - } - } - } -} - -void RenderBackend::TicketFinished() -{ - RenderTicketPtr ticket = static_cast(sender())->GetTicket(); - delete sender(); - - running_tickets_.remove(ticket); -} - -void RenderBackend::WorkerGeneratedWaveform(RenderTicketPtr ticket, TrackOutput *track, AudioVisualWaveform samples, TimeRange range) -{ - QList valid_ranges = viewer_node_->audio_playback_cache()->GetValidRanges(range, - ticket->GetJobTime()); - if (!valid_ranges.isEmpty()) { - // Generate visual waveform in this background thread - track->waveform_lock()->lock(); - - track->waveform().set_channel_count(audio_params_.channel_count()); - - foreach (const TimeRange& r, valid_ranges) { - track->waveform().OverwriteSums(samples, r.in(), r.in() - range.in(), r.length()); - } - - track->waveform_lock()->unlock(); - - emit track->PreviewChanged(); - } -} - -void RenderBackend::AutoCacheVideoInvalidated(const TimeRange &range) -{ - ClearVideoQueue(); - - // Hash these frames since that should be relatively quick. - if (ignore_next_mouse_button_ || !(qApp->mouseButtons() & Qt::LeftButton)) { - ignore_next_mouse_button_ = false; - RenderTicketWatcher* watcher = new RenderTicketWatcher(); - QVector frames = viewer_node_->video_frame_cache()->GetFrameListFromTimeRange({range}); - autocache_hash_tasks_.insert(watcher, frames); - connect(watcher, &RenderTicketWatcher::Finished, this, &RenderBackend::AutoCacheHashesGenerated); - watcher->SetTicket(Hash(frames)); - } -} - -void RenderBackend::AutoCacheAudioInvalidated(const TimeRange &range) -{ - // Start a task to re-render the audio at this range - RenderTicketWatcher* watcher = new RenderTicketWatcher(); - autocache_audio_tasks_.insert(watcher, range); - connect(watcher, &RenderTicketWatcher::Finished, this, &RenderBackend::AutoCacheAudioRendered); - watcher->SetTicket(RenderAudio(range, true)); -} - -void RenderBackend::SetHashes(FrameHashCache* cache, const QVector& times, const QVector& hashes, qint64 job_time) -{ - std::vector existing_hashes; - - for (int i=0; iCachePathName(hash)); - - if (hash_exists) { - existing_hashes.push_back(hash); - } - } - - QMetaObject::invokeMethod(cache, "SetHash", Qt::QueuedConnection, - OLIVE_NS_ARG(rational, time), - Q_ARG(QByteArray, hash), - Q_ARG(qint64, job_time), - Q_ARG(bool, hash_exists)); - } -} - -void RenderBackend::AutoCacheHashesGenerated() -{ - RenderTicketWatcher* watcher = static_cast(sender()); - - if (autocache_hash_tasks_.contains(watcher)) { - if (!watcher->WasCancelled()) { - QFutureWatcher* hw = new QFutureWatcher(); - connect(hw, &QFutureWatcher::finished, this, &RenderBackend::AutoCacheHashesProcessed); - autocache_hash_process_tasks_.append(hw); - hw->setFuture(QtConcurrent::run(this, - &RenderBackend::SetHashes, - viewer_node_->video_frame_cache(), - autocache_hash_tasks_.value(watcher), - watcher->Get().value >(), - watcher->GetTicket()->GetJobTime())); - } - - autocache_hash_tasks_.remove(watcher); - } - - delete watcher; -} - -void RenderBackend::AutoCacheHashesProcessed() -{ - QFutureWatcher* watcher = static_cast*>(sender()); - - if (autocache_hash_process_tasks_.contains(watcher)) { - autocache_hash_process_tasks_.removeOne(watcher); - - AutoCacheRequeueFrames(); - } - - delete watcher; -} - -void RenderBackend::AutoCacheAudioRendered() -{ - RenderTicketWatcher* watcher = static_cast(sender()); - - if (autocache_audio_tasks_.contains(watcher)) { - if (!watcher->WasCancelled()) { - viewer_node_->audio_playback_cache()->WritePCM(autocache_audio_tasks_.value(watcher), - watcher->Get().value(), - watcher->GetTicket()->GetJobTime()); - } - - autocache_audio_tasks_.remove(watcher); - } - - delete watcher; -} - -void RenderBackend::AutoCacheVideoRendered() -{ - RenderTicketWatcher* watcher = static_cast(sender()); - - if (autocache_video_tasks_.contains(watcher)) { - if (!watcher->WasCancelled()) { - const QByteArray& hash = autocache_video_tasks_.value(watcher); - - // Download frame in another thread - QFutureWatcher* w = new QFutureWatcher(); - autocache_video_download_tasks_.insert(w, hash); - connect(w, &QFutureWatcher::finished, this, &RenderBackend::AutoCacheVideoDownloaded); - w->setFuture(QtConcurrent::run(viewer_node_->video_frame_cache(), - &FrameHashCache::SaveCacheFrame, - hash, - watcher->Get().value())); - } - - autocache_video_tasks_.remove(watcher); - } - - delete watcher; -} - -void RenderBackend::AutoCacheVideoDownloaded() -{ - QFutureWatcher* watcher = static_cast*>(sender()); - - if (autocache_video_download_tasks_.contains(watcher)) { - if (!watcher->isCanceled()) { - if (watcher->result()) { - const QByteArray& hash = autocache_video_download_tasks_.value(watcher); - - autocache_currently_caching_hashes_.removeOne(hash); - - viewer_node_->video_frame_cache()->ValidateFramesWithHash(hash); - } else { - qCritical() << "Failed to download video frame"; - } - } - - autocache_video_download_tasks_.remove(watcher); - } - - delete watcher; -} - -void RenderBackend::QueuedInputRemoved() -{ - NodeInput* i = static_cast(sender()); - disconnect(i, &NodeInput::destroyed, this, &RenderBackend::QueuedInputRemoved); - graph_update_queue_.removeOne(i); -} - -//#define PRINT_UPDATE_QUEUE_INFO -void RenderBackend::ProcessUpdateQueue() -{ -#ifdef PRINT_UPDATE_QUEUE_INFO - qint64 t = QDateTime::currentMSecsSinceEpoch(); - qDebug() << "Processing update queue of" << graph_update_queue_.size() << "elements:"; -#endif - - while (!graph_update_queue_.isEmpty()) { - NodeInput* i = graph_update_queue_.takeFirst(); -#ifdef PRINT_UPDATE_QUEUE_INFO - qDebug() << " " << i->parentNode()->id() << i->id(); -#endif - disconnect(i, &NodeInput::destroyed, this, &RenderBackend::QueuedInputRemoved); - - CopyNodeInputValue(i); - } - -#ifdef PRINT_UPDATE_QUEUE_INFO - qDebug() << "Update queue took:" << (QDateTime::currentMSecsSinceEpoch() - t); -#endif -} - -void RenderBackend::WorkerFinished() -{ - RenderWorker* worker = static_cast(sender()); - - // Set busy state to false - for (int i=0;iparentNode()); - Q_ASSERT(our_copy_node); - NodeInput* our_copy = our_copy_node->GetInputWithID(input->id()); - - // Copy the standard/keyframe values between these two inputs - NodeInput::CopyValues(input, - our_copy, - false, - false); - - // Handle connections - if (input->is_connected() || our_copy->is_connected()) { - // If one of the inputs is connected, it's likely this change came from connecting or - // disconnecting whatever was connected to it - - // We start by removing all old dependencies from the map - QList old_deps = our_copy->GetExclusiveDependencies(); - foreach (Node* i, old_deps) { - copy_map_.take(copy_map_.key(i))->deleteLater(); - } - - // And clear any other edges - while (!our_copy->edges().isEmpty()) { - NodeParam::DisconnectEdge(our_copy->edges().first()); - } - - // Then we copy all node dependencies and connections (if there are any) - CopyNodeMakeConnection(input, our_copy); - } - - // Call on sub-elements too - if (input->IsArray()) { - foreach (NodeInput* i, static_cast(input)->sub_params()) { - CopyNodeInputValue(i); - } - } -} - -Node* RenderBackend::CopyNodeConnections(Node* src_node) -{ - // Check if this node is already in the map - Node* dst_node = copy_map_.value(src_node); - - // If not, create it now - if (!dst_node) { - dst_node = src_node->copy(); - - if (dst_node->IsTrack()) { - // Hack that ensures the track type is set since we don't bother copying the whole timeline - static_cast(dst_node)->set_track_type(static_cast(src_node)->track_type()); - } - - copy_map_.insert(src_node, dst_node); - } - - // Make sure its values are copied - Node::CopyInputs(src_node, dst_node, false); - - // Copy all connections - QList src_node_inputs = src_node->GetInputsIncludingArrays(); - QList dst_node_inputs = dst_node->GetInputsIncludingArrays(); - - for (int i=0;iis_connected()) { - Node* dst_node = CopyNodeConnections(src_input->get_connected_node()); - - NodeOutput* corresponding_output = dst_node->GetOutputWithID(src_input->get_connected_output()->id()); - - NodeParam::ConnectEdge(corresponding_output, - dst_input); - } -} - -void RenderBackend::ClearQueueOfType(RenderTicket::Type type) -{ - std::list::iterator i = render_queue_.begin(); - - while (i != render_queue_.end()) { - if ((*i)->GetType() == type) { - (*i)->Cancel(); - i = render_queue_.erase(i); - } else { - i++; - } - } -} - -void RenderBackend::SetActiveInstance() -{ - QMutexLocker locker(&instance_lock_); - - if (active_instance_ != this) { - // Signal active instance to stop - QMetaObject::invokeMethod(active_instance_, "ClearVideoQueue", Qt::QueuedConnection); - - active_instance_ = this; - } -} - -void RenderBackend::AutoCacheRequeueFrames() -{ - if (viewer_node_ - && viewer_node_->video_frame_cache()->HasInvalidatedRanges() - && autocache_hash_tasks_.isEmpty() - && autocache_hash_process_tasks_.isEmpty() - && autocache_has_changed_ - && (!autocache_paused_ || use_custom_autocache_range_)) { - TimeRange using_range; - - if (use_custom_autocache_range_) { - using_range = custom_autocache_range_; - use_custom_autocache_range_ = false; - } else { - using_range = autocache_range_; - } - - QVector invalidated_ranges = viewer_node_->video_frame_cache()->GetInvalidatedFrames(using_range); - - ClearVideoQueue(); - - // QMaps are automatically sorted by time which is always best for rendering - QList queued_hashes; - - foreach (const rational& t, invalidated_ranges) { - const QByteArray& hash = viewer_node_->video_frame_cache()->GetHash(t); - - if (t >= using_range.in() - && t < using_range.out() - && !queued_hashes.contains(hash) - && !autocache_currently_caching_hashes_.contains(hash)) { - // Don't render any hash more than once - queued_hashes.append(hash); - - RenderTicketWatcher* watcher = new RenderTicketWatcher(); - connect(watcher, &RenderTicketWatcher::Finished, this, &RenderBackend::AutoCacheVideoRendered); - autocache_video_tasks_.insert(watcher, hash); - - watcher->SetTicket(RenderFrame(t, false, hash)); - } - } - - autocache_has_changed_ = false; - } -} - -OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/renderbackend.h b/app/render/backend/renderbackend.h deleted file mode 100644 index c7e29e317..000000000 --- a/app/render/backend/renderbackend.h +++ /dev/null @@ -1,259 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef RENDERBACKEND_H -#define RENDERBACKEND_H - -#include - -#include "config/config.h" -#include "dialog/rendercancel/rendercancel.h" -#include "decodercache.h" -#include "node/graph.h" -#include "node/output/viewer/viewer.h" -#include "render/backend/colorprocessorcache.h" -#include "renderticket.h" -#include "renderticketwatcher.h" -#include "renderworker.h" - -OLIVE_NAMESPACE_ENTER - -class RenderBackend : public QObject -{ - Q_OBJECT -public: - RenderBackend(QObject* parent = nullptr); - - virtual ~RenderBackend() override; - - void Close(); - - ViewerOutput* GetViewerNode() const - { - return viewer_node_; - } - - void SetViewerNode(ViewerOutput* viewer_node); - - void SetAutoCacheEnabled(bool e) - { - autocache_enabled_ = e; - } - - bool IsAutoCachePaused() const - { - return autocache_paused_; - } - - void SetAutoCachePaused(bool paused) - { - autocache_paused_ = paused; - - if (autocache_paused_) { - // Pause the autocache - ClearVideoQueue(); - } else { - // Unpause the cache - AutoCacheRequeueFrames(); - } - } - - void AutoCacheRange(const TimeRange& range); - - void AutoCacheRequeueFrames(); - - void SetAutoCachePlayhead(const rational& playhead) - { - autocache_range_ = TimeRange(playhead - Config::Current()["DiskCacheBehind"].value(), - playhead + Config::Current()["DiskCacheAhead"].value()); - - autocache_has_changed_ = true; - use_custom_autocache_range_ = false; - - AutoCacheRequeueFrames(); - } - - void SetRenderMode(RenderMode::Mode e) - { - render_mode_ = e; - } - - void SetPreviewGenerationEnabled(bool e) - { - generate_audio_previews_ = e; - } - - void ProcessUpdateQueue(); - - /** - * @brief Asynchronously generate a hash at a given time - */ - RenderTicketPtr Hash(const QVector ×, bool prioritize = false); - - /** - * @brief Asynchronously generate a frame at a given time - */ - RenderTicketPtr RenderFrame(const rational& time, bool prioritize = false, const QByteArray& hash = QByteArray()); - - /** - * @brief Asynchronously generate a chunk of audio - */ - RenderTicketPtr RenderAudio(const TimeRange& r, bool prioritize = false); - - const VideoParams& GetVideoParams() const - { - return video_params_; - } - - const AudioParams& GetAudioParams() const - { - return audio_params_; - } - - void SetVideoParams(const VideoParams& params); - - void SetAudioParams(const AudioParams& params); - - void SetForceDownloadResolution(bool e) - { - video_force_download_resolution_ = e; - } - - void SetVideoDownloadMatrix(const QMatrix4x4& mat) - { - video_download_matrix_ = mat; - } - - void IgnoreNextMouseButton(); - - static std::list SplitRangeIntoChunks(const TimeRange& r); - -public slots: - void NodeGraphChanged(NodeInput *source); - - void ClearVideoQueue(); - - void ClearAudioQueue(); - - void ClearQueue(); - -signals: - -protected: - virtual RenderWorker* CreateNewWorker() = 0; - -private: - void CopyNodeInputValue(NodeInput* input); - Node *CopyNodeConnections(Node *src_node); - void CopyNodeMakeConnection(NodeInput *src_input, NodeInput *dst_input); - - void ClearQueueOfType(RenderTicket::Type type); - - void SetHashes(FrameHashCache* cache, const QVector& times, const QVector& hashes, qint64 job_time); - - ViewerOutput* viewer_node_; - - // VIDEO MEMBERS - VideoParams video_params_; - bool video_force_download_resolution_; - QMatrix4x4 video_download_matrix_; - - // AUDIO MEMBERS - AudioParams audio_params_; - - QList graph_update_queue_; - QHash copy_map_; - ViewerOutput* copied_viewer_node_; - - std::list render_queue_; - - std::list running_tickets_; - - struct WorkerData { - RenderWorker* worker; - bool busy; - }; - - QVector workers_; - - bool autocache_enabled_; - bool autocache_paused_; - - bool generate_audio_previews_; - - RenderMode::Mode render_mode_; - - TimeRange autocache_range_; - - bool autocache_has_changed_; - - bool use_custom_autocache_range_; - TimeRange custom_autocache_range_; - - static QVector instances_; - static QMutex instance_lock_; - static RenderBackend* active_instance_; - static QThreadPool thread_pool_; - void SetActiveInstance(); - - QMap > autocache_hash_tasks_; - - QList*> autocache_hash_process_tasks_; - - QMap autocache_audio_tasks_; - - QMap autocache_video_tasks_; - - QMap*, QByteArray> autocache_video_download_tasks_; - - QVector autocache_currently_caching_hashes_; - - bool ignore_next_mouse_button_; - -private slots: - void WorkerFinished(); - - void RunNextJob(); - - void TicketFinished(); - - void WorkerGeneratedWaveform(OLIVE_NAMESPACE::RenderTicketPtr ticket, OLIVE_NAMESPACE::TrackOutput* track, OLIVE_NAMESPACE::AudioVisualWaveform samples, OLIVE_NAMESPACE::TimeRange range); - - void AutoCacheVideoInvalidated(const OLIVE_NAMESPACE::TimeRange &range); - - void AutoCacheAudioInvalidated(const OLIVE_NAMESPACE::TimeRange &range); - - void AutoCacheHashesGenerated(); - - void AutoCacheHashesProcessed(); - - void AutoCacheAudioRendered(); - - void AutoCacheVideoRendered(); - - void AutoCacheVideoDownloaded(); - - void QueuedInputRemoved(); - -}; - -OLIVE_NAMESPACE_EXIT - -#endif // RENDERBACKEND_H diff --git a/app/render/backend/rendercontext.cpp b/app/render/backend/rendercontext.cpp new file mode 100644 index 000000000..a9a4ce73e --- /dev/null +++ b/app/render/backend/rendercontext.cpp @@ -0,0 +1,6 @@ +#include "rendercontext.h" + +RenderContext::RenderContext() +{ + +} diff --git a/app/render/backend/rendercontext.h b/app/render/backend/rendercontext.h new file mode 100644 index 000000000..fc443507e --- /dev/null +++ b/app/render/backend/rendercontext.h @@ -0,0 +1,11 @@ +#ifndef RENDERCONTEXT_H +#define RENDERCONTEXT_H + + +class RenderContext +{ +public: + RenderContext(); +}; + +#endif // RENDERCONTEXT_H diff --git a/app/render/backend/renderframebuffer.cpp b/app/render/backend/renderframebuffer.cpp new file mode 100644 index 000000000..dd03c9d59 --- /dev/null +++ b/app/render/backend/renderframebuffer.cpp @@ -0,0 +1,6 @@ +#include "renderframebuffer.h" + +RenderFrameBuffer::RenderFrameBuffer() +{ + +} diff --git a/app/render/backend/renderframebuffer.h b/app/render/backend/renderframebuffer.h new file mode 100644 index 000000000..b4b928826 --- /dev/null +++ b/app/render/backend/renderframebuffer.h @@ -0,0 +1,11 @@ +#ifndef RENDERFRAMEBUFFER_H +#define RENDERFRAMEBUFFER_H + + +class RenderFrameBuffer +{ +public: + RenderFrameBuffer(); +}; + +#endif // RENDERFRAMEBUFFER_H diff --git a/app/render/backend/rendershader.cpp b/app/render/backend/rendershader.cpp new file mode 100644 index 000000000..a90f3a1e3 --- /dev/null +++ b/app/render/backend/rendershader.cpp @@ -0,0 +1,6 @@ +#include "rendershader.h" + +RenderShader::RenderShader() +{ + +} diff --git a/app/render/backend/rendershader.h b/app/render/backend/rendershader.h new file mode 100644 index 000000000..63f5f97de --- /dev/null +++ b/app/render/backend/rendershader.h @@ -0,0 +1,11 @@ +#ifndef RENDERSHADER_H +#define RENDERSHADER_H + + +class RenderShader +{ +public: + RenderShader(); +}; + +#endif // RENDERSHADER_H diff --git a/app/render/backend/rendertexture.cpp b/app/render/backend/rendertexture.cpp new file mode 100644 index 000000000..0ba906451 --- /dev/null +++ b/app/render/backend/rendertexture.cpp @@ -0,0 +1,6 @@ +#include "rendertexture.h" + +RenderTexture::RenderTexture(RenderContext *ctx) +{ + +} diff --git a/app/render/backend/rendertexture.h b/app/render/backend/rendertexture.h new file mode 100644 index 000000000..462ad26b9 --- /dev/null +++ b/app/render/backend/rendertexture.h @@ -0,0 +1,12 @@ +#ifndef RENDERTEXTURE_H +#define RENDERTEXTURE_H + +#include "rendercontext.h" + +class RenderTexture +{ +public: + RenderTexture(RenderContext* ctx); +}; + +#endif // RENDERTEXTURE_H diff --git a/app/render/backend/renderworker.cpp b/app/render/backend/renderworker.cpp deleted file mode 100644 index 7c3e2bddd..000000000 --- a/app/render/backend/renderworker.cpp +++ /dev/null @@ -1,471 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include "renderworker.h" - -#include -#include -#include - -#include "audio/audiovisualwaveform.h" -#include "common/functiontimer.h" -#include "config/config.h" -#include "node/block/clip/clip.h" -#include "task/conform/conform.h" - -OLIVE_NAMESPACE_ENTER - -// FIXME: Hardcoded value. It seems to work fine, but is there a possibility we should make -// this a dynamic value somehow or a configurable value? -const int RenderWorker::kMaxDecoderLife = 6000; - -RenderWorker::RenderWorker(RenderBackend* parent) : - parent_(parent), - video_force_download_resolution_(false), - available_(true), - generate_audio_previews_(false), - render_mode_(RenderMode::kOnline) -{ - cleanup_timer_ = new QTimer(); - cleanup_timer_->setInterval(kMaxDecoderLife); - connect(cleanup_timer_, &QTimer::timeout, this, &RenderWorker::ClearOldDecoders, Qt::DirectConnection); - cleanup_timer_->moveToThread(qApp->thread()); - QMetaObject::invokeMethod(cleanup_timer_, "start", Qt::QueuedConnection); -} - -RenderWorker::~RenderWorker() -{ - QMetaObject::invokeMethod(cleanup_timer_, "stop", Qt::QueuedConnection); - cleanup_timer_->deleteLater(); -} - -void RenderWorker::Hash(RenderTicketPtr ticket, ViewerOutput *viewer, const QVector ×) -{ - ticket_ = ticket; - - QVector hashes(times.size()); - - for (int i=0;itexture_input()->get_connected_node(), - video_params_, - times.at(i)); - } - - ticket->Finish(QVariant::fromValue(hashes)); - - emit FinishedJob(); -} - -QByteArray RenderWorker::HashNode(const Node *n, const VideoParams ¶ms, const rational &time) -{ - QCryptographicHash hasher(QCryptographicHash::Sha1); - - // Embed video parameters into this hash - hasher.addData(reinterpret_cast(¶ms.effective_width()), sizeof(int)); - hasher.addData(reinterpret_cast(¶ms.effective_height()), sizeof(int)); - hasher.addData(reinterpret_cast(¶ms.format()), sizeof(PixelFormat::Format)); - - if (n) { - n->Hash(hasher, time); - } - - return hasher.result(); -} - -void RenderWorker::ClearOldDecoders() -{ - QMutexLocker locker(&decoder_lock_); - - QHash::iterator i = decoder_age_.begin(); - - while (i != decoder_age_.end()) { - if (i.value() < QDateTime::currentMSecsSinceEpoch() - kMaxDecoderLife) { - // This decoder is old, remove it - decoder_cache_.remove(i.key()); - - i = decoder_age_.erase(i); - } else { - i++; - } - } -} - -void RenderWorker::RenderFrame(RenderTicketPtr ticket, ViewerOutput* viewer, const rational &time) -{ - ticket_ = ticket; - - NodeValueTable table = ProcessInput(viewer->texture_input(), - TimeRange(time, time + video_params_.time_base())); - - QVariant texture = table.Get(NodeParam::kTexture); - - PixelFormat::Format output_format; - if (!texture.isNull() && TextureHasAlpha(texture)) { - output_format = PixelFormat::GetFormatWithAlphaChannel(video_params_.format()); - } else { - output_format = PixelFormat::GetFormatWithoutAlphaChannel(video_params_.format()); - } - - FramePtr frame = Frame::Create(); - frame->set_timestamp(time); - - if (video_force_download_resolution_ || texture.isNull()) { - // If we're setting the resolution ourselves or we're zeroing it out, allocate the frame now - frame->set_video_params(VideoParams(video_params_.width(), - video_params_.height(), - video_params_.time_base(), - output_format, - video_params_.pixel_aspect_ratio(), - video_params_.interlacing(), - video_params_.divider())); - frame->allocate(); - } - - if (texture.isNull()) { - // Blank frame out - memset(frame->data(), 0, frame->allocated_size()); - } else { - // Dump texture contents to frame - TextureToFrame(texture, frame, video_download_matrix_); - } - - ticket->Finish(QVariant::fromValue(frame)); - - emit FinishedJob(); -} - -void RenderWorker::RenderAudio(RenderTicketPtr ticket, ViewerOutput* viewer, const TimeRange &range) -{ - ticket_ = ticket; - - NodeValueTable table = ProcessInput(viewer->samples_input(), range); - - QVariant samples = table.Get(NodeParam::kSamples); - - ticket->Finish(samples); - - emit FinishedJob(); -} - -void RenderWorker::ClearDecoders() -{ - decoder_cache_.clear(); -} - -NodeValueTable RenderWorker::GenerateBlockTable(const TrackOutput *track, const TimeRange &range) -{ - if (track->track_type() == Timeline::kTrackTypeAudio) { - - QList active_blocks = track->BlocksAtTimeRange(range); - - // All these blocks will need to output to a buffer so we create one here - SampleBufferPtr block_range_buffer = SampleBuffer::CreateAllocated(audio_params_, - audio_params_.time_to_samples(range.length())); - block_range_buffer->fill(0); - - NodeValueTable merged_table; - - // Loop through active blocks retrieving their audio - foreach (Block* b, active_blocks) { - TimeRange range_for_block(qMax(b->in(), range.in()), - qMin(b->out(), range.out())); - - int destination_offset = audio_params_.time_to_samples(range_for_block.in() - range.in()); - int max_dest_sz = audio_params_.time_to_samples(range_for_block.length()); - - // Destination buffer - NodeValueTable table = GenerateTable(b, range_for_block); - SampleBufferPtr samples_from_this_block = table.Take(NodeParam::kSamples).value(); - - if (!samples_from_this_block) { - // If we retrieved no samples from this block, do nothing - continue; - } - - // FIXME: Doesn't handle reversing - if (b->speed_input()->is_keyframing() || b->speed_input()->is_connected()) { - // FIXME: We'll need to calculate the speed hoo boy - } else { - double speed_value = b->speed_input()->get_standard_value().toDouble(); - - if (qIsNull(speed_value)) { - // Just silence, don't think there's any other practical application of 0 speed audio - samples_from_this_block->fill(0); - } else if (!qFuzzyCompare(speed_value, 1.0)) { - // Multiply time - samples_from_this_block->speed(speed_value); - } - } - - int copy_length = qMin(max_dest_sz, samples_from_this_block->sample_count()); - - // Copy samples into destination buffer - block_range_buffer->set(samples_from_this_block->const_data(), destination_offset, copy_length); - - NodeValueTable::Merge({merged_table, table}); - } - - if (generate_audio_previews_) { - // Find original track object - TrackOutput* original_track = nullptr; - - // Have to do a manual loop since our track is const and QHash won't take it - QHash::const_iterator i; - for (i=copy_map_->constBegin(); i!=copy_map_->constEnd(); i++) { - if (i.value() == track) { - original_track = static_cast(i.key()); - break; - } - } - - if (original_track) { - // Generate a visual waveform and send it back to the main thread - AudioVisualWaveform visual_waveform; - visual_waveform.set_channel_count(audio_params_.channel_count()); - visual_waveform.OverwriteSamples(block_range_buffer, audio_params_.sample_rate()); - - emit WaveformGenerated(ticket_, original_track, visual_waveform, range); - } - } - - merged_table.Push(NodeParam::kSamples, QVariant::fromValue(block_range_buffer), track); - - return merged_table; - - } else { - return NodeTraverser::GenerateBlockTable(track, range); - } -} - -QVariant RenderWorker::ProcessSamples(const Node *node, const TimeRange &range, const SampleJob& job) -{ - if (!job.samples() || !job.samples()->is_allocated()) { - return QVariant(); - } - - SampleBufferPtr output_buffer = SampleBuffer::CreateAllocated(job.samples()->audio_params(), job.samples()->sample_count()); - NodeValueDatabase value_db; - - for (int i=0;isample_count();i++) { - // Calculate the exact rational time at this sample - double sample_to_second = static_cast(i) / static_cast(audio_params_.sample_rate()); - - rational this_sample_time = rational::fromDouble(range.in().toDouble() + sample_to_second); - - // Update all non-sample and non-footage inputs - NodeValueMap::const_iterator j; - for (j=job.GetValues().constBegin(); j!=job.GetValues().constEnd(); j++) { - NodeValueTable value; - NodeInput* corresponding_input = node->GetInputWithID(j.key()); - - if (corresponding_input) { - value = ProcessInput(corresponding_input, TimeRange(this_sample_time, this_sample_time)); - } else { - value.Push(j.value()); - } - - value_db.Insert(j.key(), value); - } - - AddGlobalsToDatabase(value_db, TimeRange(this_sample_time, this_sample_time)); - - node->ProcessSamples(value_db, - job.samples(), - output_buffer, - i); - } - - return QVariant::fromValue(output_buffer); -} - -QVariant RenderWorker::ProcessFrameGeneration(const Node* node, const GenerateJob &job) -{ - FramePtr frame = Frame::Create(); - - PixelFormat::Format output_fmt; - if (job.GetAlphaChannelRequired()) { - output_fmt = PixelFormat::GetFormatWithAlphaChannel(video_params_.format()); - } else { - output_fmt = PixelFormat::GetFormatWithoutAlphaChannel(video_params_.format()); - } - - frame->set_video_params(VideoParams(video_params_.width(), - video_params_.height(), - video_params_.time_base(), - output_fmt, - video_params_.pixel_aspect_ratio(), - video_params_.interlacing(), - video_params_.divider())); - frame->allocate(); - - node->GenerateFrame(frame, job); - - return CachedFrameToTexture(frame); -} - -QVariant RenderWorker::GetCachedFrame(const Node* node, const rational& time) -{ - if (render_mode_ == RenderMode::kOffline - && !cache_path_.isEmpty() - && node->id() == QStringLiteral("org.olivevideoeditor.Olive.videoinput")) { - QByteArray hash = HashNode(node, video_params(), time); - - FramePtr f = FrameHashCache::LoadCacheFrame(cache_path_, hash); - - if (f) { - // The cached frame won't load with the correct divider by default, so we enforce it here - f->set_video_params(VideoParams(f->width() * video_params_.divider(), - f->height() * video_params_.divider(), - f->video_params().time_base(), - f->video_params().format(), - f->video_params().pixel_aspect_ratio(), - f->video_params().interlacing(), - video_params_.divider())); - - return CachedFrameToTexture(f); - } - } - - return QVariant(); -} - -DecoderPtr RenderWorker::ResolveDecoderFromInput(StreamPtr stream) -{ - // Access a map of Node inputs and decoder instances and retrieve a frame! - QMutexLocker locker(&decoder_lock_); - - DecoderPtr decoder = decoder_cache_.value(stream.get()); - - if (!decoder && stream) { - // Create a new Decoder here - decoder = Decoder::CreateFromID(stream->footage()->decoder()); - decoder->set_stream(stream); - - if (decoder->Open()) { - decoder_cache_.insert(stream.get(), decoder); - } else { - decoder = nullptr; - qWarning() << "Failed to open decoder for" << stream->footage()->filename() - << "::" << stream->index(); - } - } - - decoder_age_.insert(stream.get(), QDateTime::currentMSecsSinceEpoch()); - - return decoder; -} - -QVariant RenderWorker::ProcessVideoFootage(StreamPtr stream, const rational &input_time) -{ - VideoStreamPtr video_stream = std::static_pointer_cast(stream); - rational time_match = (video_stream->video_type() == VideoStream::kVideoTypeStill) ? 0 : input_time; - QString colorspace_match = video_stream->get_colorspace_match_string(); - - QVariant value; - bool found_cache = false; - - if (still_image_cache_.contains(stream.get())) { - const CachedStill& cs = still_image_cache_[stream.get()]; - - if (cs.colorspace == colorspace_match - && cs.alpha_is_associated == video_stream->premultiplied_alpha() - && cs.divider == video_params_.divider() - && cs.time == time_match) { - value = cs.texture; - found_cache = true; - } else { - still_image_cache_.remove(stream.get()); - } - } - - if (!found_cache) { - - DecoderPtr decoder = ResolveDecoderFromInput(stream); - - if (decoder) { - FramePtr frame = decoder->RetrieveVideo(input_time, - video_params().divider()); - - if (frame) { - // Return a texture from the derived class - value = FootageFrameToTexture(stream, frame); - - if (!value.isNull()) { - // Put this into the image cache instead - still_image_cache_.insert(stream.get(), {value, - colorspace_match, - video_stream->premultiplied_alpha(), - video_params_.divider(), - time_match}); - } - } - } - - } - - return value; -} - -QVariant RenderWorker::ProcessAudioFootage(StreamPtr stream, const TimeRange &input_time) -{ - QVariant value; - - DecoderPtr decoder = ResolveDecoderFromInput(stream); - - if (decoder) { - // See if we have a conformed version of this audio - if (!decoder->HasConformedVersion(audio_params())) { - - // If not, the audio needs to be conformed - // For online rendering/export, it's a waste of time to render the audio until we have - // all we need, so we try to handle the conform ourselves - AudioStreamPtr as = std::static_pointer_cast(stream); - - // Check if any other threads are conforming this audio - if (as->try_start_conforming(audio_params())) { - - // If not, conform it ourselves - decoder->ConformAudio(&IsCancelled(), audio_params()); - - } else { - - // If another thread is conforming already, hackily try to wait until it's done. - do { - QThread::msleep(1000); - } while (!as->has_conformed_version(audio_params()) && !IsCancelled()); - - } - - } - - if (decoder->HasConformedVersion(audio_params())) { - SampleBufferPtr frame = decoder->RetrieveAudio(input_time.in(), input_time.length(), - audio_params()); - - if (frame) { - value = QVariant::fromValue(frame); - } - } - } - - return value; -} - -OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/renderworker.h b/app/render/backend/renderworker.h deleted file mode 100644 index 84f9fbe94..000000000 --- a/app/render/backend/renderworker.h +++ /dev/null @@ -1,208 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef RENDERWORKER_H -#define RENDERWORKER_H - -#include - -#include "decodercache.h" -#include "node/traverser.h" -#include "node/output/viewer/viewer.h" -#include "renderticket.h" - -OLIVE_NAMESPACE_ENTER - -class RenderBackend; - -class RenderWorker : public QObject, public NodeTraverser -{ - Q_OBJECT -public: - RenderWorker(RenderBackend* parent); - - virtual ~RenderWorker() override; - - bool IsAvailable() const - { - return available_; - } - - void SetAvailable(bool a) - { - available_ = a; - } - - void SetVideoParams(const VideoParams& params) - { - video_params_ = params; - } - - void SetAudioParams(const AudioParams& params) - { - audio_params_ = params; - } - - void SetForceDownloadResolution(bool e) - { - video_force_download_resolution_ = e; - } - - void SetVideoDownloadMatrix(const QMatrix4x4& mat) - { - video_download_matrix_ = mat; - } - - void SetCopyMap(QHash* copy_map) - { - copy_map_ = copy_map; - } - - void SetRenderMode(const RenderMode::Mode& mode) - { - render_mode_ = mode; - } - - void SetPreviewGenerationEnabled(bool e) - { - generate_audio_previews_ = e; - } - - void SetCachePath(const QString& s) - { - cache_path_ = s; - } - - void Hash(RenderTicketPtr ticket, ViewerOutput* viewer, const QVector& times); - - /** - * @brief Render the frame at this time - * - * Produces a fully rendered frame from the connected viewer at this time. - * - * @return - * - * A frame corresponding to the set video parameters. If no nodes are active at the time, this - * function will still return a blank frame with the same parameters. If no viewer node is set, - * nullptr is returned. - */ - void RenderFrame(RenderTicketPtr ticket, ViewerOutput* viewer, const rational &time); - - void RenderAudio(RenderTicketPtr ticket, ViewerOutput* viewer, const TimeRange& range); - - void ClearDecoders(); - -protected: - virtual void TextureToFrame(const QVariant& texture, FramePtr frame, const QMatrix4x4 &mat) const = 0; - - virtual QVariant FootageFrameToTexture(StreamPtr stream, FramePtr frame) const = 0; - - virtual QVariant CachedFrameToTexture(FramePtr frame) const = 0; - - virtual NodeValueTable GenerateBlockTable(const TrackOutput *track, const TimeRange &range) override; - - virtual QVariant ProcessVideoFootage(StreamPtr stream, const rational &input_time) override; - - virtual QVariant ProcessAudioFootage(StreamPtr stream, const TimeRange &input_time) override; - - virtual QVariant ProcessSamples(const Node *node, const TimeRange &range, const SampleJob &job) override; - - virtual QVariant ProcessFrameGeneration(const Node *node, const GenerateJob& job) override; - - virtual QVariant GetCachedFrame(const Node *node, const rational &time) override; - - virtual bool TextureHasAlpha(const QVariant& v) const = 0; - - const VideoParams& video_params() const - { - return video_params_; - } - - const AudioParams& audio_params() const - { - return audio_params_; - } - - const RenderMode::Mode& render_mode() const - { - return render_mode_; - } - -signals: - void AudioConformUnavailable(StreamPtr stream, TimeRange range, - rational stream_time, AudioParams params); - - void FinishedJob(); - - void WaveformGenerated(OLIVE_NAMESPACE::RenderTicketPtr ticket, OLIVE_NAMESPACE::TrackOutput* track, OLIVE_NAMESPACE::AudioVisualWaveform samples, OLIVE_NAMESPACE::TimeRange range); - -private: - DecoderPtr ResolveDecoderFromInput(StreamPtr stream); - - static QByteArray HashNode(const Node* n, const VideoParams& params, const rational& time); - - RenderBackend* parent_; - - RenderTicketPtr ticket_; - - VideoParams video_params_; - - AudioParams audio_params_; - - struct CachedStill { - QVariant texture; - QString colorspace; - bool alpha_is_associated; - int divider; - rational time; - }; - - QHash still_image_cache_; - - bool video_force_download_resolution_; - QMatrix4x4 video_download_matrix_; - - QMutex decoder_lock_; - DecoderCache decoder_cache_; - QHash decoder_age_; - - TimeRange audio_render_time_; - bool available_; - - bool generate_audio_previews_; - - QHash* copy_map_; - - RenderMode::Mode render_mode_; - - QTimer* cleanup_timer_; - - QString cache_path_; - - static const int kMaxDecoderLife; - -private slots: - void ClearOldDecoders(); - -}; - -OLIVE_NAMESPACE_EXIT - -#endif // RENDERWORKER_H diff --git a/app/render/backend/colorprocessorcache.h b/app/render/colorprocessorcache.h similarity index 100% rename from app/render/backend/colorprocessorcache.h rename to app/render/colorprocessorcache.h diff --git a/app/render/backend/decodercache.h b/app/render/decodercache.h similarity index 100% rename from app/render/backend/decodercache.h rename to app/render/decodercache.h diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp new file mode 100644 index 000000000..8658ba48a --- /dev/null +++ b/app/render/previewautocacher.cpp @@ -0,0 +1,695 @@ +#include "previewautocacher.h" + +#include +#include + +#include "render/rendermanager.h" + +OLIVE_NAMESPACE_ENTER + +PreviewAutoCacher::PreviewAutoCacher() : + viewer_node_(nullptr), + paused_(false), + has_changed_(false), + use_custom_range_(false), + last_update_time_(0), + ignore_next_mouse_button_(false), + video_params_changed_(false), + audio_params_changed_(false) +{ + // Set default autocache range + SetPlayhead(rational()); +} + +RenderTicketPtr PreviewAutoCacher::GetSingleFrame(const rational &t) +{ + RenderTicketPtr ticket = std::make_shared(); + + ticket->setProperty("time", QVariant::fromValue(t)); + + TryRender(); + + return ticket; +} + +void PreviewAutoCacher::SetPaused(bool paused) +{ + paused_ = paused; + + if (paused_) { + // Pause the autocache + ClearVideoQueue(); + } else { + // Unpause the cache + RequeueFrames(); + } +} + +void PreviewAutoCacher::NodeGraphChanged(NodeInput *source) +{ + // We need to determine: + // - If we don't have this input, assume that it's coming soon and ignore it + // - If we do, is this input a child of another input we're already copying? + // - Or are any of the queued inputs children of this one? + + // First we need to find our copy of the input being queued + Node* our_copy_node = copy_map_.value(source->parentNode()); + + // If we don't have this node yet, assume it's coming in a later copy in which case it'll be + // copied then + if (!our_copy_node) { + // Assert that there are updates coming + Q_ASSERT(!graph_update_queue_.isEmpty()); + return; + } + + // If we're here, we must have this node. Determine if we're already copying a "parent" of this + for (int i=0; iIsArray() && static_cast(source)->sub_params().contains(queued_input)) + || queued_input->parentNode()->OutputsTo(source, true, true)) { + // In which case, we don't need to queue it and can queue our own + graph_update_queue_.removeAt(i); + disconnect(queued_input, &NodeInput::destroyed, this, &PreviewAutoCacher::QueuedInputRemoved); + i--; + } + + // Check if the source is a member of this array, in which case it'll be copied eventually anyway + if (queued_input->IsArray() + && static_cast(queued_input)->sub_params().contains(source)) { + return; + } + + // Check if this dependency graph is already queued + if (source->parentNode()->OutputsTo(queued_input, true, true)) { + // In which case, no further copy is necessary + return; + } + } + + graph_update_queue_.append(source); + connect(source, &NodeInput::destroyed, this, &PreviewAutoCacher::QueuedInputRemoved); +} + +void PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, const QVector ×, qint64 job_time) +{ + std::vector existing_hashes; + + foreach (const rational& time, times) { + // See if hash already exists in disk cache + QByteArray hash = RenderManager::Hash(viewer, viewer->video_params(), time); + + // Check memory list since disk checking is slow + bool hash_exists = (std::find(existing_hashes.begin(), existing_hashes.end(), hash) != existing_hashes.end()); + + if (!hash_exists) { + hash_exists = QFileInfo::exists(viewer->video_frame_cache()->CachePathName(hash)); + + if (hash_exists) { + existing_hashes.push_back(hash); + } + } + + // Set hash in FrameHashCache's thread rather than in ours to prevent race conditions + QMetaObject::invokeMethod(viewer->video_frame_cache(), "SetHash", Qt::QueuedConnection, + OLIVE_NS_ARG(rational, time), + Q_ARG(QByteArray, hash), + Q_ARG(qint64, job_time), + Q_ARG(bool, hash_exists)); + } +} + +void PreviewAutoCacher::VideoInvalidated(const TimeRange &range) +{ + qDebug() << "Video invalidated"; + + ClearQueue(false); + + // Hash these frames since that should be relatively quick. + if (ignore_next_mouse_button_ || !(qApp->mouseButtons() & Qt::LeftButton)) { + ignore_next_mouse_button_ = false; + + invalidated_video_.InsertTimeRange(range); + + TryRender(); + } +} + +void PreviewAutoCacher::AudioInvalidated(const TimeRange &range) +{ + ClearQueue(false); + + // Start jobs to re-render the audio at this range, split into 2 second chunks + invalidated_audio_.InsertTimeRange(range); + + TryRender(); +} + +void PreviewAutoCacher::HashesProcessed() +{ + QFutureWatcher* watcher = static_cast*>(sender()); + + if (hash_tasks_.contains(watcher)) { + hash_tasks_.removeOne(watcher); + + RequeueFrames(); + } + + delete watcher; +} + +void PreviewAutoCacher::AudioRendered() +{ + RenderTicketWatcher* watcher = static_cast(sender()); + + if (audio_tasks_.contains(watcher)) { + if (!watcher->WasCancelled()) { + viewer_node_->audio_playback_cache()->WritePCM(audio_tasks_.value(watcher), + watcher->Get().value(), + watcher->GetTicket()->GetJobTime()); + } + + audio_tasks_.remove(watcher); + } + + delete watcher; +} + +void PreviewAutoCacher::VideoRendered() +{ + RenderTicketWatcher* watcher = static_cast(sender()); + + if (video_tasks_.contains(watcher)) { + if (watcher->WasCancelled()) { + // We didn't get this hash + currently_caching_hashes_.removeOne(watcher->property("hash").toByteArray()); + } else { + const QByteArray& hash = video_tasks_.value(watcher); + + // Download frame in another thread + QFutureWatcher* w = new QFutureWatcher(); + video_download_tasks_.insert(w, hash); + connect(w, &QFutureWatcher::finished, this, &PreviewAutoCacher::VideoDownloaded); + w->setFuture(QtConcurrent::run(viewer_node_->video_frame_cache(), + &FrameHashCache::SaveCacheFrame, + hash, + watcher->Get().value())); + } + + video_tasks_.remove(watcher); + } + + delete watcher; +} + +void PreviewAutoCacher::VideoDownloaded() +{ + QFutureWatcher* watcher = static_cast*>(sender()); + + if (video_download_tasks_.contains(watcher)) { + if (!watcher->isCanceled()) { + if (watcher->result()) { + const QByteArray& hash = video_download_tasks_.value(watcher); + + currently_caching_hashes_.removeOne(hash); + + viewer_node_->video_frame_cache()->ValidateFramesWithHash(hash); + } else { + qCritical() << "Failed to download video frame"; + } + } + + video_download_tasks_.remove(watcher); + } + + delete watcher; +} + +void PreviewAutoCacher::QueuedInputRemoved() +{ + NodeInput* i = static_cast(sender()); + disconnect(i, &NodeInput::destroyed, this, &PreviewAutoCacher::QueuedInputRemoved); + graph_update_queue_.removeOne(i); +} + +void PreviewAutoCacher::VideoParamsChanged() +{ + // In case the user is pressing the mouse at this exact moment + IgnoreNextMouseButton(); + + ClearVideoQueue(); + video_params_changed_ = true; + TryRender(); +} + +void PreviewAutoCacher::AudioParamsChanged() +{ + ClearAudioQueue(); + audio_params_changed_ = true; + TryRender(); +} + +//#define PRINT_UPDATE_QUEUE_INFO +void PreviewAutoCacher::ProcessUpdateQueue() +{ +#ifdef PRINT_UPDATE_QUEUE_INFO + qint64 t = QDateTime::currentMSecsSinceEpoch(); + qDebug() << "Processing update queue of" << graph_update_queue_.size() << "elements:"; +#endif + + while (!graph_update_queue_.isEmpty()) { + NodeInput* i = graph_update_queue_.takeFirst(); +#ifdef PRINT_UPDATE_QUEUE_INFO + qDebug() << " " << i->parentNode()->id() << i->id(); +#endif + disconnect(i, &NodeInput::destroyed, this, &PreviewAutoCacher::QueuedInputRemoved); + + CopyNodeInputValue(i); + } + +#ifdef PRINT_UPDATE_QUEUE_INFO + qDebug() << "Update queue took:" << (QDateTime::currentMSecsSinceEpoch() - t); +#endif +} + +bool PreviewAutoCacher::HasActiveJobs() const +{ + return !hash_tasks_.isEmpty() + || !audio_tasks_.isEmpty() + || !video_tasks_.isEmpty() + || !video_download_tasks_.isEmpty(); +} + +void PreviewAutoCacher::SetPlayhead(const rational &playhead) +{ + cache_range_ = TimeRange(playhead - Config::Current()["DiskCacheBehind"].value(), + playhead + Config::Current()["DiskCacheAhead"].value()); + + has_changed_ = true; + use_custom_range_ = false; + + RequeueFrames(); +} + +void PreviewAutoCacher::ClearQueue(bool wait) +{ + ClearHashQueue(wait); + ClearVideoQueue(wait); + ClearAudioQueue(wait); +} + +void PreviewAutoCacher::ClearHashQueue(bool wait) +{ + auto copy = hash_tasks_; + + for (auto it=copy.cbegin(); it!=copy.cend(); it++) { + (*it)->cancel(); + } + if (wait) { + for (auto it=copy.cbegin(); it!=copy.cend(); it++) { + (*it)->waitForFinished(); + } + } +} + +void PreviewAutoCacher::ClearVideoQueue(bool wait) +{ + // Copy because tasks that cancel immediately will be automatically removed from the list + auto copy = video_tasks_; + + for (auto it=copy.cbegin(); it!=copy.cend(); it++) { + it.key()->Cancel(); + } + if (wait) { + for (auto it=copy.cbegin(); it!=copy.cend(); it++) { + it.key()->WaitForFinished(); + } + } + + has_changed_ = true; + use_custom_range_ = false; +} + +void PreviewAutoCacher::ClearAudioQueue(bool wait) +{ + // Create a copy because otherwise + auto copy = audio_tasks_; + + for (auto it=copy.cbegin(); it!=copy.cend(); it++) { + it.key()->Cancel(); + } + if (wait) { + for (auto it=copy.cbegin(); it!=copy.cend(); it++) { + it.key()->WaitForFinished(); + } + } +} + +void PreviewAutoCacher::CopyNodeInputValue(NodeInput *input) +{ + // Find our copy of this parameter + Node* our_copy_node = copy_map_.value(input->parentNode()); + Q_ASSERT(our_copy_node); + NodeInput* our_copy = our_copy_node->GetInputWithID(input->id()); + + // Copy the standard/keyframe values between these two inputs + NodeInput::CopyValues(input, + our_copy, + false, + false); + + // Handle connections + if (input->is_connected() || our_copy->is_connected()) { + // If one of the inputs is connected, it's likely this change came from connecting or + // disconnecting whatever was connected to it + + // We start by removing all old dependencies from the map + QList old_deps = our_copy->GetExclusiveDependencies(); + foreach (Node* i, old_deps) { + copy_map_.take(copy_map_.key(i))->deleteLater(); + } + + // And clear any other edges + while (!our_copy->edges().isEmpty()) { + NodeParam::DisconnectEdge(our_copy->edges().first()); + } + + // Then we copy all node dependencies and connections (if there are any) + CopyNodeMakeConnection(input, our_copy); + } + + // Call on sub-elements too + if (input->IsArray()) { + foreach (NodeInput* i, static_cast(input)->sub_params()) { + CopyNodeInputValue(i); + } + } +} + +Node* PreviewAutoCacher::CopyNodeConnections(Node* src_node) +{ + // Check if this node is already in the map + Node* dst_node = copy_map_.value(src_node); + + // If not, create it now + if (!dst_node) { + dst_node = src_node->copy(); + + if (dst_node->IsTrack()) { + // Hack that ensures the track type is set since we don't bother copying the whole timeline + static_cast(dst_node)->set_track_type(static_cast(src_node)->track_type()); + } + + copy_map_.insert(src_node, dst_node); + } + + // Make sure its values are copied + Node::CopyInputs(src_node, dst_node, false); + + // Copy all connections + QList src_node_inputs = src_node->GetInputsIncludingArrays(); + QList dst_node_inputs = dst_node->GetInputsIncludingArrays(); + + for (int i=0;iis_connected()) { + Node* dst_node = CopyNodeConnections(src_input->get_connected_node()); + + NodeOutput* corresponding_output = dst_node->GetOutputWithID(src_input->get_connected_output()->id()); + + NodeParam::ConnectEdge(corresponding_output, + dst_input); + } +} + +void PreviewAutoCacher::TryRender() +{ + if (!graph_update_queue_.isEmpty()) { + if (HasActiveJobs()) { + // Still waiting for jobs to finish + return; + } + + // No jobs are active, we can process the update queue + last_update_time_ = QDateTime::currentMSecsSinceEpoch(); + + ProcessUpdateQueue(); + + if (video_params_changed_) { + copied_viewer_node_->set_video_params(viewer_node_->video_params()); + video_params_changed_ = false; + } + + if (audio_params_changed_) { + copied_viewer_node_->set_audio_params(viewer_node_->audio_params()); + audio_params_changed_ = false; + } + } + + // If we're here, we must be able to render + if (!invalidated_video_.isEmpty()) { + QVector frames = viewer_node_->video_frame_cache()->GetFrameListFromTimeRange(invalidated_video_); + + QFutureWatcher* watcher = new QFutureWatcher(); + hash_tasks_.append(watcher); + connect(watcher, &QFutureWatcher::finished, this, &PreviewAutoCacher::HashesProcessed); + watcher->setFuture(QtConcurrent::run(&PreviewAutoCacher::GenerateHashes, + copied_viewer_node_, + frames, + last_update_time_)); + + invalidated_video_.clear(); + } + + if (!invalidated_audio_.isEmpty()) { + foreach (const TimeRange& range, invalidated_audio_) { + std::list chunks = range.Split(2); + + foreach (const TimeRange& r, chunks) { + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); + audio_tasks_.insert(watcher, r); + watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, true)); + } + } + + invalidated_audio_.clear(); + } + + if (!single_frame_renders_.isEmpty()) { + foreach (RenderTicketPtr ticket, single_frame_renders_) { + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + + watcher->setProperty("passthrough", QVariant::fromValue(ticket)); + + connect(watcher, &RenderTicketWatcher::Finished, watcher, [watcher]{ + RenderTicketPtr passthrough = watcher->property("passthrough").value(); + passthrough->Finish(watcher->GetTicket()->Get(), watcher->GetTicket()->WasCancelled()); + watcher->deleteLater(); + }); + + watcher->SetTicket(RenderManager::instance()->RenderFrame(copied_viewer_node_, + ticket->property("time").value(), + RenderMode::kOffline, true)); + } + single_frame_renders_.clear(); + } +} + +void PreviewAutoCacher::RequeueFrames() +{ + if (viewer_node_ + && viewer_node_->video_frame_cache()->HasInvalidatedRanges() + && hash_tasks_.isEmpty() + && has_changed_ + && (!paused_ || use_custom_range_)) { + TimeRange using_range; + + if (use_custom_range_) { + using_range = custom_autocache_range_; + use_custom_range_ = false; + } else { + using_range = cache_range_; + } + + QVector invalidated_ranges = viewer_node_->video_frame_cache()->GetInvalidatedFrames(using_range); + + ClearVideoQueue(); + + foreach (const rational& t, invalidated_ranges) { + const QByteArray& hash = viewer_node_->video_frame_cache()->GetHash(t); + + if (t >= using_range.in() + && t < using_range.out() + && !currently_caching_hashes_.contains(hash)) { + // Don't render any hash more than once + currently_caching_hashes_.append(hash); + + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + watcher->setProperty("hash", hash); + connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::VideoRendered); + video_tasks_.insert(watcher, hash); + watcher->SetTicket(RenderManager::instance()->RenderFrame(copied_viewer_node_, t, RenderMode::kOffline, false)); + } + } + + has_changed_ = false; + } +} + +void PreviewAutoCacher::IgnoreNextMouseButton() +{ + ignore_next_mouse_button_ = true; +} + +void PreviewAutoCacher::ForceCacheRange(const TimeRange &range) +{ + has_changed_ = true; + use_custom_range_ = true; + custom_autocache_range_ = range; + + RequeueFrames(); +} + +void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) +{ + if (viewer_node_ == viewer_node) { + return; + } + + if (viewer_node_) { + // Cancel any remaining tickets and wait for them to finish + ClearQueue(true); + + // Clear autocache lists + { + // We need to wait for these since they work directly on the FrameHashCache. Most of the time + // this is fine, but not if the FrameHashCache gets deleted after this function. + ClearHashQueue(true); + + // This can be cleared normally (frames will be discarded and need to be rendered again) + ClearVideoQueue(false); + + // This can be cleared normally (PCM data will be discarded and need to be rendered again) + ClearAudioQueue(false); + + // We'll need to wait for these since they work directly on the FrameHashCache. Frames will + // be in the cache for later use. + { + QMap*, QByteArray>::const_iterator i; + for (i=video_download_tasks_.constBegin(); i!=video_download_tasks_.constEnd(); i++) { + i.key()->waitForFinished(); + } + video_download_tasks_.clear(); + } + + // No longer caching any hashes + currently_caching_hashes_.clear(); + } + + // Delete all of our copied nodes + foreach (Node* c, copy_map_) { + delete c; + } + copy_map_.clear(); + copied_viewer_node_ = nullptr; + graph_update_queue_.clear(); + + video_params_changed_ = false; + audio_params_changed_ = false; + + // Disconnect signal (will be a no-op if the signal was never connected) + disconnect(viewer_node_, + &ViewerOutput::GraphChangedFrom, + this, + &PreviewAutoCacher::NodeGraphChanged); + + disconnect(viewer_node_, + &ViewerOutput::VideoParamsChanged, + this, + &PreviewAutoCacher::VideoParamsChanged); + + disconnect(viewer_node_, + &ViewerOutput::AudioParamsChanged, + this, + &PreviewAutoCacher::AudioParamsChanged); + + disconnect(viewer_node_->video_frame_cache(), + &PlaybackCache::Invalidated, + this, + &PreviewAutoCacher::VideoInvalidated); + + disconnect(viewer_node_->audio_playback_cache(), + &PlaybackCache::Invalidated, + this, + &PreviewAutoCacher::AudioInvalidated); + } + + viewer_node_ = viewer_node; + + if (viewer_node_) { + // Copy graph + copied_viewer_node_ = static_cast(viewer_node_->copy()); + copy_map_.insert(viewer_node_, copied_viewer_node_); + + // Copy parameters + copied_viewer_node_->set_video_params(viewer_node_->video_params()); + copied_viewer_node_->set_audio_params(viewer_node_->audio_params()); + + // We begin an operation and never end it which prevents the copy from unnecessarily + // invalidating its own cache + copied_viewer_node_->BeginOperation(); + + NodeGraphChanged(viewer_node_->texture_input()); + NodeGraphChanged(viewer_node_->samples_input()); + ProcessUpdateQueue(); + + invalidated_video_ = viewer_node_->video_frame_cache()->GetInvalidatedRanges(); + invalidated_audio_ = viewer_node_->audio_playback_cache()->GetInvalidatedRanges(); + + connect(viewer_node_, + &ViewerOutput::GraphChangedFrom, + this, + &PreviewAutoCacher::NodeGraphChanged); + + connect(viewer_node_, + &ViewerOutput::VideoParamsChanged, + this, + &PreviewAutoCacher::VideoParamsChanged); + + connect(viewer_node_, + &ViewerOutput::AudioParamsChanged, + this, + &PreviewAutoCacher::AudioParamsChanged); + + connect(viewer_node_->video_frame_cache(), + &PlaybackCache::Invalidated, + this, + &PreviewAutoCacher::VideoInvalidated); + + connect(viewer_node_->audio_playback_cache(), + &PlaybackCache::Invalidated, + this, + &PreviewAutoCacher::AudioInvalidated); + + TryRender(); + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h new file mode 100644 index 000000000..bd897bc69 --- /dev/null +++ b/app/render/previewautocacher.h @@ -0,0 +1,198 @@ +#ifndef AUTOCACHER_H +#define AUTOCACHER_H + +#include + +#include "config/config.h" +#include "node/node.h" +#include "node/output/viewer/viewer.h" +#include "threading/threadticketwatcher.h" + +OLIVE_NAMESPACE_ENTER + +/** + * @brief Manager for dynamically caching a sequence in the background + * + * Intended to be used with a Viewer to dynamically cache parts of a sequence based on the playhead. + */ +class PreviewAutoCacher : public QObject +{ + Q_OBJECT +public: + PreviewAutoCacher(); + + RenderTicketPtr GetSingleFrame(const rational& t); + + /** + * @brief Set the viewer node to auto-cache + */ + void SetViewerNode(ViewerOutput *viewer_node); + + /** + * @brief If the mouse is held during the next cache invalidation, cache anyway + * + * By default, PreviewAutoCacher ignores invalidations that occur while the mouse is held down, + * assuming that if the mouse is held, the user is dragging something. If you know the mouse will + * be held during a certain action and want PreviewAutoCacher to cache anyway, call this before + * the cache invalidates. + */ + void IgnoreNextMouseButton(); + + /** + * @brief Returns whether the auto-cache is currently paused or not + */ + bool IsPaused() const + { + return paused_; + } + + /** + * @brief Sets whether the auto-cache is currently paused or not + * @param paused + * + * If TRUE, the cache queue is cleared (any frames currently being rendered will be processed as + * normal however). If FALSE, any uncached frames in the range will automatically be queued. + */ + void SetPaused(bool paused); + + /** + * @brief Force a certain range to be cached + * + * Usually, PreviewAutoCacher caches a user-defined range around the playhead, however there are + * times they may want certain non-playhead-related time ranges to be cached (i.e. entire sequence + * or in/out range), so that can be set here. + */ + void ForceCacheRange(const TimeRange& range); + + /** + * @brief Updates the range of frames to auto-cache + */ + void SetPlayhead(const rational& playhead); + + /** + * @brief Clears queue of running jobs + * + * Any jobs that haven't run yet are cancelled and will never run. Any jobs that are currently + * running are cancelled, but may not be finished by the time this function returns. If the + * jobs must be finished by the time this function returns, set `wait` to TRUE. + */ + void ClearQueue(bool wait = false); + + void ClearHashQueue(bool wait = false); + void ClearVideoQueue(bool wait = false); + void ClearAudioQueue(bool wait = false); + +public slots: + /** + * @brief Main handler for when the NodeGraph changes + */ + void NodeGraphChanged(NodeInput *source); + +private: + static void GenerateHashes(ViewerOutput* viewer, const QVector& times, qint64 job_time); + + void CopyNodeInputValue(NodeInput* input); + Node *CopyNodeConnections(Node *src_node); + void CopyNodeMakeConnection(NodeInput *src_input, NodeInput *dst_input); + + void TryRender(); + + /** + * @brief Generic function called whenever the frames to render need to be (re)queued + */ + void RequeueFrames(); + + /** + * @brief Process all changes to internal NodeGraph copy + * + * PreviewAutoCacher staggers updates to its internal NodeGraph copy, only applying them when the + * RenderManager is not reading from it. This function is called when such an opportunity arises. + */ + void ProcessUpdateQueue(); + + bool HasActiveJobs() const; + + QList graph_update_queue_; + QHash copy_map_; + ViewerOutput* copied_viewer_node_; + + ViewerOutput* viewer_node_; + + bool paused_; + + TimeRange cache_range_; + + bool has_changed_; + + bool use_custom_range_; + TimeRange custom_autocache_range_; + + TimeRangeList invalidated_video_; + TimeRangeList invalidated_audio_; + + QVector single_frame_renders_; + + QList*> hash_tasks_; + QMap audio_tasks_; + QMap video_tasks_; + QMap*, QByteArray> video_download_tasks_; + + QVector currently_caching_hashes_; + + qint64 last_update_time_; + + bool ignore_next_mouse_button_; + + bool video_params_changed_; + + bool audio_params_changed_; + +private slots: + /** + * @brief Handler for when the NodeGraph reports a video change over a certain time range + */ + void VideoInvalidated(const OLIVE_NAMESPACE::TimeRange &range); + + /** + * @brief Handler for when the NodeGraph reports a audio change over a certain time range + */ + void AudioInvalidated(const OLIVE_NAMESPACE::TimeRange &range); + + /** + * @brief Handler for when we have applied all the hashes to the FrameHashCache + */ + void HashesProcessed(); + + /** + * @brief Handler for when the RenderManager has returned rendered audio + */ + void AudioRendered(); + + /** + * @brief Handler for when the RenderManager has returned rendered video frames + */ + void VideoRendered(); + + /** + * @brief Handler for when we've saved a video frame to the cache + */ + void VideoDownloaded(); + + /** + * @brief Handler for when a NodeInput has been deleted so we clear it from the queue + * + * FIXME: This is hacky. It also might not be necessary anymore with recent changes to the + * node system, but I haven't tested yet. Either way, PreviewAutoCacher should probably + * be able to pick up on these sorts of things without such a slot. + */ + void QueuedInputRemoved(); + + void VideoParamsChanged(); + + void AudioParamsChanged(); + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // AUTOCACHER_H diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp new file mode 100644 index 000000000..ac8c3881d --- /dev/null +++ b/app/render/rendermanager.cpp @@ -0,0 +1,169 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "rendermanager.h" + +#include +#include +#include + +#include "config/config.h" +#include "core.h" +#include "render/backend/opengl/openglproxy.h" +#include "task/conform/conform.h" +#include "task/taskmanager.h" +#include "window/mainwindow/mainwindow.h" + +OLIVE_NAMESPACE_ENTER + +RenderManager* RenderManager::instance_ = nullptr; + +RenderManager::RenderManager(QObject *parent) : + ThreadPool(QThread::IdlePriority, 0, parent) +{ + // Initialize OpenGL service + OpenGLProxy::CreateInstance(); +} + +RenderManager::~RenderManager() +{ + OpenGLProxy::DestroyInstance(); +} + +QByteArray RenderManager::Hash(const Node *n, const VideoParams ¶ms, const rational &time) +{ + QCryptographicHash hasher(QCryptographicHash::Sha1); + + // Embed video parameters into this hash + hasher.addData(reinterpret_cast(¶ms.effective_width()), sizeof(int)); + hasher.addData(reinterpret_cast(¶ms.effective_height()), sizeof(int)); + hasher.addData(reinterpret_cast(¶ms.format()), sizeof(PixelFormat::Format)); + + if (n) { + n->Hash(hasher, time); + } + + return hasher.result(); +} + +RenderTicketPtr RenderManager::RenderFrame(ViewerOutput* viewer, const rational &time, RenderMode::Mode mode, bool prioritize) +{ + // Create ticket + RenderTicketPtr ticket = std::make_shared(); + + ticket->setProperty("viewer", Node::PtrToValue(viewer)); + ticket->setProperty("time", QVariant::fromValue(time)); + ticket->setProperty("mode", mode); + ticket->setProperty("type", kTypeVideo); + + // Queue appending the ticket and running the next job on our thread to make this function thread-safe + QMetaObject::invokeMethod(this, "AddTicket", Qt::AutoConnection, + OLIVE_NS_ARG(RenderTicketPtr, ticket), + Q_ARG(bool, prioritize)); + + return ticket; +} + +RenderTicketPtr RenderManager::RenderAudio(ViewerOutput* viewer, const TimeRange &r, bool prioritize) +{ + // Create ticket + RenderTicketPtr ticket = std::make_shared(); + + ticket->setProperty("viewer", Node::PtrToValue(viewer)); + ticket->setProperty("time", QVariant::fromValue(r)); + ticket->setProperty("type", kTypeAudio); + + // Queue appending the ticket and running the next job on our thread to make this function thread-safe + QMetaObject::invokeMethod(this, "AddTicket", Qt::AutoConnection, + OLIVE_NS_ARG(RenderTicketPtr, ticket), + Q_ARG(bool, prioritize)); + + return ticket; +} + +void RenderManager::RunTicket(RenderTicketPtr ticket) const +{ + // Depending on the render ticket type, start a job + TicketType type = ticket->property("type").value(); + + switch (type) { + case kTypeVideo: + RenderFrameInternal(ticket); + break; + case kTypeAudio: + RenderAudioInternal(ticket); + break; + default: + // Fail + ticket->Cancel(); + } +} + +void RenderManager::RenderFrameInternal(RenderTicketPtr ticket) +{ + ViewerOutput* viewer = Node::ValueToPtr(ticket->property("viewer")); + rational time = ticket->property("time").value(); + + ticket->Start(); + + qDebug() << "STUB: Rendered" << time << "frames for" << viewer; + + FramePtr frame = Frame::Create(); + frame->set_video_params(viewer->video_params()); + frame->allocate(); + + ticket->Finish(QVariant::fromValue(frame), false); +} + +void RenderManager::RenderAudioInternal(RenderTicketPtr ticket) +{ + ViewerOutput* viewer = Node::ValueToPtr(ticket->property("viewer")); + TimeRange time = ticket->property("time").value(); + + ticket->Start(); + + qDebug() << "STUB: Rendered" << time << "audio for" << viewer; + + ticket->Finish(QVariant::fromValue(SampleBuffer::CreateAllocated(viewer->audio_params(), time.length())), false); +} + +void RenderManager::WorkerGeneratedWaveform(RenderTicketPtr ticket, TrackOutput *track, AudioVisualWaveform samples, TimeRange range) +{ + ViewerOutput* viewer = Node::ValueToPtr(ticket->property("viewer")); + + QList valid_ranges = viewer->audio_playback_cache()->GetValidRanges(range, + ticket->GetJobTime()); + if (!valid_ranges.isEmpty()) { + // Generate visual waveform in this background thread + track->waveform_lock()->lock(); + + track->waveform().set_channel_count(viewer->audio_params().channel_count()); + + foreach (const TimeRange& r, valid_ranges) { + track->waveform().OverwriteSums(samples, r.in(), r.in() - range.in(), r.length()); + } + + track->waveform_lock()->unlock(); + + emit track->PreviewChanged(); + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/render/rendermanager.h b/app/render/rendermanager.h new file mode 100644 index 000000000..80a3b2bb3 --- /dev/null +++ b/app/render/rendermanager.h @@ -0,0 +1,115 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef RENDERBACKEND_H +#define RENDERBACKEND_H + +#include + +#include "config/config.h" +#include "colorprocessorcache.h" +#include "dialog/rendercancel/rendercancel.h" +#include "decodercache.h" +#include "node/graph.h" +#include "node/output/viewer/viewer.h" +#include "threading/threadpool.h" + +OLIVE_NAMESPACE_ENTER + +class RenderManager : public ThreadPool +{ + Q_OBJECT +public: + static void CreateInstance() + { + instance_ = new RenderManager(); + } + + static void DestroyInstance() + { + delete instance_; + instance_ = nullptr; + } + + static RenderManager* instance() + { + return instance_; + } + + /** + * @brief Generate a unique identifier for a certain node at a certain time + */ + static QByteArray Hash(const Node *n, const VideoParams ¶ms, const rational &time); + + /** + * @brief Asynchronously generate a frame at a given time + * + * The ticket from this function will return a FramePtr - the rendered frame in reference color + * space. + * + * Setting `prioritize` to TRUE puts this ticket at the top of the queue. Leaving it as FALSE + * appends it to the bottom. + * + * This function is thread-safe. + */ + RenderTicketPtr RenderFrame(ViewerOutput* viewer, const rational& time, RenderMode::Mode mode, bool prioritize = false); + + /** + * @brief Asynchronously generate a chunk of audio + * + * The ticket from this function will return a SampleBufferPtr - the rendered audio. + * + * Setting `prioritize` to TRUE puts this ticket at the top of the queue. Leaving it as FALSE + * appends it to the bottom. + * + * This function is thread-safe. + */ + RenderTicketPtr RenderAudio(ViewerOutput* viewer, const TimeRange& r, bool prioritize = false); + + virtual void RunTicket(RenderTicketPtr ticket) const override; + + enum TicketType { + kTypeVideo, + kTypeAudio + }; + +signals: + +private: + static void RenderFrameInternal(RenderTicketPtr ticket); + + static void RenderAudioInternal(RenderTicketPtr ticket); + + RenderManager(QObject* parent = nullptr); + + virtual ~RenderManager() override; + + static RenderManager* instance_; + +private slots: + void WorkerGeneratedWaveform(OLIVE_NAMESPACE::RenderTicketPtr ticket, OLIVE_NAMESPACE::TrackOutput* track, OLIVE_NAMESPACE::AudioVisualWaveform samples, OLIVE_NAMESPACE::TimeRange range); + +}; + +Q_DECLARE_METATYPE(RenderManager::TicketType); + +OLIVE_NAMESPACE_EXIT + +#endif // RENDERBACKEND_H diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index 28215e856..c2c27c547 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -33,9 +33,6 @@ ExportTask::ExportTask(ViewerOutput* viewer_node, params_(params) { SetTitle(tr("Exporting \"%1\"").arg(viewer_node->media_name())); - - // Render highest quality - backend()->SetRenderMode(RenderMode::kOnline); } bool ExportTask::Run() @@ -66,9 +63,6 @@ bool ExportTask::Run() if (params_.video_enabled()) { - // Ensure renderer always provides the same resolution - backend()->SetForceDownloadResolution(true); - // If a transformation matrix is applied to this video, create it here if (params_.video_scaling_method() != ExportParams::kStretch) { QMatrix4x4 mat = ExportParams::GenerateMatrix(params_.video_scaling_method(), @@ -77,7 +71,7 @@ bool ExportTask::Run() params_.video_params().width(), params_.video_params().height()); - backend()->SetVideoDownloadMatrix(mat); + // FIXME: Re-implement this } // Create color processor @@ -102,7 +96,7 @@ bool ExportTask::Run() audio_data_.SetLength(range.length()); } - Render(video_range, audio_range, false); + Render(video_range, audio_range, RenderMode::kOnline, false); bool success = true; diff --git a/app/task/precache/precachetask.cpp b/app/task/precache/precachetask.cpp index e707a6949..8250c2d01 100644 --- a/app/task/precache/precachetask.cpp +++ b/app/task/precache/precachetask.cpp @@ -29,9 +29,6 @@ PreCacheTask::PreCacheTask(VideoStreamPtr footage, Sequence* sequence) : viewer()->set_video_params(sequence->video_params()); viewer()->set_audio_params(sequence->audio_params()); - // Render fastest quality - backend()->SetRenderMode(RenderMode::kOffline); - video_node_ = new VideoInput(); video_node_->SetStream(footage); @@ -39,13 +36,11 @@ PreCacheTask::PreCacheTask(VideoStreamPtr footage, Sequence* sequence) : SetTitle(tr("Pre-caching %1:%2").arg(footage->footage()->filename(), QString::number(footage->index()))); - - backend()->NodeGraphChanged(viewer()->texture_input()); - backend()->ProcessUpdateQueue(); } PreCacheTask::~PreCacheTask() { + // We created this viewer node ourselves, so now we should delete it delete viewer(); delete video_node_; } @@ -66,7 +61,7 @@ bool PreCacheTask::Run() } */ - Render(video_range, TimeRangeList(), true); + Render(video_range, TimeRangeList(), RenderMode::kOnline, true); download_threads_.waitForDone(); diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index bd813ac19..d0b88d0c1 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -21,20 +21,20 @@ #include "render.h" #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) +RenderTask::RenderTask(ViewerOutput* viewer, const VideoParams &vparams, const AudioParams &aparams) : + viewer_(viewer), + video_params_(vparams), + audio_params_(aparams) { - backend_ = new OpenGLBackend(); - backend_->SetViewerNode(viewer); - backend_->SetVideoParams(vparams); - backend_->SetAudioParams(aparams); } RenderTask::~RenderTask() { - delete backend_; } struct TimeHashFuturePair { @@ -65,8 +65,10 @@ struct HashDownloadFuturePair { void RenderTask::Render(const TimeRangeList& video_range, const TimeRangeList &audio_range, + RenderMode::Mode mode, bool use_disk_cache) { + /* double progress_counter = 0; double total_length = 0; double video_frame_sz = video_params().time_base().toDouble(); @@ -77,7 +79,7 @@ void RenderTask::Render(const TimeRangeList& video_range, foreach (const TimeRange& r, audio_range) { total_length += r.length().toDouble(); - std::list ranges = RenderBackend::SplitRangeIntoChunks(r); + std::list ranges = r.Split(2); audio_queue.insert(audio_queue.end(), ranges.begin(), ranges.end()); } } @@ -93,7 +95,7 @@ void RenderTask::Render(const TimeRangeList& video_range, total_length += video_frame_sz * times.size(); - RenderTicketPtr hash_future = backend_->Hash(times); + RenderTicketPtr hash_future = RenderManager::instance()->Hash(viewer(), times); hashes = hash_future->Get().value >(); hash_job_time = hash_future->GetJobTime(); @@ -159,7 +161,7 @@ void RenderTask::Render(const TimeRangeList& video_range, // If no existing disk cache was found, queue it now if (!hash_exists) { - render_lookup_table.push_back({p.hash, backend_->RenderFrame(p.time)}); + render_lookup_table.push_back({p.hash, RenderManager::instance()->RenderFrame(viewer(), p.time, mode)}); running_hashes.push_back(p.hash); } } @@ -169,7 +171,7 @@ void RenderTask::Render(const TimeRangeList& video_range, } while (!IsCancelled() && !audio_queue.empty()) { - audio_lookup_table.push_back({audio_queue.front(), backend_->RenderAudio(audio_queue.front())}); + audio_lookup_table.push_back({audio_queue.front(), RenderManager::instance()->RenderAudio(viewer(), audio_queue.front())}); audio_queue.pop_front(); } @@ -235,9 +237,7 @@ void RenderTask::Render(const TimeRangeList& video_range, } } } - - // `Close` will block until all jobs are done making a safe deletion - backend_->Close(); + */ } OLIVE_NAMESPACE_EXIT diff --git a/app/task/render/render.h b/app/task/render/render.h index 8571c0894..c70e40468 100644 --- a/app/task/render/render.h +++ b/app/task/render/render.h @@ -24,7 +24,6 @@ #include #include "node/output/viewer/viewer.h" -#include "render/backend/opengl/openglbackend.h" #include "task/task.h" OLIVE_NAMESPACE_ENTER @@ -38,7 +37,7 @@ public: protected: void Render(const TimeRangeList &video_range, - const TimeRangeList &audio_range, + const TimeRangeList &audio_range, RenderMode::Mode mode, bool use_disk_cache); virtual QFuture DownloadFrame(FramePtr frame, const QByteArray &hash) = 0; @@ -49,26 +48,25 @@ protected: ViewerOutput* viewer() const { - return backend_->GetViewerNode(); + return viewer_; } - VideoParams video_params() const + const VideoParams& video_params() const { - return backend_->GetVideoParams(); + return video_params_; } - AudioParams audio_params() const + const AudioParams& audio_params() const { - return backend_->GetAudioParams(); - } - - RenderBackend* backend() - { - return backend_; + return audio_params_; } private: - RenderBackend* backend_; + ViewerOutput* viewer_; + + VideoParams video_params_; + + AudioParams audio_params_; }; diff --git a/app/threading/CMakeLists.txt b/app/threading/CMakeLists.txt new file mode 100644 index 000000000..b7169048c --- /dev/null +++ b/app/threading/CMakeLists.txt @@ -0,0 +1,26 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + threading/threadticket.cpp + threading/threadticket.h + threading/threadticketwatcher.cpp + threading/threadticketwatcher.h + threading/threadpool.cpp + threading/threadpool.h + PARENT_SCOPE +) diff --git a/app/threading/threadpool.cpp b/app/threading/threadpool.cpp new file mode 100644 index 000000000..0b801b707 --- /dev/null +++ b/app/threading/threadpool.cpp @@ -0,0 +1,138 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "threadpool.h" + +OLIVE_NAMESPACE_ENTER + +ThreadPool::ThreadPool(QThread::Priority priority, int threads, QObject *parent) : + QObject(parent) +{ + all_threads_.resize(threads ? threads : QThread::idealThreadCount()); + + // Create threads + for (int i=0; istart(priority); + } +} + +ThreadPool::~ThreadPool() +{ + foreach (ThreadPoolThread* thread, all_threads_) { + thread->Cancel(); + thread->wait(); + delete thread; + } + + RunNext(); +} + +void ThreadPool::AddTicket(RenderTicketPtr ticket, bool prioritize) +{ + if (prioritize) { + ticket_queue_.push_front(ticket); + } else { + ticket_queue_.push_back(ticket); + } + + RunNext(); +} + +void ThreadPool::RunNext() +{ + while (!ticket_queue_.empty() && !available_threads_.empty()) { + // Run function + RenderTicketPtr ticket = ticket_queue_.front(); + ticket_queue_.pop_front(); + + if (!ticket->WasCancelled()) { + ThreadPoolThread* thread = available_threads_.front(); + available_threads_.pop_front(); + + // Run the ticket in the thread, which actually just calls our virtual function RunTicket + thread->RunTicket(ticket); + } + } +} + +void ThreadPool::ThreadDone() +{ + ThreadPoolThread* thread = static_cast(sender()); + + available_threads_.push_back(thread); + + RunNext(); +} + +ThreadPoolThread::ThreadPoolThread(ThreadPool *parent) +{ + pool_ = parent; + cancelled_ = false; + + // Ensures mutex is definitely locked by the time the thread is running + mutex_.lock(); +} + +ThreadPoolThread::~ThreadPoolThread() +{ + mutex_.unlock(); +} + +void ThreadPoolThread::RunTicket(RenderTicketPtr ticket) +{ + mutex_.lock(); + ticket_ = ticket; + wait_cond_.wakeAll(); + mutex_.unlock(); +} + +void ThreadPoolThread::Cancel() +{ + cancelled_ = true; + wait_cond_.wakeAll(); +} + +void ThreadPoolThread::run() +{ + while (!cancelled_) { + wait_cond_.wait(&mutex_); + + if (ticket_) { + pool_->RunTicket(ticket_); + ticket_ = nullptr; + } + + emit Done(); + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/threading/threadpool.h b/app/threading/threadpool.h new file mode 100644 index 000000000..9c3431674 --- /dev/null +++ b/app/threading/threadpool.h @@ -0,0 +1,94 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef THREADPOOL_H +#define THREADPOOL_H + +#include + +#include "threading/threadticket.h" + +OLIVE_NAMESPACE_ENTER + +class ThreadPoolThread; + +class ThreadPool : public QObject +{ + Q_OBJECT +public: + ThreadPool(QThread::Priority priority = QThread::InheritPriority, int threads = 0, QObject* parent = nullptr); + + virtual ~ThreadPool() override; + + RenderTicketPtr Queue(); + + virtual void RunTicket(RenderTicketPtr ticket) const = 0; + +public slots: + void AddTicket(OLIVE_NAMESPACE::RenderTicketPtr ticket, bool prioritize = false); + +private: + void RunNext(); + + QVector all_threads_; + + std::list available_threads_; + + std::list ticket_queue_; + +private slots: + void ThreadDone(); + +}; + +class ThreadPoolThread : public QThread +{ + Q_OBJECT +public: + ThreadPoolThread(ThreadPool* parent); + + virtual ~ThreadPoolThread() override; + + void RunTicket(RenderTicketPtr ticket); + + void Cancel(); + +protected: + virtual void run() override; + +signals: + void Done(); + +private: + ThreadPool* pool_; + + RenderTicketPtr ticket_; + + QMutex mutex_; + + QWaitCondition wait_cond_; + + QAtomicInt cancelled_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // THREADPOOL_H diff --git a/app/render/backend/renderticket.cpp b/app/threading/threadticket.cpp similarity index 63% rename from app/render/backend/renderticket.cpp rename to app/threading/threadticket.cpp index 027683cd1..30ae3b5e1 100644 --- a/app/render/backend/renderticket.cpp +++ b/app/threading/threadticket.cpp @@ -18,17 +18,16 @@ ***/ -#include "renderticket.h" +#include "threadticket.h" OLIVE_NAMESPACE_ENTER -RenderTicket::RenderTicket(Type type, const QVariant &time) : +RenderTicket::RenderTicket() : + started_(false), finished_(false), - cancelled_(false), - time_(time), - type_(type), - job_time_(0) + cancelled_(false) { + SetJobTime(); } void RenderTicket::WaitForFinished() @@ -42,12 +41,10 @@ void RenderTicket::WaitForFinished() QVariant RenderTicket::Get() { - QMutexLocker locker(&lock_); - - if (!finished_) { - wait_.wait(&lock_); - } + WaitForFinished(); + // We don't have to mutex around this because there is no way to write to `result_` after + // the ticket has finished and the above function blocks the calling thread until it is finished return result_; } @@ -73,32 +70,50 @@ bool RenderTicket::WasCancelled() return cancelled_; } -void RenderTicket::Finish(QVariant result) +void RenderTicket::Start() { QMutexLocker locker(&lock_); - finished_ = true; - result_ = result; + if (!started_ && !finished_) { + started_ = true; + } +} - wait_.wakeAll(); +void RenderTicket::Finish(QVariant result, bool cancelled) +{ + QMutexLocker locker(&lock_); - locker.unlock(); + if (started_ && !finished_) { + finished_ = true; + cancelled_ = cancelled; - emit Finished(); + result_ = result; + + wait_.wakeAll(); + + locker.unlock(); + + emit Finished(); + } } void RenderTicket::Cancel() { QMutexLocker locker(&lock_); - finished_ = true; - cancelled_ = true; + if (!finished_) { + cancelled_ = true; - wait_.wakeAll(); + if (!started_) { + finished_ = true; - locker.unlock(); + wait_.wakeAll(); - emit Finished(); + locker.unlock(); + + emit Finished(); + } + } } OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/renderticket.h b/app/threading/threadticket.h similarity index 84% rename from app/render/backend/renderticket.h rename to app/threading/threadticket.h index f4fca0c6b..7bb644640 100644 --- a/app/render/backend/renderticket.h +++ b/app/threading/threadticket.h @@ -28,6 +28,7 @@ #include "codec/frame.h" #include "codec/samplebuffer.h" #include "common/timerange.h" +#include "node/output/viewer/viewer.h" OLIVE_NAMESPACE_ENTER @@ -35,13 +36,7 @@ class RenderTicket : public QObject { Q_OBJECT public: - enum Type { - kTypeHash, - kTypeVideo, - kTypeAudio - }; - - RenderTicket(Type type, const QVariant& time); + RenderTicket(); qint64 GetJobTime() const { @@ -53,16 +48,6 @@ public: job_time_ = QDateTime::currentMSecsSinceEpoch(); } - const QVariant& GetTime() const - { - return time_; - } - - Type GetType() const - { - return type_; - } - void WaitForFinished(); QVariant Get(); @@ -76,7 +61,9 @@ public: return &lock_; } - void Finish(QVariant result); + void Start(); + + void Finish(QVariant result, bool cancelled); void Cancel(); @@ -84,6 +71,8 @@ signals: void Finished(); private: + bool started_; + bool finished_; bool cancelled_; @@ -94,10 +83,6 @@ private: QWaitCondition wait_; - QVariant time_; - - Type type_; - qint64 job_time_; }; diff --git a/app/render/backend/renderticketwatcher.cpp b/app/threading/threadticketwatcher.cpp similarity index 93% rename from app/render/backend/renderticketwatcher.cpp rename to app/threading/threadticketwatcher.cpp index d5860311d..13940668e 100644 --- a/app/render/backend/renderticketwatcher.cpp +++ b/app/threading/threadticketwatcher.cpp @@ -18,7 +18,7 @@ ***/ -#include "renderticketwatcher.h" +#include "threadticketwatcher.h" OLIVE_NAMESPACE_ENTER @@ -79,4 +79,11 @@ QVariant RenderTicketWatcher::Get() } } +void RenderTicketWatcher::Cancel() +{ + if (ticket_) { + ticket_->Cancel(); + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/renderticketwatcher.h b/app/threading/threadticketwatcher.h similarity index 96% rename from app/render/backend/renderticketwatcher.h rename to app/threading/threadticketwatcher.h index fba301a3b..6bbe979c2 100644 --- a/app/render/backend/renderticketwatcher.h +++ b/app/threading/threadticketwatcher.h @@ -21,7 +21,7 @@ #ifndef RENDERTICKETWATCHER_H #define RENDERTICKETWATCHER_H -#include "renderticket.h" +#include "threadticket.h" OLIVE_NAMESPACE_ENTER @@ -38,6 +38,8 @@ public: void SetTicket(RenderTicketPtr ticket); + void Cancel(); + bool WasCancelled(); bool IsFinished(); diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 5d6bdca82..b46619cbc 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -38,6 +38,7 @@ #include "project/item/sequence/sequence.h" #include "project/project.h" #include "render/pixelformat.h" +#include "render/rendermanager.h" #include "task/taskmanager.h" #include "widget/menu/menu.h" #include "window/mainwindow/mainwindow.h" @@ -108,12 +109,6 @@ ViewerWidget::ViewerWidget(QWidget *parent) : // FIXME: Magic number SetScale(48.0); - // Start background renderer - renderer_ = new OpenGLBackend(this); - renderer_->SetAutoCacheEnabled(true); - renderer_->SetRenderMode(RenderMode::kOffline); - renderer_->SetPreviewGenerationEnabled(true); - // Ensures that seeking on the waveform view updates the time as expected connect(waveform_view_, &AudioWaveformView::TimeChanged, this, &ViewerWidget::TimeChangedFromWaveform); connect(waveform_view_, &AudioWaveformView::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu); @@ -167,7 +162,7 @@ void ViewerWidget::TimeChangedEvent(const int64_t &i) } if (!pause_autocache_during_playback_ || !IsPlaying()) { - renderer_->SetAutoCachePlayhead(time_set); + auto_cacher_.SetPlayhead(time_set); } display_widget_->SetTime(time_set); @@ -192,8 +187,6 @@ void ViewerWidget::ConnectNodeInternal(ViewerOutput *n) ruler()->SetPlaybackCache(n->video_frame_cache()); - n->audio_playback_cache()->SetParameters(n->audio_params()); - SetViewerResolution(n->video_params().width(), n->video_params().height()); SetViewerPixelAspect(n->video_params().pixel_aspect_ratio()); last_length_ = rational(); @@ -261,7 +254,7 @@ void ViewerWidget::DisconnectNodeInternal(ViewerOutput *n) void ViewerWidget::ConnectedNodeChanged(ViewerOutput *n) { - renderer_->SetViewerNode(n); + auto_cacher_.SetViewerNode(n); } void ViewerWidget::ScaleChangedEvent(const double &s) @@ -355,18 +348,18 @@ void ViewerWidget::ForceUpdate() void ViewerWidget::SetAutoCacheEnabled(bool e) { - renderer_->SetAutoCachePaused(!e); + auto_cacher_.SetPaused(!e); } void ViewerWidget::CacheEntireSequence() { - renderer_->AutoCacheRange(TimeRange(rational(), GetConnectedNode()->video_frame_cache()->GetLength())); + auto_cacher_.ForceCacheRange(TimeRange(rational(), GetConnectedNode()->video_frame_cache()->GetLength())); } void ViewerWidget::CacheSequenceInOut() { if (GetConnectedTimelinePoints() && GetConnectedTimelinePoints()->workarea()->enabled()) { - renderer_->AutoCacheRange(GetConnectedTimelinePoints()->workarea()->range()); + auto_cacher_.ForceCacheRange(GetConnectedTimelinePoints()->workarea()->range()); } else { QMessageBox::warning(this, tr("Error"), @@ -396,7 +389,7 @@ FramePtr ViewerWidget::DecodeCachedImage(const QString &fn, const rational& time void ViewerWidget::DecodeCachedImage(RenderTicketPtr ticket, const QString &fn, const rational& time) const { - ticket->Finish(QVariant::fromValue(DecodeCachedImage(fn, time))); + ticket->Finish(QVariant::fromValue(DecodeCachedImage(fn, time)), false); } bool ViewerWidget::ShouldForceWaveform() const @@ -472,7 +465,7 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) // Kindly tell all viewers to stop caching if (pause_autocache_during_playback_) { foreach (ViewerWidget* viewer, instances_) { - viewer->renderer_->ClearVideoQueue(); + viewer->auto_cacher_.ClearVideoQueue(); } } @@ -633,14 +626,14 @@ RenderTicketPtr ViewerWidget::GetFrame(const rational &t, bool clear_render_queu if (cached_hash.isEmpty() || !QFileInfo::exists(cache_fn)) { // Frame hasn't been cached, start render job if (clear_render_queue) { - renderer_->ClearVideoQueue(); + auto_cacher_.ClearVideoQueue(); } - return renderer_->RenderFrame(t, true); + return auto_cacher_.GetSingleFrame(t); } else { // Frame has been cached, grab the frame - RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeVideo, - QVariant::fromValue(t)); + RenderTicketPtr ticket = std::make_shared(); + ticket->setProperty("time", QVariant::fromValue(t)); QtConcurrent::run(this, &ViewerWidget::DecodeCachedImage, ticket, cache_fn, t); return ticket; @@ -896,7 +889,7 @@ void ViewerWidget::ShowContextMenu(const QPoint &pos) // Auto-cache QAction* autocache_action = cache_menu->addAction(tr("Auto-Cache")); autocache_action->setCheckable(true); - autocache_action->setChecked(!renderer_->IsAutoCachePaused()); + autocache_action->setChecked(!auto_cacher_.IsPaused()); connect(autocache_action, &QAction::triggered, this, &ViewerWidget::SetAutoCacheEnabled); cache_menu->addSeparator(); @@ -981,7 +974,7 @@ void ViewerWidget::Pause() { PauseInternal(); - renderer_->SetAutoCachePlayhead(GetTime()); + auto_cacher_.SetPlayhead(GetTime()); } void ViewerWidget::ShuttleLeft() @@ -1162,25 +1155,14 @@ void ViewerWidget::InterlacingChangedSlot(VideoParams::Interlacing interlacing) void ViewerWidget::UpdateRendererVideoParameters() { - renderer_->ClearVideoQueue(); - - renderer_->SetVideoParams(GetConnectedNode()->video_params()); - - // In case the user is pressing the mouse at this exact moment - renderer_->IgnoreNextMouseButton(); - - GetConnectedNode()->video_frame_cache()->InvalidateAll(); - display_widget_->SetVideoParams(GetConnectedNode()->video_params()); + foreach (ViewerWindow* window, windows_) { + window->display_widget()->SetVideoParams(GetConnectedNode()->video_params()); + } } void ViewerWidget::UpdateRendererAudioParameters() { - renderer_->ClearAudioQueue(); - - renderer_->SetAudioParams(GetConnectedNode()->audio_params()); - - GetConnectedNode()->audio_playback_cache()->InvalidateAll(); } void ViewerWidget::SetZoomFromMenu(QAction *action) diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 06b096d7d..887f92b06 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -32,8 +32,8 @@ #include "common/rational.h" #include "node/output/viewer/viewer.h" #include "panel/scope/scope.h" -#include "render/backend/opengl/openglbackend.h" -#include "render/backend/renderticketwatcher.h" +#include "render/previewautocacher.h" +#include "threading/threadticketwatcher.h" #include "viewerdisplay.h" #include "viewerplaybacktimer.h" #include "viewerqueue.h" @@ -82,11 +82,6 @@ public: */ void SetFullScreen(QScreen* screen = nullptr); - RenderBackend* renderer() const - { - return renderer_; - } - ColorManager* color_manager() const { return display_widget_->color_manager(); @@ -246,8 +241,6 @@ private: ViewerQueue playback_queue_; int64_t playback_queue_next_frame_; - RenderBackend* renderer_; - bool prequeuing_; QList nonqueue_watchers_; @@ -256,6 +249,8 @@ private: int prequeue_length_; + PreviewAutoCacher auto_cacher_; + static QVector instances_; private slots: