Files
oak-editor/app/render/backend/renderbackend.cpp
T
itsmattkc efd90d5c8f renderer: use shared backend when caching
I think there was an earlier commit with a similar name but turns out
I'd only done foundational work in that commit and never actually
properly set it up. Of course once I did, there were several issues that
needed fixing to make it work correctly, but now it works as expected.

Heavily optimizes larger projects by allowing cache jobs to only copy
what has changed.
2020-07-10 18:39:47 +10:00

478 lines
13 KiB
C++

/***
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 <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
RenderBackend::RenderBackend(QObject *parent) :
QObject(parent),
viewer_node_(nullptr),
update_with_graph_(false),
preview_job_time_(0),
render_mode_(RenderMode::kOnline)
{
}
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
viewer_node_ = viewer_node;
}
if (old_viewer) {
// Delete all of our copied nodes
pool_.clear();
pool_.waitForDone();
// Cancel all tickets
foreach (RenderTicketPtr t, render_queue_) {
t->Cancel();
}
render_queue_.clear();
// Delete all the nodes
foreach (Node* c, copy_map_) {
c->deleteLater();
}
copy_map_.clear();
copied_viewer_node_ = nullptr;
graph_update_queue_.clear();
disconnect(old_viewer,
&ViewerOutput::GraphChangedFrom,
this,
&RenderBackend::NodeGraphChanged);
}
if (viewer_node) {
// If setting to non-null, set it now
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_);
NodeGraphChanged(viewer_node_->texture_input());
NodeGraphChanged(viewer_node_->samples_input());
ProcessUpdateQueue();
if (update_with_graph_) {
connect(viewer_node_,
&ViewerOutput::GraphChangedFrom,
this,
&RenderBackend::NodeGraphChanged);
}
}
}
void RenderBackend::ClearVideoQueue()
{
foreach (RenderTicketPtr t, render_queue_) {
t->Cancel();
}
render_queue_.clear();
}
RenderTicketPtr RenderBackend::Hash(const QVector<rational> &times)
{
if (!viewer_node_) {
return nullptr;
}
RenderTicketPtr ticket = std::make_shared<RenderTicket>(RenderTicket::kTypeHash,
QVariant::fromValue(times));
render_queue_.push_back(ticket);
QMetaObject::invokeMethod(this, "RunNextJob", Qt::QueuedConnection);
return ticket;
}
RenderTicketPtr RenderBackend::RenderFrame(const rational &time)
{
if (!viewer_node_) {
return nullptr;
}
RenderTicketPtr ticket = std::make_shared<RenderTicket>(RenderTicket::kTypeVideo,
QVariant::fromValue(time));
render_queue_.push_back(ticket);
QMetaObject::invokeMethod(this, "RunNextJob", Qt::QueuedConnection);
return ticket;
}
RenderTicketPtr RenderBackend::RenderAudio(const TimeRange &r)
{
if (!viewer_node_) {
return nullptr;
}
RenderTicketPtr ticket = std::make_shared<RenderTicket>(RenderTicket::kTypeAudio,
QVariant::fromValue(r));
render_queue_.push_back(ticket);
QMetaObject::invokeMethod(this, "RunNextJob", Qt::QueuedConnection);
return ticket;
}
void RenderBackend::SetVideoParams(const VideoParams &params)
{
video_params_ = params;
}
void RenderBackend::SetAudioParams(const AudioParams &params)
{
audio_params_ = params;
}
void RenderBackend::SetVideoDownloadMatrix(const QMatrix4x4 &mat)
{
video_download_matrix_ = mat;
}
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::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 dependency graph is already queued
if (source->parentNode()->OutputsTo(queued_input, true)) {
// In which case, no further copy is necessary
return;
}
// Check if this input supersedes an already queued input
if (queued_input->parentNode()->OutputsTo(source, true)) {
// In which case, we don't need to queue it and can queue our own
graph_update_queue_.removeAt(i);
i--;
}
}
graph_update_queue_.append(source);
}
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()) {
return;
}
// Check if params are valid
if (!video_params_.is_valid()
|| !audio_params_.is_valid()) {
qDebug() << "Failed to run job, parameters are invalid";
return;
}
// If we have a value update queued, check if all workers are available and proceed from there
if (update_with_graph_ && !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(pool_.maxThreadCount());
for (int i=0;i<workers_.size();i++) {
RenderWorker* worker = CreateNewWorker();
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->SetVideoDownloadMatrix(video_dwnload_matrix_);
worker->SetRenderMode(render_mode_);
if (preview_job_time_) {
worker->EnablePreviewGeneration(viewer_node_->audio_playback_cache(), preview_job_time_);
}
worker->SetCopyMap(&copy_map_);
RenderTicketPtr ticket = render_queue_.front();
render_queue_.pop_front();
switch (ticket->GetType()) {
case RenderTicket::kTypeHash:
QtConcurrent::run(&pool_,
worker,
&RenderWorker::Hash,
ticket,
copied_viewer_node_,
ticket->GetTime().value<QVector<rational> >());
break;
case RenderTicket::kTypeVideo:
QtConcurrent::run(&pool_,
worker,
&RenderWorker::RenderFrame,
ticket,
copied_viewer_node_,
ticket->GetTime().value<rational>());
break;
case RenderTicket::kTypeAudio:
QtConcurrent::run(&pool_,
worker,
&RenderWorker::RenderAudio,
ticket,
copied_viewer_node_,
ticket->GetTime().value<TimeRange>());
break;
}
if (render_queue_.empty()) {
// No more jobs, can exit here
break;
}
}
}
}
//#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
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());
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);
}
}
OLIVE_NAMESPACE_EXIT