From 53a2956350b1119cebfb7d5c24eb9c773a7891ba Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 15 May 2020 18:36:02 +1000 Subject: [PATCH] cache: added cache task that can be manually invoked --- app/node/output/viewer/viewer.cpp | 2 + app/render/framehashcache.cpp | 9 +- app/render/framehashcache.h | 1 + app/render/playbackcache.h | 8 +- app/task/CMakeLists.txt | 1 + app/task/cache/CMakeLists.txt | 22 ++++ app/task/cache/cache.cpp | 174 +++++++++++++++++++++++++++++ app/task/cache/cache.h | 49 ++++++++ app/widget/viewer/viewer.cpp | 125 +++++++++++---------- app/widget/viewer/viewer.h | 8 +- app/widget/viewer/viewerqueue.h | 8 -- app/widget/viewer/viewerwindow.cpp | 2 - app/window/mainwindow/mainmenu.cpp | 13 ++- 13 files changed, 346 insertions(+), 76 deletions(-) create mode 100644 app/task/cache/CMakeLists.txt create mode 100644 app/task/cache/cache.cpp create mode 100644 app/task/cache/cache.h diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index fa5c8c1df..a13650fee 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -106,6 +106,8 @@ void ViewerOutput::set_video_params(const VideoParams &video) { video_params_ = video; + video_frame_cache_.SetTimebase(video_params_.time_base()); + emit SizeChanged(video_params_.width(), video_params_.height()); emit TimebaseChanged(video_params_.time_base()); emit ParamsChanged(); diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index 070a53094..e83dd6169 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -116,6 +116,11 @@ void FrameHashCache::SaveCacheFrame(const QByteArray& hash, } } +void FrameHashCache::SaveCacheFrame(const QByteArray &hash, FramePtr frame) +{ + SaveCacheFrame(hash, frame->data(), frame->video_params()); +} + void FrameHashCache::LengthChangedEvent(const rational &old, const rational &newlen) { if (newlen < old) { @@ -174,8 +179,8 @@ bool FrameHashCache::SaveCacheFrame(const QString &filename, char *data, const V // Attempt to keep this write to one thread out->threads(1); - out->open(fn_std, OIIO::ImageSpec(vparam.width(), - vparam.height(), + out->open(fn_std, OIIO::ImageSpec(vparam.effective_width(), + vparam.effective_height(), PixelFormat::ChannelCount(vparam.format()), PixelFormat::GetOIIOTypeDesc(vparam.format()))); diff --git a/app/render/framehashcache.h b/app/render/framehashcache.h index 343cfed0c..3c57310b8 100644 --- a/app/render/framehashcache.h +++ b/app/render/framehashcache.h @@ -62,6 +62,7 @@ public: static bool SaveCacheFrame(const QString& filename, char *data, const VideoRenderingParams &vparam); static void SaveCacheFrame(const QByteArray& hash, char *data, const VideoRenderingParams &vparam); + static void SaveCacheFrame(const QByteArray& hash, FramePtr frame); static QString GetFormatExtension(const PixelFormat::Format& f); diff --git a/app/render/playbackcache.h b/app/render/playbackcache.h index 1ade109b7..949d15e64 100644 --- a/app/render/playbackcache.h +++ b/app/render/playbackcache.h @@ -29,7 +29,7 @@ OLIVE_NAMESPACE_ENTER class PlaybackCache : public QObject { - Q_OBJECT + Q_OBJECT public: PlaybackCache(); @@ -50,11 +50,11 @@ public: } signals: - void Invalidated(const TimeRange& r); + void Invalidated(const OLIVE_NAMESPACE::TimeRange& r); - void Validated(const TimeRange& r); + void Validated(const OLIVE_NAMESPACE::TimeRange& r); - void LengthChanged(const rational& r); + void LengthChanged(const OLIVE_NAMESPACE::rational& r); protected: void Validate(const TimeRange& r); diff --git a/app/task/CMakeLists.txt b/app/task/CMakeLists.txt index e9d4fa410..1e472bf2b 100644 --- a/app/task/CMakeLists.txt +++ b/app/task/CMakeLists.txt @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(cache) add_subdirectory(conform) add_subdirectory(proxy) diff --git a/app/task/cache/CMakeLists.txt b/app/task/cache/CMakeLists.txt new file mode 100644 index 000000000..85f683a5d --- /dev/null +++ b/app/task/cache/CMakeLists.txt @@ -0,0 +1,22 @@ +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + task/cache/cache.h + task/cache/cache.cpp + PARENT_SCOPE +) diff --git a/app/task/cache/cache.cpp b/app/task/cache/cache.cpp new file mode 100644 index 000000000..9f34ec875 --- /dev/null +++ b/app/task/cache/cache.cpp @@ -0,0 +1,174 @@ +/*** + + 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 "cache.h" + +#include + +#include "common/timecodefunctions.h" +#include "project/item/sequence/sequence.h" +#include "render/backend/opengl/openglbackend.h" + +OLIVE_NAMESPACE_ENTER + +CacheTask::CacheTask(ViewerOutput* viewer, int divider, bool in_out_only) : + viewer_(viewer), + in_out_only_(in_out_only), + divider_(divider) +{ + SetTitle(tr("Caching \"%1\"").arg(viewer_->media_name())); +} + +struct TimeHashFuturePair { + rational time; + QFuture hash_future; +}; + +struct HashFrameFuturePair { + QByteArray hash; + QFuture frame_future; +}; + +struct HashDownloadFuturePair { + QByteArray hash; + QFuture download_future; +}; + +void CacheTask::Action() +{ + OpenGLBackend backend; + + RenderMode::Mode mode = RenderMode::kOffline; + PixelFormat::Format format = PixelFormat::instance()->GetConfiguredFormatForMode(mode); + + backend.SetViewerNode(viewer_); + backend.SetPixelFormat(format); + backend.SetMode(mode); + backend.SetDivider(divider_); + backend.SetSampleFormat(SampleFormat::kInternalFormat); + + // Get list of invalidated ranges + TimeRangeList range_to_cache = viewer_->video_frame_cache()->GetInvalidatedRanges(); + + // If we're caching only in-out, limit the range to that + if (in_out_only_) { + Sequence* s = static_cast(viewer_->parent()); + + if (s->workarea()->enabled()) { + range_to_cache = range_to_cache.Intersects(s->workarea()->range()); + } + } + // Get hashes for each frame + QLinkedList hash_list; + + while (!range_to_cache.isEmpty()) { + const TimeRange& range = range_to_cache.first(); + + const rational& timebase = viewer_->video_params().time_base(); + + rational time = range.in(); + rational snapped = Timecode::snap_time_to_timebase(time, timebase); + rational next; + + if (snapped > time) { + next = snapped; + snapped -= timebase; + } else { + next = snapped + timebase; + } + + hash_list.append({snapped, backend.Hash(snapped)}); + range_to_cache.RemoveTimeRange(TimeRange(snapped, next)); + } + + // Determine any duplicates + QMap< QByteArray, QLinkedList > times_to_render; + foreach (const TimeHashFuturePair& i, hash_list) { + times_to_render[i.hash_future.result()].append(i.time); + } + + // Render all frames necessary + QLinkedList render_lookup_table; + { + QMap< QByteArray, QLinkedList >::const_iterator i; + for (i=times_to_render.constBegin(); i!=times_to_render.constEnd(); i++) { + const QByteArray& hash = i.key(); + + render_lookup_table.append({hash, backend.RenderFrame(i.value().first(), false)}); + } + } + + OIIO::TypeDesc output_desc = PixelFormat::GetOIIOTypeDesc(format); + OIIO::ImageSpec output_spec(viewer_->video_params().width() / divider_, + viewer_->video_params().height() / divider_, + PixelFormat::ChannelCount(format), + output_desc); + + // Start downloading frames that have finished + { + int counter = 0; + + QLinkedList download_futures; + + // Iterators + QLinkedList::iterator i; + QLinkedList::iterator j; + + while (!render_lookup_table.isEmpty() || !download_futures.isEmpty()) { + i = render_lookup_table.begin(); + + while (i != render_lookup_table.end()) { + if (i->frame_future.isFinished()) { + FramePtr f = i->frame_future.result(); + + // Start multithreaded download here + download_futures.append({i->hash, + QtConcurrent::run(FrameHashCache::SaveCacheFrame, i->hash, f)}); + + i = render_lookup_table.erase(i); + } else { + i++; + } + } + + j = download_futures.begin(); + + while (j != download_futures.end()) { + if (j->download_future.isFinished()) { + // Place it in the cache + const QLinkedList& times_with_hash = times_to_render.value(j->hash); + foreach (const rational& t, times_with_hash) { + viewer_->video_frame_cache()->SetHash(t, j->hash); + } + + // Signal process + counter++; + emit ProgressChanged(qRound(100.0 * static_cast(counter) / static_cast(render_lookup_table.size()))); + + j = download_futures.erase(j); + } else { + j++; + } + } + } + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/task/cache/cache.h b/app/task/cache/cache.h new file mode 100644 index 000000000..afaccb47a --- /dev/null +++ b/app/task/cache/cache.h @@ -0,0 +1,49 @@ +/*** + + 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 . + +***/ + +#ifndef CACHETASK_H +#define CACHETASK_H + +#include "node/output/viewer/viewer.h" +#include "task/task.h" + +OLIVE_NAMESPACE_ENTER + +class CacheTask : public Task +{ + Q_OBJECT +public: + CacheTask(ViewerOutput* viewer, int divider, bool in_out_only); + +protected: + virtual void Action() override; + +private: + ViewerOutput* viewer_; + + bool in_out_only_; + + int divider_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // CACHETASK_H diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 4a8b32a8b..1d3c393cb 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -340,6 +340,45 @@ void ViewerWidget::SetGizmos(Node *node) display_widget_->SetGizmos(node); } +FramePtr DecodeCachedImage(const QString &fn, const rational& time) +{ + FramePtr frame = nullptr; + + if (!fn.isEmpty() && QFileInfo::exists(fn)) { + auto input = OIIO::ImageInput::open(fn.toStdString()); + + if (input) { + + PixelFormat::Format image_format = PixelFormat::OIIOFormatToOliveFormat(input->spec().format, + input->spec().nchannels == kRGBAChannels); + + frame = Frame::Create(); + frame->set_timestamp(time); + frame->set_video_params(VideoRenderingParams(input->spec().width, + input->spec().height, + image_format)); + + frame->allocate(); + + input->read_image(input->spec().format, + frame->data(), + OIIO::AutoStride, + frame->linesize_bytes()); + + input->close(); + +#if OIIO_VERSION < 10903 + OIIO::ImageInput::destroy(input); +#endif + + } else { + qWarning() << "OIIO Error:" << OIIO::geterror().c_str(); + } + } + + return frame; +} + void ViewerWidget::UpdateTextureFromNode(const rational& time) { if (!FrameExistsAtTime(time)) { @@ -348,9 +387,8 @@ void ViewerWidget::UpdateTextureFromNode(const rational& time) return; } - { - QMutexLocker locker(playback_queue_.lock()); - + // Check playback queue for a frame + if (IsPlaying()) { while (!playback_queue_.isEmpty()) { const ViewerPlaybackFrame& pf = playback_queue_.first(); @@ -368,13 +406,11 @@ void ViewerWidget::UpdateTextureFromNode(const rational& time) } } - } - if (IsPlaying()) { qDebug() << "Playback queue couldn't keep up - falling back to realtime decoding"; } - // Frame was not in queue, will require decoding + // Frame was not in queue, will require rendering or decoding from cache QFutureWatcher* watcher = new QFutureWatcher(); connect(watcher, @@ -382,7 +418,7 @@ void ViewerWidget::UpdateTextureFromNode(const rational& time) this, &ViewerWidget::RendererGeneratedFrame); - watcher->setFuture(renderer_->RenderFrame(time, true)); + watcher->setFuture(GetFrame(time, true)); } void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) @@ -485,11 +521,11 @@ void ViewerWidget::SetColorTransform(const ColorTransform &transform, ViewerDisp void ViewerWidget::FillPlaybackQueue() { // FIXME: Replace with an asynchronous approach - while (playback_queue_.size() < 8) { + while (playback_queue_.size() < 16) { rational next_time = Timecode::timestamp_to_time(playback_queue_next_frame_, timebase()); playback_queue_next_frame_ += playback_speed_; - QFuture future = renderer_->RenderFrame(next_time, false); + QFuture future = GetFrame(next_time, false); future.waitForFinished(); playback_queue_.AppendTimewise({future.result()->timestamp(), future.result()}, playback_speed_); @@ -507,7 +543,7 @@ QString ViewerWidget::GetCachedFilenameFromTime(const rational &time) if (!hash.isEmpty()) { return GetConnectedNode()->video_frame_cache()->CachePathName( hash, - PixelFormat::instance()->GetConfiguredFormatForMode(RenderMode::kOffline)); + GetCurrentPixelFormat()); } } @@ -519,45 +555,6 @@ bool ViewerWidget::FrameExistsAtTime(const rational &time) return GetConnectedNode() && time < GetConnectedNode()->GetLength(); } -FramePtr ViewerWidget::DecodeCachedImage(const QString &fn) -{ - FramePtr frame = nullptr; - - if (!fn.isEmpty() && QFileInfo::exists(fn)) { - auto input = OIIO::ImageInput::open(fn.toStdString()); - - if (input) { - - PixelFormat::Format image_format = PixelFormat::OIIOFormatToOliveFormat(input->spec().format, - input->spec().nchannels == kRGBAChannels); - - frame = Frame::Create(); - - frame->set_video_params(VideoRenderingParams(input->spec().width, - input->spec().height, - image_format)); - - frame->allocate(); - - input->read_image(input->spec().format, - frame->data(), - OIIO::AutoStride, - frame->linesize_bytes()); - - input->close(); - -#if OIIO_VERSION < 10903 - OIIO::ImageInput::destroy(input); -#endif - - } else { - qWarning() << "OIIO Error:" << OIIO::geterror().c_str(); - } - } - - return frame; -} - void ViewerWidget::SetDisplayImage(FramePtr frame, bool main_only) { display_widget_->SetImage(frame); @@ -587,7 +584,27 @@ void ViewerWidget::RequestNextFrameForQueue() &QFutureWatcher::finished, this, &ViewerWidget::RendererGeneratedFrameForQueue); - watcher->setFuture(renderer_->RenderFrame(next_time, false)); + watcher->setFuture(GetFrame(next_time, false)); +} + +PixelFormat::Format ViewerWidget::GetCurrentPixelFormat() const +{ + return PixelFormat::instance()->GetConfiguredFormatForMode(RenderMode::kOffline); +} + +QFuture ViewerWidget::GetFrame(const rational &t, bool clear_render_queue) +{ + QByteArray cached_hash = GetConnectedNode()->video_frame_cache()->GetHash(t); + if (cached_hash.isEmpty()) { + // Frame hasn't been cached, start render job + return renderer_->RenderFrame(t, clear_render_queue); + } else { + // Frame has been cached, grab the frame + QString cache_fn = GetConnectedNode()->video_frame_cache()->CachePathName(cached_hash, + GetCurrentPixelFormat()); + + return QtConcurrent::run(DecodeCachedImage, cache_fn, t); + } } void ViewerWidget::UpdateStack() @@ -699,7 +716,7 @@ void ViewerWidget::UpdateRendererParameters() renderer_->SetDivider(divider_); renderer_->SetMode(render_mode); - renderer_->SetPixelFormat(PixelFormat::instance()->GetConfiguredFormatForMode(render_mode)); + renderer_->SetPixelFormat(GetCurrentPixelFormat()); display_widget_->SetVideoParams(GetConnectedNode()->video_params()); @@ -859,11 +876,7 @@ void ViewerWidget::Pause() window->Pause(); } - { - playback_queue_.lock()->lock(); - playback_queue_.clear(); - playback_queue_.lock()->unlock(); - } + playback_queue_.clear(); } } diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 66d5364ea..9958c1ef8 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -182,12 +182,14 @@ private: bool FrameExistsAtTime(const rational& time); - FramePtr DecodeCachedImage(const QString& fn); - void SetDisplayImage(FramePtr frame, bool main_only); void RequestNextFrameForQueue(); + PixelFormat::Format GetCurrentPixelFormat() const; + + QFuture GetFrame(const rational& t, bool clear_render_queue); + QStackedWidget* stack_; ViewerSizer* sizer_; @@ -240,7 +242,7 @@ private slots: void SetZoomFromMenu(QAction* action); - void ViewerInvalidatedRange(const TimeRange &range); + void ViewerInvalidatedRange(const OLIVE_NAMESPACE::TimeRange &range); void UpdateStack(); diff --git a/app/widget/viewer/viewerqueue.h b/app/widget/viewer/viewerqueue.h index 3cb6443d9..6c3704867 100644 --- a/app/widget/viewer/viewerqueue.h +++ b/app/widget/viewer/viewerqueue.h @@ -56,14 +56,6 @@ public: } } - QMutex* lock() - { - return &queue_lock_; - } - -private: - QMutex queue_lock_; - }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/viewer/viewerwindow.cpp b/app/widget/viewer/viewerwindow.cpp index 04ae55afb..3325be924 100644 --- a/app/widget/viewer/viewerwindow.cpp +++ b/app/widget/viewer/viewerwindow.cpp @@ -77,7 +77,6 @@ void ViewerWindow::Pause() { disconnect(display_widget_, &ViewerDisplayWidget::frameSwapped, this, &ViewerWindow::UpdateFromQueue); - QMutexLocker locker(queue_.lock()); queue_.clear(); } @@ -103,7 +102,6 @@ void ViewerWindow::UpdateFromQueue() rational time = Timecode::timestamp_to_time(t, playback_timebase_); - QMutexLocker locker(queue_.lock()); while (!queue_.isEmpty()) { const ViewerPlaybackFrame& pf = queue_.first(); diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index 4b9791b2b..92345a34f 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -27,7 +27,9 @@ #include "config/config.h" #include "core.h" #include "dialog/actionsearch/actionsearch.h" +#include "dialog/task/task.h" #include "panel/panelmanager.h" +#include "task/cache/cache.h" #include "tool/tool.h" #include "ui/style/style.h" #include "undo/undostack.h" @@ -611,7 +613,16 @@ void MainMenu::OpenRecentItemTriggered() void MainMenu::SequenceCacheTriggered() { - qDebug() << "STUB"; + TimeBasedPanel* p = PanelManager::instance()->MostRecentlyFocused(); + + if (p && p->GetConnectedViewer()) { + // FIXME: Hardcoded divider... + // FIXME: Consider preventing caching the footage viewer + CacheTask* task = new CacheTask(p->GetConnectedViewer(), 2, false); + + TaskDialog* dialog = new TaskDialog(task, tr("Caching Sequence"), parentWidget()); + dialog->open(); + } } void MainMenu::SequenceCacheInOutTriggered()