From 677277c16d51bcd9aa6073fc71fa8fe4bdbc720e Mon Sep 17 00:00:00 2001 From: itsmattkc <34096995+itsmattkc@users.noreply.github.com> Date: Tue, 21 Sep 2021 19:10:03 -0700 Subject: [PATCH] renderer: improved code flow --- app/codec/decoder.cpp | 11 +- app/codec/decoder.h | 4 +- app/codec/ffmpeg/ffmpegdecoder.cpp | 10 +- app/codec/ffmpeg/ffmpegdecoder.h | 4 +- app/codec/oiio/oiiodecoder.cpp | 3 +- app/codec/oiio/oiiodecoder.h | 2 +- app/common/timerange.cpp | 7 + app/common/timerange.h | 11 + app/node/traverser.cpp | 5 + app/node/traverser.h | 21 +- app/render/previewautocacher.cpp | 522 ++++++++++++-------------- app/render/previewautocacher.h | 40 +- app/render/renderprocessor.cpp | 76 ++-- app/threading/threadticket.h | 3 +- app/threading/threadticketwatcher.cpp | 7 + app/threading/threadticketwatcher.h | 2 + app/widget/viewer/viewer.cpp | 18 +- 17 files changed, 401 insertions(+), 345 deletions(-) diff --git a/app/codec/decoder.cpp b/app/codec/decoder.cpp index cac7d553f..2b8aee6db 100644 --- a/app/codec/decoder.cpp +++ b/app/codec/decoder.cpp @@ -85,7 +85,7 @@ bool Decoder::Open(const CodecStream &stream) } } -FramePtr Decoder::RetrieveVideo(const rational &timecode, const RetrieveVideoParams ÷r) +FramePtr Decoder::RetrieveVideo(const rational &timecode, const RetrieveVideoParams ÷r, const QAtomicInt *cancelled) { QMutexLocker locker(&mutex_); @@ -101,7 +101,11 @@ FramePtr Decoder::RetrieveVideo(const rational &timecode, const RetrieveVideoPar return nullptr; } - return RetrieveVideoInternal(timecode, divider); + if (cancelled && *cancelled) { + return nullptr; + } + + return RetrieveVideoInternal(timecode, divider, cancelled); } Decoder::RetrieveAudioData Decoder::RetrieveAudio(const TimeRange &range, const AudioParams ¶ms, const QString& cache_path, Footage::LoopMode loop_mode, RenderMode::Mode mode) @@ -248,10 +252,11 @@ int64_t Decoder::GetImageSequenceIndex(const QString &filename) return number_only.toLongLong(); } -FramePtr Decoder::RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams ÷r) +FramePtr Decoder::RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams ÷r, const QAtomicInt *cancelled) { Q_UNUSED(timecode) Q_UNUSED(divider) + Q_UNUSED(cancelled) return nullptr; } diff --git a/app/codec/decoder.h b/app/codec/decoder.h index 3b9a64ac9..0de9b4f52 100644 --- a/app/codec/decoder.h +++ b/app/codec/decoder.h @@ -184,7 +184,7 @@ public: * * This function is thread safe and can only run while the decoder is open. \see Open() */ - FramePtr RetrieveVideo(const rational& timecode, const RetrieveVideoParams& divider); + FramePtr RetrieveVideo(const rational& timecode, const RetrieveVideoParams& divider, const QAtomicInt *cancelled = nullptr); enum RetrieveAudioStatus { kInvalid = -1, @@ -284,7 +284,7 @@ protected: * Sub-classes must override this function IF they support video. Function is already mutexed * so sub-classes don't need to worry about thread safety. */ - virtual FramePtr RetrieveVideoInternal(const rational& timecode, const RetrieveVideoParams& divider); + virtual FramePtr RetrieveVideoInternal(const rational& timecode, const RetrieveVideoParams& divider, const QAtomicInt *cancelled); virtual bool ConformAudioInternal(const QString& filename, const AudioParams ¶ms, const QAtomicInt* cancelled); diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index bae2688fa..84a4a3576 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -145,7 +145,7 @@ bool FFmpegDecoder::OpenInternal() return output_frame; }*/ -FramePtr FFmpegDecoder::RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams ¶ms) +FramePtr FFmpegDecoder::RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams ¶ms, const QAtomicInt *cancelled) { if (!InitScaler(params)) { return nullptr; @@ -154,7 +154,7 @@ FramePtr FFmpegDecoder::RetrieveVideoInternal(const rational &timecode, const Re AVStream* s = instance_.avstream(); // Retrieve frame - FFmpegFramePool::ElementPtr return_frame = RetrieveFrame(timecode); + FFmpegFramePool::ElementPtr return_frame = RetrieveFrame(timecode, cancelled); // We found the frame, we'll return a copy if (return_frame) { @@ -661,7 +661,7 @@ void FFmpegDecoder::ClearFrameCache() } } -FFmpegFramePool::ElementPtr FFmpegDecoder::RetrieveFrame(const rational& time) +FFmpegFramePool::ElementPtr FFmpegDecoder::RetrieveFrame(const rational& time, const QAtomicInt *cancelled) { int64_t target_ts = GetTimeInTimebaseUnits(time, instance_.avstream()->time_base, instance_.avstream()->start_time); @@ -704,6 +704,10 @@ FFmpegFramePool::ElementPtr FFmpegDecoder::RetrieveFrame(const rational& time) AVFrame* working_frame = av_frame_alloc(); while (true) { + // Break out of loop if we've cancelled + if (cancelled && *cancelled) { + break; + } // Pull from the decoder av_frame_unref(working_frame); diff --git a/app/codec/ffmpeg/ffmpegdecoder.h b/app/codec/ffmpeg/ffmpegdecoder.h index 1239cc421..be06f9a60 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.h +++ b/app/codec/ffmpeg/ffmpegdecoder.h @@ -63,7 +63,7 @@ public: protected: virtual bool OpenInternal() override; - virtual FramePtr RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams& params) override; + virtual FramePtr RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams& params, const QAtomicInt *cancelled) override; virtual bool ConformAudioInternal(const QString& filename, const AudioParams ¶ms, const QAtomicInt* cancelled) override; virtual void CloseInternal() override; @@ -137,7 +137,7 @@ private: void ClearFrameCache(); - FFmpegFramePool::ElementPtr RetrieveFrame(const rational &time); + FFmpegFramePool::ElementPtr RetrieveFrame(const rational &time, const QAtomicInt *cancelled); void RemoveFirstFrame(); diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index b82a4fae4..dea90b2e6 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -101,9 +101,10 @@ bool OIIODecoder::OpenInternal() return OpenImageHandler(stream().filename()); } -FramePtr OIIODecoder::RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams ÷r) +FramePtr OIIODecoder::RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams ÷r, const QAtomicInt *cancelled) { Q_UNUSED(timecode) + Q_UNUSED(cancelled) FramePtr frame = Frame::Create(); diff --git a/app/codec/oiio/oiiodecoder.h b/app/codec/oiio/oiiodecoder.h index f1b798adf..4232cb2d1 100644 --- a/app/codec/oiio/oiiodecoder.h +++ b/app/codec/oiio/oiiodecoder.h @@ -44,7 +44,7 @@ public: protected: virtual bool OpenInternal() override; - virtual FramePtr RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams& divider) override; + virtual FramePtr RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams& divider, const QAtomicInt *cancelled) override; virtual void CloseInternal() override; private: diff --git a/app/common/timerange.cpp b/app/common/timerange.cpp index 95a740761..91758fc64 100644 --- a/app/common/timerange.cpp +++ b/app/common/timerange.cpp @@ -176,6 +176,13 @@ void TimeRange::normalize() length_ = out_ - in_; } +void TimeRangeList::insert(const TimeRangeList &list_to_add) +{ + for (auto it=list_to_add.cbegin(); it!=list_to_add.cend(); it++) { + insert(*it); + } +} + void TimeRangeList::insert(TimeRange range_to_add) { // See if list contains this range diff --git a/app/common/timerange.h b/app/common/timerange.h index 1b39bf4e9..846818084 100644 --- a/app/common/timerange.h +++ b/app/common/timerange.h @@ -77,6 +77,7 @@ public: { } + void insert(const TimeRangeList &list_to_add); void insert(TimeRange range_to_add); void remove(const TimeRange& remove); @@ -217,6 +218,16 @@ public: *this = TimeRangeListFrameIterator(); } + void insert(const TimeRange &range) + { + list_.insert(range); + } + + void insert(const TimeRangeList &list) + { + list_.insert(list); + } + private: void UpdateIndexIfNecessary(); diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index 17f3d9bb8..15e7e4b5d 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -221,6 +221,11 @@ NodeValueTable NodeTraverser::ProcessInput(const Node* node, const QString& inpu } } +NodeTraverser::NodeTraverser() : + cancel_(nullptr) +{ +} + NodeValueTable NodeTraverser::GenerateTable(const Node *n, const Node::ValueHint &hint, const TimeRange& range) { const Track* track = dynamic_cast(n); diff --git a/app/node/traverser.h b/app/node/traverser.h index 7c831bfce..058afa7ee 100644 --- a/app/node/traverser.h +++ b/app/node/traverser.h @@ -31,10 +31,10 @@ namespace olive { -class NodeTraverser : public CancelableObject +class NodeTraverser { public: - NodeTraverser() = default; + NodeTraverser(); NodeValueTable GenerateTable(const Node *n, const Node::ValueHint &hint, const TimeRange &range); @@ -93,11 +93,28 @@ protected: QVector2D GenerateResolution() const; + bool IsCancelled() const + { + return cancel_ && *cancel_; + } + + const QAtomicInt *GetCancelPointer() const + { + return cancel_; + } + + void SetCancelPointer(const QAtomicInt *cancel) + { + cancel_ = cancel; + } + private: void PostProcessTable(const Node *node, const Node::ValueHint &hint, const TimeRange &range, NodeValueTable &output_params); VideoParams video_params_; + const QAtomicInt *cancel_; + }; } diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index de7a9648d..4fbcbf848 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -17,12 +17,15 @@ PreviewAutoCacher::PreviewAutoCacher() : use_custom_range_(false), single_frame_render_(nullptr) { + // Set defaults SetPlayhead(0); + // Wait a certain amount of time before requeuing when we receive an invalidate signal delayed_requeue_timer_.setInterval(Config::Current()[QStringLiteral("AutoCacheDelay")].toInt()); delayed_requeue_timer_.setSingleShot(true); connect(&delayed_requeue_timer_, &QTimer::timeout, this, &PreviewAutoCacher::RequeueFrames); + // Catch when a conform is ready connect(ConformManager::instance(), &ConformManager::ConformReady, this, &PreviewAutoCacher::ConformFinished); } @@ -34,20 +37,24 @@ PreviewAutoCacher::~PreviewAutoCacher() RenderTicketPtr PreviewAutoCacher::GetSingleFrame(const rational &t, bool prioritize) { + // If we have a single frame render queued (but not yet sent to the RenderManager), cancel it now CancelQueuedSingleFrameRender(); + // See if we have a hash, we may or may not retrieve one depending on the state of the video + // frame cache QByteArray hash; if (!paused_) { hash = viewer_node_->video_frame_cache()->GetHash(t); } + // Create a new single frame render ticket auto sfr = std::make_shared(); sfr->Start(); sfr->setProperty("time", QVariant::fromValue(t)); sfr->setProperty("prioritize", prioritize); sfr->setProperty("hash", hash); - // Attempt to queue + // Queue it and try to render single_frame_render_ = sfr; TryRender(); @@ -93,9 +100,11 @@ QVector PreviewAutoCacher::GenerateHashes(ViewerOut void PreviewAutoCacher::VideoInvalidated(const TimeRange &range) { - ClearVideoQueue(); + // Stop any current render tasks because a) they might be out of date now anyway, and b) we + // want to dedicate all our rendering power to realtime feedback for the user + CancelVideoTasks(); - // Hash these frames since that should be relatively quick. + // If a slider is not being dragged, queue up to hash these frames if (!NumericSliderBase::IsEffectsSliderBeingDragged()) { invalidated_video_.insert(range); video_job_tracker_.insert(range, graph_changed_time_); @@ -106,6 +115,8 @@ void PreviewAutoCacher::VideoInvalidated(const TimeRange &range) void PreviewAutoCacher::AudioInvalidated(const TimeRange &range) { + // We don't stop rendering audio because currently there's no system of requeuing audio if it's + // cancelled, so some areas may end up unrendered forever // ClearAudioQueue(); audio_job_tracker_.insert(range, graph_changed_time_); @@ -118,12 +129,16 @@ void PreviewAutoCacher::AudioInvalidated(const TimeRange &range) void PreviewAutoCacher::HashesProcessed() { + // Receive watcher QFutureWatcher< QVector >* watcher = static_cast >*>(sender()); + // If the task list doesn't contain this watcher, presumably it was cleared as a result of a + // viewer switch, so we'll completely ignore this watcher if (hash_tasks_.contains(watcher)) { + // Remove task from hash task list hash_tasks_.removeOne(watcher); - // Set all hashes we received + // Set all hashes we received that are still current JobTime job_time = watcher->property("job").value(); auto hashes = watcher->result(); foreach (auto hash, hashes) { @@ -132,101 +147,92 @@ void PreviewAutoCacher::HashesProcessed() } } + // HACK: When viewer is first set, there's nothing to requeue the range unless the user + // manually moves the playhead, so we ensure a requeue is done here. if (!hash_iterator_.HasNext()) { - // Restart delayed requeue timer delayed_requeue_timer_.stop(); delayed_requeue_timer_.start(); } - } - // The cacher might be waiting for this job to finish - if (!graph_update_queue_.isEmpty()) { + // Continue rendering TryRender(); } - if (hash_iterator_.HasNext()) { - // Launch next hashes - QueueNextHashTask(); - } - delete watcher; } void PreviewAutoCacher::AudioRendered() { + // Receive watcher RenderTicketWatcher* watcher = static_cast(sender()); + // If the task list doesn't contain this watcher, presumably it was cleared as a result of a + // viewer switch, so we'll completely ignore this watcher if (audio_tasks_.contains(watcher)) { + // Assume that a "result" is a fully completed image and a non-result is a cancelled ticket + TimeRange range = audio_tasks_.take(watcher); + if (watcher->HasResult()) { - const TimeRange &range = audio_tasks_.value(watcher); + // Remove this task from the list JobTime watcher_job_time = watcher->property("job").value(); TimeRangeList valid_ranges = audio_job_tracker_.getCurrentSubRanges(range, watcher_job_time); AudioVisualWaveform waveform = watcher->GetTicket()->property("waveform").value(); + // WritePCM is tolerant to its buffer being null, it will just write silence instead viewer_node_->audio_playback_cache()->WritePCM(range, valid_ranges, watcher->Get().value(), &waveform); - bool pcm_is_usable = true; - + // Detect if this audio was incomplete because it was waiting on a conform to finish if (watcher->GetTicket()->property("incomplete").toBool()) { if (last_conform_task_ > watcher_job_time) { // Requeue now viewer_node_->audio_playback_cache()->Invalidate(range); - pcm_is_usable = false; } else { // Wait for conform audio_needing_conform_.insert(range); } } - if (pcm_is_usable) { - // Retrieve visual waveforms - QVector waveform_list = watcher->GetTicket()->property("waveforms").value< QVector >(); - foreach (const RenderProcessor::RenderedWaveform& waveform_info, waveform_list) { - // Find original track - ClipBlock* block = nullptr; + // Retrieve visual waveforms + QVector waveform_list = watcher->GetTicket()->property("waveforms").value< QVector >(); + foreach (const RenderProcessor::RenderedWaveform& waveform_info, waveform_list) { + // Find original track + ClipBlock* block = nullptr; - for (auto it=copy_map_.cbegin(); it!=copy_map_.cend(); it++) { - if (it.value() == waveform_info.block) { - block = static_cast(it.key()); - break; + for (auto it=copy_map_.cbegin(); it!=copy_map_.cend(); it++) { + if (it.value() == waveform_info.block) { + block = static_cast(it.key()); + break; + } + } + + if (block && !valid_ranges.isEmpty()) { + // Generate visual waveform in this background thread + block->waveform().set_channel_count(viewer_node_->GetAudioParams().channel_count()); + + // Determine which of the waveform ranges we got intersects with the valid ranges + TimeRangeList intersections = valid_ranges.Intersects(waveform_info.range + block->in()); + foreach (TimeRange r, intersections) { + // For each range, adjust it relative to the block and write it + r -= block->in(); + + if (waveform_info.silence) { + block->waveform().OverwriteSilence(r.in(), r.length()); + } else { + block->waveform().OverwriteSums(waveform_info.waveform, r.in(), r.in() - waveform_info.range.in(), r.length()); } } - if (block && !valid_ranges.isEmpty()) { - // Generate visual waveform in this background thread - block->waveform().set_channel_count(viewer_node_->GetAudioParams().channel_count()); - - // Determine which of the waveform ranges we got intersects with the valid ranges - TimeRangeList intersections = valid_ranges.Intersects(waveform_info.range + block->in()); - foreach (TimeRange r, intersections) { - // For each range, adjust it relative to the block and write it - r -= block->in(); - - if (waveform_info.silence) { - block->waveform().OverwriteSilence(r.in(), r.length()); - } else { - block->waveform().OverwriteSums(waveform_info.waveform, r.in(), r.in() - waveform_info.range.in(), r.length()); - } - } - - emit block->PreviewChanged(); - } + emit block->PreviewChanged(); } } } - audio_tasks_.remove(watcher); - } - - // The cacher might be waiting for this job to finish - if (graph_update_queue_.isEmpty()) { - QueueNextAudioTask(); - } else { + // Continue rendering TryRender(); } @@ -237,10 +243,13 @@ void PreviewAutoCacher::VideoRendered() { RenderTicketWatcher* watcher = static_cast(sender()); + // If the task list doesn't contain this watcher, presumably it was cleared as a result of a + // viewer switch, so we'll completely ignore this watcher if (video_tasks_.contains(watcher)) { - if (watcher->HasResult()) { - const QByteArray& hash = video_tasks_.value(watcher); + // Assume that a "result" is a fully completed image and a non-result is a cancelled ticket + QByteArray hash = video_tasks_.take(watcher); + if (watcher->HasResult()) { // Download frame in another thread if (!hash.isEmpty() && VideoParams::FormatIsFloat(viewer_node_->GetVideoParams().format())) { FramePtr frame = watcher->Get().value(); @@ -256,10 +265,12 @@ void PreviewAutoCacher::VideoRendered() } } - video_tasks_.remove(watcher); + // Continue rendering + TryRender(); } - // Process passthroughs + // Process passthroughs no matter what, if the viewer was switched, the passthrough map would be + // cleared anyway QVector tickets = video_immediate_passthroughs_.take(watcher); foreach (RenderTicketPtr t, tickets) { if (watcher->HasResult()) { @@ -269,13 +280,6 @@ void PreviewAutoCacher::VideoRendered() } } - // The cacher might be waiting for this job to finish - if (!graph_update_queue_.isEmpty()) { - TryRender(); - } - - QueueNextFrameInRange(1); - delete watcher; } @@ -283,23 +287,21 @@ void PreviewAutoCacher::VideoDownloaded() { RenderTicketWatcher* watcher = static_cast(sender()); + // If the task list doesn't contain this watcher, presumably it was cleared as a result of a + // viewer switch, so we'll completely ignore this watcher if (video_download_tasks_.contains(watcher)) { - if (watcher->HasResult()) { - if (watcher->Get().toBool()) { - const QByteArray& hash = video_download_tasks_.value(watcher); + // Remove from task list + QByteArray hash = video_download_tasks_.take(watcher); - viewer_node_->video_frame_cache()->ValidateFramesWithHash(hash); - } else { - qCritical() << "Failed to download video frame"; - } + // Assume that `true` is a completely successful frame save + if (watcher->Get().toBool()) { + viewer_node_->video_frame_cache()->ValidateFramesWithHash(hash); + } else { + qCritical() << "Failed to download video frame"; } - video_download_tasks_.remove(watcher); - } - - // The cacher might be waiting for this job to finish - if (!graph_update_queue_.isEmpty()) { - TryRender(); + // No need to call TryRender here because it would not have been held up by a download task + // nor does the completion of this ticket automatically trigger another ticket } delete watcher; @@ -307,6 +309,7 @@ void PreviewAutoCacher::VideoDownloaded() void PreviewAutoCacher::ProcessUpdateQueue() { + // Iterate everything that happened to the graph and do the same thing on our end foreach (const QueuedJob& job, graph_update_queue_) { switch (job.type) { case QueuedJob::kNodeAdded: @@ -331,16 +334,11 @@ void PreviewAutoCacher::ProcessUpdateQueue() } graph_update_queue_.clear(); + // Indicate that we have synchronized to this point, which is compared with the graph change + // time to see if our copied graph is up to date UpdateLastSyncedValue(); } -bool PreviewAutoCacher::HasActiveJobs() const -{ - return !hash_tasks_.isEmpty() - || !audio_tasks_.isEmpty() - || !video_tasks_.isEmpty(); -} - void PreviewAutoCacher::AddNode(Node *node) { // Copy node @@ -370,6 +368,7 @@ void PreviewAutoCacher::RemoveNode(Node *node) void PreviewAutoCacher::AddEdge(Node *output, const NodeInput &input) { + // Create same connection with our copied graph Node* our_output = copy_map_.value(output); Node* our_input = copy_map_.value(input.node()); @@ -378,6 +377,7 @@ void PreviewAutoCacher::AddEdge(Node *output, const NodeInput &input) void PreviewAutoCacher::RemoveEdge(Node *output, const NodeInput &input) { + // Remove same connection with our copied graph Node* our_output = copy_map_.value(output); Node* our_input = copy_map_.value(input.node()); @@ -386,12 +386,14 @@ void PreviewAutoCacher::RemoveEdge(Node *output, const NodeInput &input) void PreviewAutoCacher::CopyValue(const NodeInput &input) { + // Copy all values to our graph Node* our_input = copy_map_.value(input.node()); Node::CopyValuesOfElement(input.node(), our_input, input.input(), input.element()); } void PreviewAutoCacher::CopyValueHint(const NodeInput &input) { + // Copy value hint to our graph Node* our_input = copy_map_.value(input.node()); Node::ValueHint hint = input.node()->GetValueHintForInput(input.input(), input.element()); our_input->SetValueHintForInput(input.input(), hint, input.element()); @@ -425,6 +427,20 @@ void PreviewAutoCacher::CancelQueuedSingleFrameRender() } } +void PreviewAutoCacher::VideoInvalidatedList(const TimeRangeList &list) +{ + foreach (const TimeRange &range, list) { + VideoInvalidated(range); + } +} + +void PreviewAutoCacher::AudioInvalidatedList(const TimeRangeList &list) +{ + foreach (const TimeRange &range, list) { + AudioInvalidated(range); + } +} + void PreviewAutoCacher::SetPlayhead(const rational &playhead) { cache_range_ = TimeRange(playhead - Config::Current()[QStringLiteral("DiskCacheBehind")].value(), @@ -435,41 +451,44 @@ void PreviewAutoCacher::SetPlayhead(const rational &playhead) RequeueFrames(); } -void PreviewAutoCacher::ClearHashQueue(bool wait) +void PreviewAutoCacher::WaitForHashesToFinish() { - auto copy = hash_tasks_; - - for (auto it=copy.cbegin(); it!=copy.cend(); it++) { - (*it)->cancel(); + for (auto it=hash_tasks_.cbegin(); it!=hash_tasks_.cend(); it++) { + (*it)->waitForFinished(); } - if (wait) { - copy = hash_tasks_; - for (auto it=copy.cbegin(); it!=copy.cend(); it++) { - (*it)->waitForFinished(); +} + +void PreviewAutoCacher::WaitForVideoDownloadsToFinish() +{ + for (auto it=video_download_tasks_.cbegin(); it!=video_download_tasks_.cend(); it++) { + it.key()->WaitForFinished(); + } +} + +template +void CancelTasks(const T &task_list, bool and_wait) +{ + for (auto it=task_list.cbegin(); it!=task_list.cend(); it++) { + // Signal that the ticket should not be finished + it.key()->Cancel(); + } + + if (and_wait) { + // Wait for each ticket to finish + for (auto it=task_list.cbegin(); it!=task_list.cend(); it++) { + it.key()->WaitForFinished(); } - - hash_tasks_.clear(); } } -void PreviewAutoCacher::ClearVideoQueue(bool hard) +void PreviewAutoCacher::CancelVideoTasks(bool and_wait_for_them_to_finish) { - ClearQueueInternal(video_tasks_, hard, &PreviewAutoCacher::VideoRendered); - - use_custom_range_ = false; - queued_frame_iterator_.reset(); + CancelTasks(video_tasks_, and_wait_for_them_to_finish); } -void PreviewAutoCacher::ClearAudioQueue(bool hard) +void PreviewAutoCacher::CancelAudioTasks(bool and_wait_for_them_to_finish) { - ClearQueueInternal(audio_tasks_, hard, &PreviewAutoCacher::AudioRendered); - - audio_iterator_.clear(); -} - -void PreviewAutoCacher::ClearVideoDownloadQueue(bool hard) -{ - ClearQueueInternal(video_download_tasks_, hard, &PreviewAutoCacher::VideoDownloaded); + CancelTasks(audio_tasks_, and_wait_for_them_to_finish); } void PreviewAutoCacher::NodeAdded(Node *node) @@ -511,8 +530,12 @@ void PreviewAutoCacher::ValueHintChanged(const NodeInput &input) void PreviewAutoCacher::TryRender() { if (!graph_update_queue_.isEmpty()) { - if (HasActiveJobs()) { - // Still waiting for jobs to finish + // Check if we have jobs running in other threads that shouldn't be interrupted right now + // NOTE: We don't check for downloads because, while they run in another thread, they don't + // require any access to the graph and therefore don't risk race conditions. + if (!hash_tasks_.isEmpty() + || !audio_tasks_.isEmpty() + || !video_tasks_.isEmpty()) { return; } @@ -520,24 +543,19 @@ void PreviewAutoCacher::TryRender() ProcessUpdateQueue(); } - // If we're here, we must be able to render + // Check for newly invalidated video and hash it if (!invalidated_video_.isEmpty()) { - hash_iterator_ = TimeRangeListFrameIterator(invalidated_video_, viewer_node_->GetVideoParams().frame_rate_as_time_base()); - - for (int i=0; iGetVideoParams().frame_rate_as_time_base()); } - invalidated_video_.clear(); } if (!invalidated_audio_.isEmpty()) { - audio_iterator_ = invalidated_audio_; - - for (int i=0; i times(kMaxFrames); + for (int i=0; i >* watcher = new QFutureWatcher< QVector >(); + watcher->setProperty("job", QVariant::fromValue(last_update_time_)); + hash_tasks_.append(watcher); + connect(watcher, &QFutureWatcher< QVector >::finished, this, &PreviewAutoCacher::HashesProcessed); + watcher->setFuture(QtConcurrent::run(PreviewAutoCacher::GenerateHashes, + copied_viewer_node_, + viewer_node_->video_frame_cache(), + times)); + } + + // Handle video tasks + rational t; + while (video_tasks_.size() < max_tasks && queued_frame_iterator_.GetNext(&t)) { + const QByteArray& hash = viewer_node_->video_frame_cache()->GetHash(t); + + RenderTicketWatcher* render_task = video_tasks_.key(hash); + + // We want this hash, if we're not already rendering, start render now + if (!render_task && !video_download_tasks_.key(hash)) { + // Don't render any hash more than once + RenderFrame(hash, t, false, false); + } + } + + // Handle audio tasks + while (!audio_iterator_.isEmpty() && audio_tasks_.size() < max_tasks) { + // Copy first range in list + TimeRange r = audio_iterator_.first(); + + // Limit to 30 seconds (FIXME: Hardcoded) + r.set_out(qMin(r.out(), r.in() + 30)); + + // Start job + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + watcher->setProperty("job", QVariant::fromValue(last_update_time_)); + connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); + audio_tasks_.insert(watcher, r); + watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, true)); + + audio_iterator_.remove(r); + } } RenderTicketWatcher* PreviewAutoCacher::RenderFrame(const QByteArray &hash, const rational& time, bool prioritize, bool texture_only) @@ -600,15 +679,17 @@ void PreviewAutoCacher::RequeueFrames() TimeRangeList invalidated = viewer_node_->video_frame_cache()->GetInvalidatedRanges(using_range); queued_frame_iterator_ = TimeRangeListFrameIterator(invalidated, viewer_node_->video_frame_cache()->GetTimebase()); - QueueNextFrameInRange(RenderManager::GetNumberOfIdealConcurrentJobs()); + TryRender(); } } void PreviewAutoCacher::ConformFinished() { + // Got an audio conform, requeue all the audio currently needing a conform last_conform_task_.Acquire(); - if (viewer_node_) { + if (!audio_needing_conform_.isEmpty()) { + // This list should be empty if there was a viewer switch foreach (const TimeRange &range, audio_needing_conform_) { viewer_node_->audio_playback_cache()->Invalidate(range); } @@ -631,29 +712,54 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) } if (viewer_node_) { - // Cancel any remaining tickets and wait for them to finish + // We must wait for any jobs to finish because they'll be using our copied graph and we're + // about to destroy it - // We need to wait for these since they send signals directly to the FrameHashCache - // which might get deleted after this function. - ClearHashQueue(true); + // Stop requeue timer if it's running + delayed_requeue_timer_.stop(); - // This can be cleared normally (frames will be discarded and need to be rendered again) - ClearVideoQueue(true); + // Handle hashes + if (!hash_tasks_.isEmpty()) { + // Wait for hashes to finish + WaitForHashesToFinish(); - // This can be cleared normally (PCM data will be discarded and need to be rendered again) - ClearAudioQueue(true); + // Clear the hash list to indicate we're not interested in the results of any of these + hash_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. - ClearVideoDownloadQueue(true); + // Handle video rendering tasks + if (!video_tasks_.isEmpty()) { + // Cancel any video tasks and wait for them to finish + CancelVideoTasks(true); + } + + // Handle audio rendering tasks + if (!audio_tasks_.isEmpty()) { + // Cancel any audio tasks and wait for them to finish + CancelAudioTasks(true); + } + + // Handle video download tasks + if (!video_download_tasks_.isEmpty()) { + WaitForVideoDownloadsToFinish(); + } + + // Clear iterators + queued_frame_iterator_.reset(); + audio_iterator_.clear(); + hash_iterator_.reset(); + + // Clear any invalidated ranges + invalidated_video_.clear(); + invalidated_audio_.clear(); // Clear any single frame render that might be queued CancelQueuedSingleFrameRender(); - // No more immediate passthroughts + // Not interested in video passthroughs anymore video_immediate_passthroughs_.clear(); - // No more audio conforms + // Not interested in audio conforming anymore audio_needing_conform_.clear(); // Delete all of our copied nodes @@ -726,12 +832,6 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) connect(graph, &NodeGraph::ValueChanged, this, &PreviewAutoCacher::ValueChanged); connect(graph, &NodeGraph::InputValueHintChanged, this, &PreviewAutoCacher::ValueHintChanged); - // Copy invalidated ranges - used to determine which frames need hashing - invalidated_video_ = viewer_node_->video_frame_cache()->GetInvalidatedRanges(viewer_node_->GetVideoLength()); - video_job_tracker_.insert(invalidated_video_, graph_changed_time_); - invalidated_audio_ = viewer_node_->audio_playback_cache()->GetInvalidatedRanges(viewer_node_->GetAudioLength()); - audio_job_tracker_.insert(invalidated_audio_, graph_changed_time_); - connect(viewer_node_->video_frame_cache(), &PlaybackCache::Invalidated, this, @@ -742,139 +842,11 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) this, &PreviewAutoCacher::AudioInvalidated); + // Copy invalidated ranges and start rendering if necessary + VideoInvalidatedList(viewer_node_->video_frame_cache()->GetInvalidatedRanges(viewer_node_->GetVideoLength())); + AudioInvalidatedList(viewer_node_->audio_playback_cache()->GetInvalidatedRanges(viewer_node_->GetAudioLength())); TryRender(); } } -RenderTicketWatcher* RetrieveFromQueueIterator(QMap::iterator it) -{ - return it.key(); -} - -RenderTicketWatcher* RetrieveFromQueueIterator(QMap::iterator it) -{ - return it.key(); -} - -RenderTicketWatcher* RetrieveFromQueueIterator(QVector::iterator it) -{ - return *it; -} - -void PreviewAutoCacher::ClearQueueRemoveEventInternal(QMap::iterator it) -{ - Q_UNUSED(it) -} - -void PreviewAutoCacher::ClearQueueRemoveEventInternal(QMap::iterator it) -{ - Q_UNUSED(it) -} - -void PreviewAutoCacher::ClearQueueRemoveEventInternal(QVector::iterator it) -{ - Q_UNUSED(it) -} - -void PreviewAutoCacher::QueueNextFrameInRange(int max) -{ - rational t; - while (max && queued_frame_iterator_.GetNext(&t)) { - const QByteArray& hash = viewer_node_->video_frame_cache()->GetHash(t); - - RenderTicketWatcher* render_task = video_tasks_.key(hash); - - // We want this hash, if we're not already rendering, start render now - if (!render_task && !video_download_tasks_.key(hash)) { - // Don't render any hash more than once - RenderFrame(hash, t, false, false); - - max--; - } - } -} - -void PreviewAutoCacher::QueueNextHashTask() -{ - // Magic number: dunno what the best number for this is yet - static const int kMaxFrames = 1000; - - QVector times(kMaxFrames); - for (int i=0; i >* watcher = new QFutureWatcher< QVector >(); - watcher->setProperty("job", QVariant::fromValue(last_update_time_)); - hash_tasks_.append(watcher); - connect(watcher, &QFutureWatcher< QVector >::finished, this, &PreviewAutoCacher::HashesProcessed); - watcher->setFuture(QtConcurrent::run(PreviewAutoCacher::GenerateHashes, - copied_viewer_node_, - viewer_node_->video_frame_cache(), - times)); -} - -void PreviewAutoCacher::QueueNextAudioTask() -{ - if (!audio_iterator_.isEmpty()) { - // Copy first range in list - TimeRange r = audio_iterator_.first(); - - // Limit to 30 seconds (FIXME: Hardcoded) - r.set_out(qMin(r.out(), r.in() + 30)); - - // Start job - RenderTicketWatcher* watcher = new RenderTicketWatcher(); - watcher->setProperty("job", QVariant::fromValue(last_update_time_)); - connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); - audio_tasks_.insert(watcher, r); - watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, true)); - - audio_iterator_.remove(r); - } -} - -template -void PreviewAutoCacher::ClearQueueInternal(T& list, bool hard, Func member) -{ - for (auto it=list.begin(); it!=list.end(); ) { - RenderTicketWatcher* ticket = RetrieveFromQueueIterator(it); - - QMutexLocker locker(ticket->GetTicket()->lock()); - - bool ticket_is_running = ticket->GetTicket()->IsRunning(false); - - if (hard || !ticket_is_running) { - // Override default signalling - disconnect(ticket, &RenderTicketWatcher::Finished, this, member); - - if (ticket_is_running) { - // If ticket is running, assume we're hard clearing and wait for the ticket to finish - ticket->GetTicket()->WaitForFinished(ticket->GetTicket()->lock()); - } else { - // Just remove the ticket from the queue - RenderManager::instance()->RemoveTicket(ticket->GetTicket()); - } - - // Special functionality for certain queues - ClearQueueRemoveEventInternal(it); - - // Destroy ticket - locker.unlock(); - delete ticket; - - it = list.erase(it); - } else { - // We can't clear this, probably because we're soft clearing and the ticket is currently running - it++; - } - } -} - } diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index ebe12fe7b..1ff5083aa 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -65,10 +65,28 @@ public: */ void SetPlayhead(const rational& playhead); - void ClearHashQueue(bool wait = false); - void ClearVideoQueue(bool wait = false); - void ClearAudioQueue(bool wait = false); - void ClearVideoDownloadQueue(bool wait = false); + /** + * @brief If any hashes are currently running, wait for them to finish + * + * Once this function returns, it can be guaranteed that all hash tasks have been finished. + * They will NOT have been removed from the hash task list yet until they run HashesProcessed. + * If you don't want the continued processing in HashesProcessed to run, remove the task manually + * from the list after calling this function. It will still call HashesProcessed, but will be + * largely ignored (that function will simply free it). + */ + void WaitForHashesToFinish(); + void WaitForVideoDownloadsToFinish(); + + /** + * @brief Call cancel on all currently running video tasks + * + * Signalling cancel to a video task indicates that we're no longer interested in its end result. + * This does not end all video tasks immediately, the RenderManager will do what it can to speed + * up finishing the task. The RenderManager will also return "no result", which can be checked + * with watcher->HasResult. + */ + void CancelVideoTasks(bool and_wait_for_them_to_finish = false); + void CancelAudioTasks(bool and_wait_for_them_to_finish = false); private: void TryRender(); @@ -83,8 +101,6 @@ private: */ void ProcessUpdateQueue(); - bool HasActiveJobs() const; - void AddNode(Node* node); void RemoveNode(Node* node); void AddEdge(Node *output, const NodeInput& input); @@ -99,16 +115,8 @@ private: void CancelQueuedSingleFrameRender(); - template - void ClearQueueInternal(T& list, bool hard, Func member); - - void ClearQueueRemoveEventInternal(QMap::iterator it); - void ClearQueueRemoveEventInternal(QMap::iterator it); - void ClearQueueRemoveEventInternal(QVector::iterator it); - - void QueueNextFrameInRange(int max); - void QueueNextHashTask(); - void QueueNextAudioTask(); + void VideoInvalidatedList(const TimeRangeList &list); + void AudioInvalidatedList(const TimeRangeList &list); struct HashData { rational time; diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 757e581e6..f9f519ad3 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -121,6 +121,8 @@ void RenderProcessor::Run() // Depending on the render ticket type, start a job RenderManager::TicketType type = ticket_->property("type").value(); + SetCancelPointer(&ticket_->IsCancelled()); + switch (type) { case RenderManager::kTypeVideo: { @@ -146,7 +148,11 @@ void RenderProcessor::Run() texture = render_ctx_->InterlaceTexture(top, bottom, GetCacheVideoParams()); } - if (ticket_->property("textureonly").toBool()) { + if (ticket_->IsCancelled()) { + // Finish cancelled ticket with nothing since we can't guarantee the frame we generated + // is actually "complete + ticket_->Finish(); + } else if (ticket_->property("textureonly").toBool()) { // Return GPU texture if (!texture) { texture = render_ctx_->CreateTexture(GetCacheVideoParams()); @@ -182,7 +188,11 @@ void RenderProcessor::Run() ticket_->setProperty("waveform", QVariant::fromValue(vis)); } - ticket_->Finish(sample_variant); + if (ticket_->IsCancelled()) { + ticket_->Finish(); + } else { + ticket_->Finish(sample_variant); + } break; } case RenderManager::kTypeVideoDownload: @@ -386,41 +396,43 @@ TexturePtr RenderProcessor::ProcessVideoFootage(const FootageJob &stream, const p.src_interlacing = stream_data.interlacing(); p.dst_interlacing = GetCacheVideoParams().interlacing(); - FramePtr frame = decoder->RetrieveVideo((stream_data.video_type() == VideoParams::kVideoTypeVideo) ? input_time : Decoder::kAnyTimecode, p); + if (!IsCancelled()) { + FramePtr frame = decoder->RetrieveVideo((stream_data.video_type() == VideoParams::kVideoTypeVideo) ? input_time : Decoder::kAnyTimecode, p, GetCancelPointer()); - if (frame) { - // Return a texture from the derived class - TexturePtr unmanaged_texture = render_ctx_->CreateTexture(frame->video_params(), - frame->data(), - frame->linesize_pixels()); + if (frame) { + // Return a texture from the derived class + TexturePtr unmanaged_texture = render_ctx_->CreateTexture(frame->video_params(), + frame->data(), + frame->linesize_pixels()); - // We convert to our rendering pixel format, since that will always be float-based which - // is necessary for correct color conversion - VideoParams managed_params = frame->video_params(); - managed_params.set_format(render_params.format()); - managed_params.set_pixel_aspect_ratio(stream_data.pixel_aspect_ratio()); - managed_params.set_interlacing(stream_data.interlacing()); - TexturePtr value = render_ctx_->CreateTexture(managed_params); + // We convert to our rendering pixel format, since that will always be float-based which + // is necessary for correct color conversion + VideoParams managed_params = frame->video_params(); + managed_params.set_format(render_params.format()); + managed_params.set_pixel_aspect_ratio(stream_data.pixel_aspect_ratio()); + managed_params.set_interlacing(stream_data.interlacing()); + TexturePtr value = render_ctx_->CreateTexture(managed_params); - ColorProcessorPtr processor = ColorProcessor::Create(color_manager, - using_colorspace, - color_manager->GetReferenceColorSpace()); + ColorProcessorPtr processor = ColorProcessor::Create(color_manager, + using_colorspace, + color_manager->GetReferenceColorSpace()); - Renderer::AlphaAssociated alpha_assoc; - if (stream_data.channel_count() != VideoParams::kRGBAChannelCount - || stream_data.colorspace() == color_manager->GetReferenceColorSpace()) { - alpha_assoc = Renderer::kAlphaNone; - } else if (stream_data.premultiplied_alpha()) { - alpha_assoc = Renderer::kAlphaAssociated; - } else { - alpha_assoc = Renderer::kAlphaUnassociated; + Renderer::AlphaAssociated alpha_assoc; + if (stream_data.channel_count() != VideoParams::kRGBAChannelCount + || stream_data.colorspace() == color_manager->GetReferenceColorSpace()) { + alpha_assoc = Renderer::kAlphaNone; + } else if (stream_data.premultiplied_alpha()) { + alpha_assoc = Renderer::kAlphaAssociated; + } else { + alpha_assoc = Renderer::kAlphaUnassociated; + } + + render_ctx_->BlitColorManaged(processor, unmanaged_texture, + alpha_assoc, + value.get()); + + return value; } - - render_ctx_->BlitColorManaged(processor, unmanaged_texture, - alpha_assoc, - value.get()); - - return value; } } diff --git a/app/threading/threadticket.h b/app/threading/threadticket.h index d4035c832..bb8629f06 100644 --- a/app/threading/threadticket.h +++ b/app/threading/threadticket.h @@ -27,12 +27,13 @@ #include "codec/frame.h" #include "codec/samplebuffer.h" +#include "common/cancelableobject.h" #include "common/timerange.h" #include "node/output/viewer/viewer.h" namespace olive { -class RenderTicket : public QObject +class RenderTicket : public QObject, public CancelableObject { Q_OBJECT public: diff --git a/app/threading/threadticketwatcher.cpp b/app/threading/threadticketwatcher.cpp index 495f45721..891c7eff1 100644 --- a/app/threading/threadticketwatcher.cpp +++ b/app/threading/threadticketwatcher.cpp @@ -88,6 +88,13 @@ bool RenderTicketWatcher::HasResult() } } +void RenderTicketWatcher::Cancel() +{ + if (ticket_) { + ticket_->Cancel(); + } +} + void RenderTicketWatcher::TicketFinished() { emit Finished(this); diff --git a/app/threading/threadticketwatcher.h b/app/threading/threadticketwatcher.h index 993b56ab3..eac476835 100644 --- a/app/threading/threadticketwatcher.h +++ b/app/threading/threadticketwatcher.h @@ -46,6 +46,8 @@ public: bool HasResult(); + void Cancel(); + signals: void Finished(RenderTicketWatcher* watcher); diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index dd6f68588..cb39a802c 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -420,11 +420,7 @@ void ViewerWidget::UpdateAutoCacher() rational time = GetTime(); if (GetConnectedNode() // Ensure valid node - && cache_time_ != time // Ensure cache hasn't already been to this time - && !auto_cacher_.IsPaused()) { // Follow cache setting - if (!IsPlaying()) { - ClearAutoCacherQueue(); - } + && cache_time_ != time) { // Ensure cache hasn't already been to this time auto_cacher_.SetPlayhead(time); cache_time_ = time; } @@ -432,7 +428,7 @@ void ViewerWidget::UpdateAutoCacher() void ViewerWidget::ClearAutoCacherQueue() { - auto_cacher_.ClearVideoQueue(); + auto_cacher_.CancelVideoTasks(); cache_time_ = rational::NaN; } @@ -522,6 +518,10 @@ void ViewerWidget::UpdateTextureFromNode() watcher->setProperty("time", QVariant::fromValue(time)); connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::RendererGeneratedFrame); nonqueue_watchers_.append(watcher); + + // Clear queue because we want this frame more than any others + ClearAutoCacherQueue(); + watcher->SetTicket(GetFrame(time, true)); } } else { @@ -851,12 +851,16 @@ void ViewerWidget::RendererGeneratedFrame() if (ticket->HasResult()) { if (nonqueue_watchers_.contains(ticket)) { while (!nonqueue_watchers_.isEmpty()) { + // Pop frames that are "old" if (nonqueue_watchers_.takeFirst() == ticket) { break; } } - SetDisplayImage(ticket->Get()); + if (nonqueue_watchers_.isEmpty()) { + // Only set frame if we're not waiting on any others + SetDisplayImage(ticket->Get()); + } } }