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.
478 lines
13 KiB
C++
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> ×)
|
|
{
|
|
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 ¶ms)
|
|
{
|
|
video_params_ = params;
|
|
}
|
|
|
|
void RenderBackend::SetAudioParams(const AudioParams ¶ms)
|
|
{
|
|
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(©_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
|