/*** Olive - Non-Linear Video Editor Copyright (C) 2019 Olive Team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***/ #include "renderbackend.h" #include #include #include "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), auto_audio_(true), ic_from_conform_(false) { // FIXME: Don't create in CLI mode //cancel_dialog_ = new RenderCancelDialog(Core::instance()->main_window()); } RenderBackend::~RenderBackend() { Close(); } void RenderBackend::SetViewerNode(ViewerOutput *viewer_node) { if (viewer_node_ == viewer_node) { return; } if (viewer_node_) { // Clear queue and wait for any currently running actions to complete CancelQueue(); // Delete all of our copied nodes hash_pool_.Close(); video_pool_.Close(); audio_pool_.Close(); queued_audio_.clear(); disconnect(viewer_node_, &ViewerOutput::GraphChangedFrom, this, &RenderBackend::NodeGraphChanged); disconnect(viewer_node_->audio_playback_cache(), &AudioPlaybackCache::Invalidated, this, &RenderBackend::AudioInvalidated); } // Set viewer node viewer_node_ = viewer_node; if (viewer_node_) { // Initiate instances with new node hash_pool_.Init(viewer_node_); video_pool_.Init(viewer_node_); audio_pool_.Init(viewer_node_); connect(viewer_node_, &ViewerOutput::GraphChangedFrom, this, &RenderBackend::NodeGraphChanged); if (auto_audio_) { // Listen for audio invalidation signals connect(viewer_node_->audio_playback_cache(), &AudioPlaybackCache::Invalidated, this, &RenderBackend::AudioInvalidated); // Start caching audio foreach (const TimeRange& r, viewer_node_->audio_playback_cache()->GetInvalidatedRanges()) { AudioInvalidated(r); } } } } void RenderBackend::CancelQueue() { // FIXME: Implement something better than this... video_pool_.threads.waitForDone(); audio_pool_.threads.waitForDone(); hash_pool_.threads.waitForDone(); } QFuture RenderBackend::Hash(const rational &time, bool block_for_update) { return QtConcurrent::run(&hash_pool_.threads, GetInstanceFromPool(hash_pool_), &RenderWorker::Hash, time, block_for_update); } QFuture RenderBackend::RenderFrame(const rational &time, bool clear_queue, bool block_for_update) { if (clear_queue) { video_pool_.threads.clear(); } return QtConcurrent::run(&video_pool_.threads, GetInstanceFromPool(video_pool_), &RenderWorker::RenderFrame, time, block_for_update); } QFuture RenderBackend::RenderAudio(const TimeRange &r, bool block_for_update) { return QtConcurrent::run(&audio_pool_.threads, GetInstanceFromPool(audio_pool_), &RenderWorker::RenderAudio, r, block_for_update); } void RenderBackend::SetVideoParams(const VideoRenderingParams ¶ms) { video_params_ = params; } void RenderBackend::SetAudioParams(const AudioRenderingParams ¶ms) { audio_params_ = params; } void RenderBackend::SetVideoDownloadMatrix(const QMatrix4x4 &mat) { video_download_matrix_ = mat; } void RenderBackend::SetAutomaticAudio(bool e) { auto_audio_ = e; } void RenderBackend::WorkerStartedRenderingAudio(const TimeRange &r) { queued_audio_lock_.lock(); queued_audio_.RemoveTimeRange(r); queued_audio_lock_.unlock(); } QList RenderBackend::SplitRangeIntoChunks(const TimeRange &r) { const int chunk_size = 2; QList split_ranges; int start_time = qFloor(r.in().toDouble() / static_cast(chunk_size)) * chunk_size; int end_time = qCeil(r.out().toDouble() / static_cast(chunk_size)) * chunk_size; for (int i=start_time; iSetAvailable(false); instance->ProcessQueue(); instance->SetVideoParams(video_params_); instance->SetAudioParams(audio_params_); instance->SetVideoDownloadMatrix(video_download_matrix_); instance->SetAudioModeIsPreview(auto_audio_); } void RenderBackend::Close() { CancelQueue(); video_pool_.Destroy(); audio_pool_.Destroy(); hash_pool_.Destroy(); } RenderWorker *RenderBackend::GetInstanceFromPool(RenderPool& pool) { RenderWorker* instance = nullptr; foreach (RenderWorker* worker, pool.instances) { if (worker->IsAvailable()) { instance = worker; break; } } if (!instance) { if (pool.instances.size() < pool.threads.maxThreadCount()) { // Can create another instance instance = CreateNewWorker(); pool.instances.append(instance); if (viewer_node_) { instance->Init(viewer_node_); } connect(instance, &RenderWorker::FinishedJob, this, &RenderBackend::WorkerFinished, Qt::QueuedConnection); connect(instance, &RenderWorker::AudioConformUnavailable, this, &RenderBackend::AudioConformUnavailable, Qt::QueuedConnection); } else { instance = pool.instances.at(pool.queuer % pool.instances.size()); pool.queuer++; } } return instance; } void RenderBackend::ListenForConformSignal(AudioStreamPtr s) { foreach (const ConformWaitInfo& info, conform_wait_info_) { if (info.stream == s) { // We've probably already connected to this one return; } } connect(s.get(), &AudioStream::ConformAppended, this, &RenderBackend::AudioConformUpdated); } void RenderBackend::StopListeningForConformSignal(AudioStream *s) { foreach (const ConformWaitInfo& info, conform_wait_info_) { if (info.stream.get() == s) { // There are still conforms we're waiting for, don't disconnect return; } } disconnect(s, &AudioStream::ConformAppended, this, &RenderBackend::AudioConformUpdated); } void RenderBackend::AudioConformUnavailable(StreamPtr stream, TimeRange range, rational stream_time, AudioRenderingParams params) { ConformWaitInfo info = {stream, params, range, stream_time}; if (conform_wait_info_.contains(info)) { return; } AudioStreamPtr audio_stream = std::static_pointer_cast(stream); if (audio_stream->try_start_conforming(params)) { // Start indexing process ListenForConformSignal(audio_stream); conform_wait_info_.append(info); ConformTask* conform_task = new ConformTask(audio_stream, params); TaskManager::instance()->AddTask(conform_task); } else if (audio_stream->has_conformed_version(params)) { // Conform JUST finished, requeue this time ic_from_conform_ = true; AudioInvalidated(range); ic_from_conform_ = false; } else { // A conform task is already running, so we'll just wait for it ListenForConformSignal(audio_stream); conform_wait_info_.append(info); } } void RenderBackend::AudioConformUpdated(AudioRenderingParams params) { AudioStream *stream = static_cast(sender()); for (int i=0;i r.in()) { info.affected_range.set_out(r.in()); } else if (info.affected_range.in() < r.out() && info.affected_range.out() > r.out()) { info.affected_range.set_in(r.out()); } } } // Split into 2 second chunks, one for each thread QList split_ranges = SplitRangeIntoChunks(r); foreach (const TimeRange& this_range, split_ranges) { { // Check if this range is already in the queue but hasn't started yet, in which case it'll // automatically update to the parameters we have now anyway and we don't need to queue again QMutexLocker locker(&queued_audio_lock_); if (queued_audio_.ContainsTimeRange(this_range)) { continue; } queued_audio_.InsertTimeRange(this_range); } // Queue this range QFutureWatcher* watcher = new QFutureWatcher(); connect(watcher, &QFutureWatcher::finished, this, &RenderBackend::AudioRendered); audio_jobs_.insert(watcher, this_range); watcher->setFuture(RenderAudio(this_range, auto_audio_)); } } void RenderBackend::AudioRendered() { QFutureWatcher* watcher = static_cast*>(sender()); if (audio_jobs_.contains(watcher)) { TimeRange r = audio_jobs_.take(watcher); if (watcher->result()) { viewer_node_->audio_playback_cache()->WritePCM(r, watcher->result()); } else { viewer_node_->audio_playback_cache()->WriteSilence(r); } } watcher->deleteLater(); } void RenderBackend::WorkerFinished() { static_cast(sender())->SetAvailable(true); } bool RenderBackend::ConformWaitInfo::operator==(const RenderBackend::ConformWaitInfo &rhs) const { return rhs.stream == stream && rhs.stream_time == stream_time && rhs.affected_range == affected_range; } void RenderBackend::RenderPool::Init(ViewerOutput* v) { foreach (RenderWorker* instance, instances) { instance->Init(v); } } void RenderBackend::RenderPool::Queue(NodeInput *input) { foreach (RenderWorker* worker, instances) { worker->Queue(input); } } void RenderBackend::RenderPool::Destroy() { qDeleteAll(instances); instances.clear(); } void RenderBackend::RenderPool::Close() { foreach (RenderWorker* worker, instances) { worker->Close(); } } OLIVE_NAMESPACE_EXIT