moved things around and started rewriting render backend
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -38,6 +38,11 @@ SampleBufferPtr SampleBuffer::Create()
|
||||
return std::make_shared<SampleBuffer>();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "timerange.h"
|
||||
|
||||
#include <QtMath>
|
||||
#include <utility>
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
@@ -148,6 +149,21 @@ const TimeRange &TimeRange::operator-=(const rational &rhs)
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::list<TimeRange> TimeRange::Split(const int &chunk_size) const
|
||||
{
|
||||
std::list<TimeRange> split_ranges;
|
||||
|
||||
int start_time = qFloor(this->in().toDouble() / static_cast<double>(chunk_size)) * chunk_size;
|
||||
int end_time = qCeil(this->out().toDouble() / static_cast<double>(chunk_size)) * chunk_size;
|
||||
|
||||
for (int i=start_time; i<end_time; i+=chunk_size) {
|
||||
split_ranges.push_back(TimeRange(qMax(this->in(), rational(i)),
|
||||
qMin(this->out(), rational(i + chunk_size))));
|
||||
}
|
||||
|
||||
return split_ranges;
|
||||
}
|
||||
|
||||
void TimeRange::normalize()
|
||||
{
|
||||
// If `out` is earlier than `in`, swap them
|
||||
|
||||
@@ -56,6 +56,8 @@ public:
|
||||
const TimeRange& operator+=(const rational &rhs);
|
||||
const TimeRange& operator-=(const rational &rhs);
|
||||
|
||||
std::list<TimeRange> Split(const int &chunk_size) const;
|
||||
|
||||
private:
|
||||
void normalize();
|
||||
|
||||
|
||||
+5
-3
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "sequence.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "common/channellayout.h"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#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<OpenGLTextureCache::ReferencePtr>()->texture()->format());
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "renderbackend.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QThread>
|
||||
|
||||
#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*> 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<void>* 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<QFutureWatcher<bool>*, 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<ViewerOutput*>(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<rational> ×, bool prioritize)
|
||||
{
|
||||
Q_ASSERT(viewer_node_);
|
||||
|
||||
SetActiveInstance();
|
||||
|
||||
RenderTicketPtr ticket = std::make_shared<RenderTicket>(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>(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>(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<TimeRange> RenderBackend::SplitRangeIntoChunks(const TimeRange &r)
|
||||
{
|
||||
// FIXME: Magic number
|
||||
const int chunk_size = 2;
|
||||
|
||||
std::list<TimeRange> split_ranges;
|
||||
|
||||
int start_time = qFloor(r.in().toDouble() / static_cast<double>(chunk_size)) * chunk_size;
|
||||
int end_time = qCeil(r.out().toDouble() / static_cast<double>(chunk_size)) * chunk_size;
|
||||
|
||||
for (int i=start_time; i<end_time; i+=chunk_size) {
|
||||
split_ranges.push_back(TimeRange(qMax(r.in(), rational(i)),
|
||||
qMin(r.out(), rational(i + chunk_size))));
|
||||
}
|
||||
|
||||
return split_ranges;
|
||||
}
|
||||
|
||||
void RenderBackend::ClearVideoQueue()
|
||||
{
|
||||
ClearQueueOfType(RenderTicket::kTypeVideo);
|
||||
|
||||
autocache_has_changed_ = true;
|
||||
use_custom_autocache_range_ = false;
|
||||
}
|
||||
|
||||
void RenderBackend::ClearAudioQueue()
|
||||
{
|
||||
ClearQueueOfType(RenderTicket::kTypeAudio);
|
||||
}
|
||||
|
||||
void RenderBackend::ClearQueue()
|
||||
{
|
||||
foreach (RenderTicketPtr t, render_queue_) {
|
||||
t->Cancel();
|
||||
}
|
||||
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; i<graph_update_queue_.size(); i++) {
|
||||
NodeInput* queued_input = graph_update_queue_.at(i);
|
||||
|
||||
// If this input is already queued, nothing to be done
|
||||
if (source == queued_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this input supersedes an already queued input
|
||||
if ((source->IsArray() && static_cast<NodeInputArray*>(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<NodeInputArray*>(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;i<workers_.size();i++) {
|
||||
workers_.at(i).worker->deleteLater();
|
||||
}
|
||||
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;i<workers_.size();i++) {
|
||||
RenderWorker* worker = CreateNewWorker();
|
||||
|
||||
connect(worker, &RenderWorker::WaveformGenerated, this, &RenderBackend::WorkerGeneratedWaveform);
|
||||
connect(worker, &RenderWorker::FinishedJob, this, &RenderBackend::WorkerFinished);
|
||||
|
||||
workers_.replace(i, {worker, false});
|
||||
}
|
||||
}
|
||||
|
||||
// Start popping jobs off the queue
|
||||
for (int i=0;i<workers_.size();i++) {
|
||||
if (!workers_.at(i).busy) {
|
||||
// This worker is available, send it the job
|
||||
|
||||
RenderWorker* worker = workers_[i].worker;
|
||||
|
||||
workers_[i].busy = true;
|
||||
|
||||
worker->SetVideoParams(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<QVector<rational> >());
|
||||
break;
|
||||
case RenderTicket::kTypeVideo:
|
||||
{
|
||||
Q_ASSERT(video_params_.is_valid());
|
||||
|
||||
rational frame = ticket->GetTime().value<rational>();
|
||||
|
||||
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<TimeRange>());
|
||||
break;
|
||||
}
|
||||
|
||||
if (render_queue_.empty()) {
|
||||
// No more jobs, can exit here
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RenderBackend::TicketFinished()
|
||||
{
|
||||
RenderTicketPtr ticket = static_cast<RenderTicketWatcher*>(sender())->GetTicket();
|
||||
delete sender();
|
||||
|
||||
running_tickets_.remove(ticket);
|
||||
}
|
||||
|
||||
void RenderBackend::WorkerGeneratedWaveform(RenderTicketPtr ticket, TrackOutput *track, AudioVisualWaveform samples, TimeRange range)
|
||||
{
|
||||
QList<TimeRange> 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<rational> 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<rational>& times, const QVector<QByteArray>& hashes, qint64 job_time)
|
||||
{
|
||||
std::vector<QByteArray> existing_hashes;
|
||||
|
||||
for (int i=0; i<times.size(); i++) {
|
||||
// See if hash already exists in disk cache
|
||||
const QByteArray& hash = hashes.at(i);
|
||||
const rational& time = times.at(i);
|
||||
|
||||
// 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(cache->CachePathName(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<RenderTicketWatcher*>(sender());
|
||||
|
||||
if (autocache_hash_tasks_.contains(watcher)) {
|
||||
if (!watcher->WasCancelled()) {
|
||||
QFutureWatcher<void>* hw = new QFutureWatcher<void>();
|
||||
connect(hw, &QFutureWatcher<void>::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<QVector<QByteArray> >(),
|
||||
watcher->GetTicket()->GetJobTime()));
|
||||
}
|
||||
|
||||
autocache_hash_tasks_.remove(watcher);
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
|
||||
void RenderBackend::AutoCacheHashesProcessed()
|
||||
{
|
||||
QFutureWatcher<void>* watcher = static_cast<QFutureWatcher<void>*>(sender());
|
||||
|
||||
if (autocache_hash_process_tasks_.contains(watcher)) {
|
||||
autocache_hash_process_tasks_.removeOne(watcher);
|
||||
|
||||
AutoCacheRequeueFrames();
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
|
||||
void RenderBackend::AutoCacheAudioRendered()
|
||||
{
|
||||
RenderTicketWatcher* watcher = static_cast<RenderTicketWatcher*>(sender());
|
||||
|
||||
if (autocache_audio_tasks_.contains(watcher)) {
|
||||
if (!watcher->WasCancelled()) {
|
||||
viewer_node_->audio_playback_cache()->WritePCM(autocache_audio_tasks_.value(watcher),
|
||||
watcher->Get().value<SampleBufferPtr>(),
|
||||
watcher->GetTicket()->GetJobTime());
|
||||
}
|
||||
|
||||
autocache_audio_tasks_.remove(watcher);
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
|
||||
void RenderBackend::AutoCacheVideoRendered()
|
||||
{
|
||||
RenderTicketWatcher* watcher = static_cast<RenderTicketWatcher*>(sender());
|
||||
|
||||
if (autocache_video_tasks_.contains(watcher)) {
|
||||
if (!watcher->WasCancelled()) {
|
||||
const QByteArray& hash = autocache_video_tasks_.value(watcher);
|
||||
|
||||
// Download frame in another thread
|
||||
QFutureWatcher<bool>* w = new QFutureWatcher<bool>();
|
||||
autocache_video_download_tasks_.insert(w, hash);
|
||||
connect(w, &QFutureWatcher<bool>::finished, this, &RenderBackend::AutoCacheVideoDownloaded);
|
||||
w->setFuture(QtConcurrent::run(viewer_node_->video_frame_cache(),
|
||||
&FrameHashCache::SaveCacheFrame,
|
||||
hash,
|
||||
watcher->Get().value<FramePtr>()));
|
||||
}
|
||||
|
||||
autocache_video_tasks_.remove(watcher);
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
|
||||
void RenderBackend::AutoCacheVideoDownloaded()
|
||||
{
|
||||
QFutureWatcher<bool>* watcher = static_cast<QFutureWatcher<bool>*>(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<NodeInput*>(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<RenderWorker*>(sender());
|
||||
|
||||
// Set busy state to false
|
||||
for (int i=0;i<workers_.size();i++) {
|
||||
if (workers_.at(i).worker == worker) {
|
||||
workers_[i].busy = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (viewer_node_) {
|
||||
RunNextJob();
|
||||
}
|
||||
}
|
||||
|
||||
void RenderBackend::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<Node*> 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<NodeInputArray*>(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<TrackOutput*>(dst_node)->set_track_type(static_cast<TrackOutput*>(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<NodeInput*> src_node_inputs = src_node->GetInputsIncludingArrays();
|
||||
QList<NodeInput*> dst_node_inputs = dst_node->GetInputsIncludingArrays();
|
||||
|
||||
for (int i=0;i<src_node_inputs.size();i++) {
|
||||
NodeInput* src_input = src_node_inputs.at(i);
|
||||
|
||||
CopyNodeMakeConnection(src_input, dst_node_inputs.at(i));
|
||||
}
|
||||
|
||||
return dst_node;
|
||||
}
|
||||
|
||||
void RenderBackend::CopyNodeMakeConnection(NodeInput* src_input, NodeInput* dst_input)
|
||||
{
|
||||
if (src_input->is_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<RenderTicketPtr>::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<rational> invalidated_ranges = viewer_node_->video_frame_cache()->GetInvalidatedFrames(using_range);
|
||||
|
||||
ClearVideoQueue();
|
||||
|
||||
// QMaps are automatically sorted by time which is always best for rendering
|
||||
QList<QByteArray> 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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef RENDERBACKEND_H
|
||||
#define RENDERBACKEND_H
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#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<rational>(),
|
||||
playhead + Config::Current()["DiskCacheAhead"].value<rational>());
|
||||
|
||||
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<rational> ×, 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<TimeRange> 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<rational>& times, const QVector<QByteArray>& 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<NodeInput*> graph_update_queue_;
|
||||
QHash<Node*, Node*> copy_map_;
|
||||
ViewerOutput* copied_viewer_node_;
|
||||
|
||||
std::list<RenderTicketPtr> render_queue_;
|
||||
|
||||
std::list<RenderTicketPtr> running_tickets_;
|
||||
|
||||
struct WorkerData {
|
||||
RenderWorker* worker;
|
||||
bool busy;
|
||||
};
|
||||
|
||||
QVector<WorkerData> 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<RenderBackend*> instances_;
|
||||
static QMutex instance_lock_;
|
||||
static RenderBackend* active_instance_;
|
||||
static QThreadPool thread_pool_;
|
||||
void SetActiveInstance();
|
||||
|
||||
QMap<RenderTicketWatcher*, QVector<rational> > autocache_hash_tasks_;
|
||||
|
||||
QList<QFutureWatcher<void>*> autocache_hash_process_tasks_;
|
||||
|
||||
QMap<RenderTicketWatcher*, TimeRange> autocache_audio_tasks_;
|
||||
|
||||
QMap<RenderTicketWatcher*, QByteArray> autocache_video_tasks_;
|
||||
|
||||
QMap<QFutureWatcher<bool>*, QByteArray> autocache_video_download_tasks_;
|
||||
|
||||
QVector<QByteArray> 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
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "rendercontext.h"
|
||||
|
||||
RenderContext::RenderContext()
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#ifndef RENDERCONTEXT_H
|
||||
#define RENDERCONTEXT_H
|
||||
|
||||
|
||||
class RenderContext
|
||||
{
|
||||
public:
|
||||
RenderContext();
|
||||
};
|
||||
|
||||
#endif // RENDERCONTEXT_H
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "renderframebuffer.h"
|
||||
|
||||
RenderFrameBuffer::RenderFrameBuffer()
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#ifndef RENDERFRAMEBUFFER_H
|
||||
#define RENDERFRAMEBUFFER_H
|
||||
|
||||
|
||||
class RenderFrameBuffer
|
||||
{
|
||||
public:
|
||||
RenderFrameBuffer();
|
||||
};
|
||||
|
||||
#endif // RENDERFRAMEBUFFER_H
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "rendershader.h"
|
||||
|
||||
RenderShader::RenderShader()
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#ifndef RENDERSHADER_H
|
||||
#define RENDERSHADER_H
|
||||
|
||||
|
||||
class RenderShader
|
||||
{
|
||||
public:
|
||||
RenderShader();
|
||||
};
|
||||
|
||||
#endif // RENDERSHADER_H
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "rendertexture.h"
|
||||
|
||||
RenderTexture::RenderTexture(RenderContext *ctx)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef RENDERTEXTURE_H
|
||||
#define RENDERTEXTURE_H
|
||||
|
||||
#include "rendercontext.h"
|
||||
|
||||
class RenderTexture
|
||||
{
|
||||
public:
|
||||
RenderTexture(RenderContext* ctx);
|
||||
};
|
||||
|
||||
#endif // RENDERTEXTURE_H
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "renderworker.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
|
||||
#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<rational> ×)
|
||||
{
|
||||
ticket_ = ticket;
|
||||
|
||||
QVector<QByteArray> hashes(times.size());
|
||||
|
||||
for (int i=0;i<hashes.size();i++) {
|
||||
hashes[i] = HashNode(viewer->texture_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<const char*>(¶ms.effective_width()), sizeof(int));
|
||||
hasher.addData(reinterpret_cast<const char*>(¶ms.effective_height()), sizeof(int));
|
||||
hasher.addData(reinterpret_cast<const char*>(¶ms.format()), sizeof(PixelFormat::Format));
|
||||
|
||||
if (n) {
|
||||
n->Hash(hasher, time);
|
||||
}
|
||||
|
||||
return hasher.result();
|
||||
}
|
||||
|
||||
void RenderWorker::ClearOldDecoders()
|
||||
{
|
||||
QMutexLocker locker(&decoder_lock_);
|
||||
|
||||
QHash<Stream*, qint64>::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<Block*> 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<SampleBufferPtr>();
|
||||
|
||||
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<Node*, Node*>::const_iterator i;
|
||||
for (i=copy_map_->constBegin(); i!=copy_map_->constEnd(); i++) {
|
||||
if (i.value() == track) {
|
||||
original_track = static_cast<TrackOutput*>(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;i<job.samples()->sample_count();i++) {
|
||||
// Calculate the exact rational time at this sample
|
||||
double sample_to_second = static_cast<double>(i) / static_cast<double>(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<VideoStream>(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<AudioStream>(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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef RENDERWORKER_H
|
||||
#define RENDERWORKER_H
|
||||
|
||||
#include <QMatrix4x4>
|
||||
|
||||
#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<Node*, Node*>* 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<rational>& 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<Stream*, CachedStill> still_image_cache_;
|
||||
|
||||
bool video_force_download_resolution_;
|
||||
QMatrix4x4 video_download_matrix_;
|
||||
|
||||
QMutex decoder_lock_;
|
||||
DecoderCache decoder_cache_;
|
||||
QHash<Stream*, qint64> decoder_age_;
|
||||
|
||||
TimeRange audio_render_time_;
|
||||
bool available_;
|
||||
|
||||
bool generate_audio_previews_;
|
||||
|
||||
QHash<Node*, Node*>* 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
|
||||
@@ -0,0 +1,695 @@
|
||||
#include "previewautocacher.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#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<RenderTicket>();
|
||||
|
||||
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; i<graph_update_queue_.size(); i++) {
|
||||
NodeInput* queued_input = graph_update_queue_.at(i);
|
||||
|
||||
// If this input is already queued, nothing to be done
|
||||
if (source == queued_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this input supersedes an already queued input
|
||||
if ((source->IsArray() && static_cast<NodeInputArray*>(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<NodeInputArray*>(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<rational> ×, qint64 job_time)
|
||||
{
|
||||
std::vector<QByteArray> 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<void>* watcher = static_cast<QFutureWatcher<void>*>(sender());
|
||||
|
||||
if (hash_tasks_.contains(watcher)) {
|
||||
hash_tasks_.removeOne(watcher);
|
||||
|
||||
RequeueFrames();
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::AudioRendered()
|
||||
{
|
||||
RenderTicketWatcher* watcher = static_cast<RenderTicketWatcher*>(sender());
|
||||
|
||||
if (audio_tasks_.contains(watcher)) {
|
||||
if (!watcher->WasCancelled()) {
|
||||
viewer_node_->audio_playback_cache()->WritePCM(audio_tasks_.value(watcher),
|
||||
watcher->Get().value<SampleBufferPtr>(),
|
||||
watcher->GetTicket()->GetJobTime());
|
||||
}
|
||||
|
||||
audio_tasks_.remove(watcher);
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::VideoRendered()
|
||||
{
|
||||
RenderTicketWatcher* watcher = static_cast<RenderTicketWatcher*>(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<bool>* w = new QFutureWatcher<bool>();
|
||||
video_download_tasks_.insert(w, hash);
|
||||
connect(w, &QFutureWatcher<bool>::finished, this, &PreviewAutoCacher::VideoDownloaded);
|
||||
w->setFuture(QtConcurrent::run(viewer_node_->video_frame_cache(),
|
||||
&FrameHashCache::SaveCacheFrame,
|
||||
hash,
|
||||
watcher->Get().value<FramePtr>()));
|
||||
}
|
||||
|
||||
video_tasks_.remove(watcher);
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::VideoDownloaded()
|
||||
{
|
||||
QFutureWatcher<bool>* watcher = static_cast<QFutureWatcher<bool>*>(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<NodeInput*>(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<rational>(),
|
||||
playhead + Config::Current()["DiskCacheAhead"].value<rational>());
|
||||
|
||||
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<Node*> 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<NodeInputArray*>(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<TrackOutput*>(dst_node)->set_track_type(static_cast<TrackOutput*>(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<NodeInput*> src_node_inputs = src_node->GetInputsIncludingArrays();
|
||||
QList<NodeInput*> dst_node_inputs = dst_node->GetInputsIncludingArrays();
|
||||
|
||||
for (int i=0;i<src_node_inputs.size();i++) {
|
||||
NodeInput* src_input = src_node_inputs.at(i);
|
||||
|
||||
CopyNodeMakeConnection(src_input, dst_node_inputs.at(i));
|
||||
}
|
||||
|
||||
return dst_node;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::CopyNodeMakeConnection(NodeInput* src_input, NodeInput* dst_input)
|
||||
{
|
||||
if (src_input->is_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<rational> frames = viewer_node_->video_frame_cache()->GetFrameListFromTimeRange(invalidated_video_);
|
||||
|
||||
QFutureWatcher<void>* watcher = new QFutureWatcher<void>();
|
||||
hash_tasks_.append(watcher);
|
||||
connect(watcher, &QFutureWatcher<void>::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<TimeRange> 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<RenderTicketPtr>();
|
||||
passthrough->Finish(watcher->GetTicket()->Get(), watcher->GetTicket()->WasCancelled());
|
||||
watcher->deleteLater();
|
||||
});
|
||||
|
||||
watcher->SetTicket(RenderManager::instance()->RenderFrame(copied_viewer_node_,
|
||||
ticket->property("time").value<rational>(),
|
||||
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<rational> 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<QFutureWatcher<bool>*, 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<ViewerOutput*>(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
|
||||
@@ -0,0 +1,198 @@
|
||||
#ifndef AUTOCACHER_H
|
||||
#define AUTOCACHER_H
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#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<rational>& 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<NodeInput*> graph_update_queue_;
|
||||
QHash<Node*, Node*> 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<RenderTicketPtr> single_frame_renders_;
|
||||
|
||||
QList<QFutureWatcher<void>*> hash_tasks_;
|
||||
QMap<RenderTicketWatcher*, TimeRange> audio_tasks_;
|
||||
QMap<RenderTicketWatcher*, QByteArray> video_tasks_;
|
||||
QMap<QFutureWatcher<bool>*, QByteArray> video_download_tasks_;
|
||||
|
||||
QVector<QByteArray> 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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "rendermanager.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QThread>
|
||||
|
||||
#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<const char*>(¶ms.effective_width()), sizeof(int));
|
||||
hasher.addData(reinterpret_cast<const char*>(¶ms.effective_height()), sizeof(int));
|
||||
hasher.addData(reinterpret_cast<const char*>(¶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<RenderTicket>();
|
||||
|
||||
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<RenderTicket>();
|
||||
|
||||
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<TicketType>();
|
||||
|
||||
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<ViewerOutput>(ticket->property("viewer"));
|
||||
rational time = ticket->property("time").value<rational>();
|
||||
|
||||
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<ViewerOutput>(ticket->property("viewer"));
|
||||
TimeRange time = ticket->property("time").value<TimeRange>();
|
||||
|
||||
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<ViewerOutput>(ticket->property("viewer"));
|
||||
|
||||
QList<TimeRange> 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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef RENDERBACKEND_H
|
||||
#define RENDERBACKEND_H
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
+13
-13
@@ -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<TimeRange> ranges = RenderBackend::SplitRangeIntoChunks(r);
|
||||
std::list<TimeRange> 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<QVector<QByteArray> >();
|
||||
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
|
||||
|
||||
+11
-13
@@ -24,7 +24,6 @@
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#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<void> 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_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
)
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#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; i<all_threads_.size(); i++) {
|
||||
ThreadPoolThread* t = new ThreadPoolThread(this);
|
||||
|
||||
// Add to vector of all threads
|
||||
all_threads_[i] = t;
|
||||
|
||||
// Append to list of available threads
|
||||
available_threads_.push_back(t);
|
||||
|
||||
// Connect done signal
|
||||
connect(t, &ThreadPoolThread::Done, this, &ThreadPool::ThreadDone);
|
||||
|
||||
// Start the thread at the given priority
|
||||
t->start(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<ThreadPoolThread*>(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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef THREADPOOL_H
|
||||
#define THREADPOOL_H
|
||||
|
||||
#include <QThread>
|
||||
|
||||
#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<ThreadPoolThread*> all_threads_;
|
||||
|
||||
std::list<ThreadPoolThread*> available_threads_;
|
||||
|
||||
std::list<RenderTicketPtr> 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
|
||||
@@ -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
|
||||
@@ -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_;
|
||||
|
||||
};
|
||||
@@ -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
|
||||
@@ -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();
|
||||
@@ -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>(RenderTicket::kTypeVideo,
|
||||
QVariant::fromValue(t));
|
||||
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
|
||||
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)
|
||||
|
||||
@@ -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<RenderTicketWatcher*> nonqueue_watchers_;
|
||||
@@ -256,6 +249,8 @@ private:
|
||||
|
||||
int prequeue_length_;
|
||||
|
||||
PreviewAutoCacher auto_cacher_;
|
||||
|
||||
static QVector<ViewerWidget*> instances_;
|
||||
|
||||
private slots:
|
||||
|
||||
Reference in New Issue
Block a user