diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index b603d4af7..696f900ae 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -187,9 +187,9 @@ void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int if (Node *connected = GetConnectedOutput(from, element)) { TimeRange max_range = InputTimeAdjustment(from, element, TimeRange(0, length())); if (type == Track::kVideo) { - emit connected->video_frame_cache()->Request(range.Intersected(max_range), true); + emit connected->video_frame_cache()->Request(range.Intersected(max_range), PlaybackCache::kPreviewsOnly); } else if (type == Track::kAudio) { - emit connected->audio_playback_cache()->Request(range.Intersected(max_range), true); + emit connected->audio_playback_cache()->Request(range.Intersected(max_range), PlaybackCache::kPreviewsOnly); } } } @@ -250,6 +250,7 @@ void ClipBlock::InputConnectedEvent(const QString &input, int element, Node *out super::InputConnectedEvent(input, element, output); if (input == kBufferIn) { + connect(output->video_frame_cache(), &FrameHashCache::ThumbnailsUpdated, this, &Block::PreviewChanged); connect(output->audio_playback_cache(), &AudioPlaybackCache::WaveformUpdated, this, &Block::PreviewChanged); } } @@ -259,6 +260,7 @@ void ClipBlock::InputDisconnectedEvent(const QString &input, int element, Node * super::InputDisconnectedEvent(input, element, output); if (input == kBufferIn) { + disconnect(output->video_frame_cache(), &FrameHashCache::ThumbnailsUpdated, this, &Block::PreviewChanged); disconnect(output->audio_playback_cache(), &AudioPlaybackCache::WaveformUpdated, this, &Block::PreviewChanged); } } diff --git a/app/node/block/clip/clip.h b/app/node/block/clip/clip.h index dd075b728..dab1ecf41 100644 --- a/app/node/block/clip/clip.h +++ b/app/node/block/clip/clip.h @@ -119,6 +119,15 @@ public: return block_links_; } + const FrameHashCache *thumbnails() + { + if (Node *n = GetConnectedOutput(kBufferIn)) { + return n->video_frame_cache(); + } else { + return nullptr; + } + } + const AudioVisualWaveform *waveform() { if (Node *n = GetConnectedOutput(kBufferIn)) { diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 541698d55..e40022ff5 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -389,10 +389,6 @@ void ViewerOutput::InputValueChangedEvent(const QString &input, int element) } if (frame_rate_changed) { - // FIXME: Will need to find a better way to update this soon - //if (video_frame_cache()->IsEnabled()) { - video_frame_cache()->SetTimebase(new_video_params.frame_rate_as_time_base()); - //} emit FrameRateChanged(new_video_params.frame_rate()); } @@ -412,11 +408,6 @@ void ViewerOutput::InputValueChangedEvent(const QString &input, int element) emit AudioParamsChanged(); - // FIXME: Will need to find a better way to update this soon - //if (audio_playback_cache()->IsEnabled()) { - audio_playback_cache()->SetParameters(GetAudioParams()); - //} - cached_audio_params_ = new_audio_params; } diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index a75ab921e..da04f5e99 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -443,17 +443,14 @@ void NodeTraverser::ResolveJobs(NodeValue &val, const TimeRange &range) VideoParams render_params = GetCacheVideoParams(); VideoParams job_params = job.video_params(); - // HACK/FIXME: Override old cached probe data that contains an invalid divider. Might be - // good in the future to version the probe data so we can automatically - // ignore older stuff. - job_params.set_divider(render_params.divider()); - - // See if we can make this divider larger (i.e. if the footage is smaller) - while (job_params.divider() > 1 - && VideoParams::GetScaledDimension(job_params.width(), job_params.divider()-1) < render_params.effective_width() - && VideoParams::GetScaledDimension(job_params.height(), job_params.divider()-1) < render_params.effective_height()) { - job_params.set_divider(job_params.divider() - 1); + if (render_params.divider() > 1) { + // Use a divider appropriate for this target resolution + job_params.set_divider(VideoParams::GetDividerForTargetResolution(job_params.width(), job_params.height(), render_params.effective_width(), render_params.effective_height())); + } else { + // Render everything at full res + job_params.set_divider(1); } + job.set_video_params(job_params); if (footage_time.isNaN()) { diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index a99d61a7a..ad0832459 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -32,12 +32,11 @@ #include "codec/frame.h" #include "common/filefunctions.h" +#include "common/oiioutils.h" #include "render/diskmanager.h" namespace olive { -const QString FrameHashCache::kCacheFormatExtension = QStringLiteral(".exr"); - #define super PlaybackCache FrameHashCache::FrameHashCache(QObject *parent) : @@ -256,63 +255,132 @@ QString FrameHashCache::CachePathName(const QString &cache_path, const QUuid &ca bool FrameHashCache::SaveCacheFrame(const QString &filename, const FramePtr frame) { - if (!VideoParams::FormatIsFloat(frame->format())) { - return false; - } - // Ensure directory is created QDir cache_dir = QFileInfo(filename).dir(); if (!FileFunctions::DirectoryIsValid(cache_dir)) { return false; } - // Floating point types are stored in EXR - Imf::PixelType pix_type; + if (VideoParams::FormatIsFloat(frame->format())) { + // Floating point types are stored in EXR + Imf::PixelType pix_type; - if (frame->format() == VideoParams::kFormatFloat16) { - pix_type = Imf::HALF; - } else { - pix_type = Imf::FLOAT; - } - - Imf::Header header(frame->width(), frame->height()); - header.channels().insert("R", Imf::Channel(pix_type)); - header.channels().insert("G", Imf::Channel(pix_type)); - header.channels().insert("B", Imf::Channel(pix_type)); - if (frame->channel_count() == VideoParams::kRGBAChannelCount) { - header.channels().insert("A", Imf::Channel(pix_type)); - } - - header.compression() = Imf::DWAA_COMPRESSION; - header.insert("dwaCompressionLevel", Imf::FloatAttribute(200.0f)); - header.pixelAspectRatio() = frame->video_params().pixel_aspect_ratio().toDouble(); - - header.insert("oliveDivider", Imf::IntAttribute(frame->video_params().divider())); - - try { - Imf::OutputFile out(filename.toUtf8(), header, 0); - - int bpc = VideoParams::GetBytesPerChannel(frame->format()); - - size_t xs = frame->channel_count() * bpc; - size_t ys = frame->linesize_bytes(); - - Imf::FrameBuffer framebuffer; - framebuffer.insert("R", Imf::Slice(pix_type, frame->data(), xs, ys)); - framebuffer.insert("G", Imf::Slice(pix_type, frame->data() + bpc, xs, ys)); - framebuffer.insert("B", Imf::Slice(pix_type, frame->data() + 2*bpc, xs, ys)); - if (frame->channel_count() == VideoParams::kRGBAChannelCount) { - framebuffer.insert("A", Imf::Slice(pix_type, frame->data() + 3*bpc, xs, ys)); + if (frame->format() == VideoParams::kFormatFloat16) { + pix_type = Imf::HALF; + } else { + pix_type = Imf::FLOAT; } - out.setFrameBuffer(framebuffer); - out.writePixels(frame->height()); + Imf::Header header(frame->width(), frame->height()); + header.channels().insert("R", Imf::Channel(pix_type)); + header.channels().insert("G", Imf::Channel(pix_type)); + header.channels().insert("B", Imf::Channel(pix_type)); + if (frame->channel_count() == VideoParams::kRGBAChannelCount) { + header.channels().insert("A", Imf::Channel(pix_type)); + } - return true; - } catch (const std::exception &e) { - qCritical() << "Failed to write cache frame:" << e.what(); + header.compression() = Imf::DWAA_COMPRESSION; + header.insert("dwaCompressionLevel", Imf::FloatAttribute(200.0f)); + header.pixelAspectRatio() = frame->video_params().pixel_aspect_ratio().toDouble(); - return false; + header.insert("oliveDivider", Imf::IntAttribute(frame->video_params().divider())); + + try { + Imf::OutputFile out(filename.toUtf8(), header, 0); + + int bpc = VideoParams::GetBytesPerChannel(frame->format()); + + size_t xs = frame->channel_count() * bpc; + size_t ys = frame->linesize_bytes(); + + Imf::FrameBuffer framebuffer; + framebuffer.insert("R", Imf::Slice(pix_type, frame->data(), xs, ys)); + framebuffer.insert("G", Imf::Slice(pix_type, frame->data() + bpc, xs, ys)); + framebuffer.insert("B", Imf::Slice(pix_type, frame->data() + 2*bpc, xs, ys)); + if (frame->channel_count() == VideoParams::kRGBAChannelCount) { + framebuffer.insert("A", Imf::Slice(pix_type, frame->data() + 3*bpc, xs, ys)); + } + out.setFrameBuffer(framebuffer); + + out.writePixels(frame->height()); + + return true; + } catch (const std::exception &e) { + qCritical() << "Failed to write cache frame:" << e.what(); + + return false; + } + } else { + QImage::Format fmt = QImage::Format_Invalid; + + switch (frame->format()) { + case VideoParams::kFormatUnsigned8: + if (frame->channel_count() == VideoParams::kRGBAChannelCount){ + fmt = QImage::Format_RGBA8888_Premultiplied; + } else if (frame->channel_count() == VideoParams::kRGBChannelCount){ + fmt = QImage::Format_RGB888; + } + break; + case VideoParams::kFormatUnsigned16: + if (frame->channel_count() == VideoParams::kRGBAChannelCount){ + fmt = QImage::Format_RGBA64_Premultiplied; + } + break; + case VideoParams::kFormatFloat16: + case VideoParams::kFormatFloat32: + case VideoParams::kFormatCount: + case VideoParams::kFormatInvalid: + break; + } + + if (fmt == QImage::Format_Invalid) { + return false; + } + + QImage img(reinterpret_cast(frame->data()), frame->width(), frame->height(), frame->linesize_bytes(), fmt); + + return img.save(filename, "jpg"); + + + /* + qDebug() << "hello?" << filename; + + // Integer types are stored in JPG + QString tmp = filename; + tmp.append(QStringLiteral(".jpg")); + + std::string tmp_std = tmp.toStdString(); + auto out = OIIO::ImageOutput::create(tmp_std); + if (!out) { + qDebug() << "fail create"; + return false; + } + + auto fmt = OIIOUtils::GetOIIOBaseTypeFromFormat(frame->format()); + qDebug() << "writing" << fmt; + if (!out->open(tmp_std, OIIO::ImageSpec(frame->width(), frame->height(), frame->channel_count(), fmt))) { + qDebug() << "fail open"; + return false; + } + + bool ret = out->write_image(fmt, frame->data(), OIIO::AutoStride, frame->linesize_bytes()); + out->close(); + + if (ret) { + QFile f(filename); + if (f.exists()) { + f.remove(); + } + ret = QFile::rename(tmp, filename); + if (!ret) { + qDebug() << "fail rename from" << tmp << "to" << filename; + } + } else { + qDebug() << "fail write"; + } + + return ret; + */ } } diff --git a/app/render/framehashcache.h b/app/render/framehashcache.h index a98432ac5..d45228a7c 100644 --- a/app/render/framehashcache.h +++ b/app/render/framehashcache.h @@ -65,6 +65,9 @@ public: FramePtr LoadCacheFrame(const int64_t &time) const; static FramePtr LoadCacheFrame(const QString& fn); +signals: + void ThumbnailsUpdated(); + private: rational ToTime(const int64_t &ts) const; int64_t ToTimestamp(const rational &ts, Timecode::Rounding rounding = Timecode::kRound) const; @@ -80,8 +83,6 @@ private: rational timebase_; - static const QString kCacheFormatExtension; - private slots: void HashDeleted(const QString &path, const QString &filename); diff --git a/app/render/playbackcache.cpp b/app/render/playbackcache.cpp index 9c6e476b9..762393223 100644 --- a/app/render/playbackcache.cpp +++ b/app/render/playbackcache.cpp @@ -41,7 +41,7 @@ void PlaybackCache::Invalidate(const TimeRange &r) emit Invalidated(r); if (automatic_) { - emit Request(r, false); + emit Request(r, kCacheOnly); } } diff --git a/app/render/playbackcache.h b/app/render/playbackcache.h index c3b464305..64045257b 100644 --- a/app/render/playbackcache.h +++ b/app/render/playbackcache.h @@ -69,6 +69,11 @@ public: QDir GetThisCacheDirectory() const; static QDir GetThisCacheDirectory(const QString &cache_path, const QUuid &cache_id); + enum RequestType { + kCacheOnly, + kPreviewsOnly + }; + public slots: void InvalidateAll(); @@ -77,7 +82,7 @@ signals: void Validated(const olive::TimeRange& r); - void Request(const olive::TimeRange& r, bool previews_only); + void Request(const olive::TimeRange& r, olive::PlaybackCache::RequestType type); void AutomaticChanged(bool e); diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index d52c76dad..36d60b099 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -30,6 +30,7 @@ #include "task/customcache/customcachetask.h" #include "task/taskmanager.h" #include "widget/slider/base/numericsliderbase.h" +#include "widget/viewer/viewer.h" namespace olive { @@ -38,7 +39,8 @@ PreviewAutoCacher::PreviewAutoCacher(QObject *parent) : viewer_node_(nullptr), use_custom_range_(false), pause_renders_(false), - single_frame_render_(nullptr) + single_frame_render_(nullptr), + display_color_processor_(nullptr) { // Set defaults SetPlayhead(0); @@ -78,21 +80,21 @@ RenderTicketPtr PreviewAutoCacher::GetSingleFrame(const rational &t, RenderTicke RenderTicketPtr PreviewAutoCacher::GetRangeOfAudio(TimeRange range, RenderTicketPriority priority) { - return RenderAudio(range, false, priority); + return RenderAudio(copied_viewer_node_->GetConnectedSampleOutput(), range, PlaybackCache::kCacheOnly, priority); } -void PreviewAutoCacher::VideoInvalidatedFromCache(const TimeRange &range) +void PreviewAutoCacher::VideoInvalidatedFromCache(const TimeRange &range, PlaybackCache::RequestType type) { FrameHashCache *cache = static_cast(sender()); - VideoInvalidatedFromNode(cache->parent(), range); + VideoInvalidatedFromNode(cache->parent(), range, type); } -void PreviewAutoCacher::AudioInvalidatedFromCache(const TimeRange &range) +void PreviewAutoCacher::AudioInvalidatedFromCache(const TimeRange &range, PlaybackCache::RequestType type) { AudioPlaybackCache *cache = static_cast(sender()); - AudioInvalidatedFromNode(cache->parent(), range); + AudioInvalidatedFromNode(cache->parent(), range, type); } void PreviewAutoCacher::AudioRendered() @@ -105,9 +107,9 @@ void PreviewAutoCacher::AudioRendered() 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); - Node *node = Node::ValueToPtr(watcher->property("node")); + Node *node = copy_map_.key(Node::ValueToPtr(watcher->property("node"))); - if (watcher->HasResult()) { + if (watcher->HasResult() && node) { AudioCacheData &d = audio_cache_data_[node]; JobTime watcher_job_time = watcher->property("job").value(); @@ -119,23 +121,26 @@ void PreviewAutoCacher::AudioRendered() SampleBuffer buf = watcher->Get().value(); node->audio_playback_cache()->SetParameters(buf.audio_params()); - // WritePCM is tolerant to its buffer being null, it will just write silence instead - node->audio_playback_cache()->WritePCM(range, - valid_ranges, - watcher->Get().value()); + PlaybackCache::RequestType type = PlaybackCache::RequestType(watcher->property("type").toInt()); - // 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 - node->audio_playback_cache()->Invalidate(range); - } else { - // Wait for conform - d.needing_conform.insert(range); - } + if (type == PlaybackCache::kCacheOnly) { + // WritePCM is tolerant to its buffer being null, it will just write silence instead + node->audio_playback_cache()->WritePCM(range, + valid_ranges, + watcher->Get().value()); } else { - qDebug() << "Writing waveforms to" << range << valid_ranges; - node->audio_playback_cache()->WriteWaveform(range, valid_ranges, &waveform); + // 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 + node->audio_playback_cache()->Invalidate(range); + } else { + // Wait for conform + d.needing_conform.insert(range); + } + } else { + node->audio_playback_cache()->WriteWaveform(range, valid_ranges, &waveform); + } } } @@ -157,10 +162,17 @@ void PreviewAutoCacher::VideoRendered() if (it != video_tasks_.end()) { // Assume that a "result" is a fully completed image and a non-result is a cancelled ticket if (watcher->HasResult()) { - // Download frame in another thread - if (watcher->GetTicket()->property("cached").toBool()) { + PlaybackCache::RequestType type = PlaybackCache::RequestType(watcher->property("type").toInt()); + + if (type == PlaybackCache::kCacheOnly) { + if (watcher->GetTicket()->property("cached").toBool()) { + if (FrameHashCache *cache = Node::ValueToPtr(watcher->property("cache"))) { + cache->ValidateTime(it.value()); + } + } + } else { if (FrameHashCache *cache = Node::ValueToPtr(watcher->property("cache"))) { - cache->ValidateTime(it.value()); + emit cache->ThumbnailsUpdated(); } } } @@ -392,14 +404,14 @@ void PreviewAutoCacher::CancelQueuedSingleFrameRender() void PreviewAutoCacher::VideoInvalidatedList(Node *node, const TimeRangeList &list) { foreach (const TimeRange &range, list) { - VideoInvalidatedFromNode(node, range); + VideoInvalidatedFromNode(node, range, PlaybackCache::kCacheOnly); } } void PreviewAutoCacher::AudioInvalidatedList(Node *node, const TimeRangeList &list) { foreach (const TimeRange &range, list) { - AudioInvalidatedFromNode(node, range); + AudioInvalidatedFromNode(node, range, PlaybackCache::kCacheOnly); } } @@ -409,42 +421,40 @@ void PreviewAutoCacher::StartCachingRange(const TimeRange &range, TimeRangeList tracker->insert(range, graph_changed_time_); } -void PreviewAutoCacher::StartCachingVideoRange(Node *node, const TimeRange &range) +void PreviewAutoCacher::StartCachingVideoRange(Node *node, const TimeRange &range, PlaybackCache::RequestType type) { - VideoCacheData &d = video_cache_data_[node]; - - StartCachingRange(range, &d.invalidated, &d.job_tracker); + pending_video_jobs_.push_back({node, range, TimeRangeListFrameIterator({range}, viewer_node_->GetVideoParams().frame_rate_as_time_base()), type}); + video_cache_data_[node].job_tracker.insert(range, graph_changed_time_); TryRender(); } -void PreviewAutoCacher::StartCachingAudioRange(Node *node, const TimeRange &range) +void PreviewAutoCacher::StartCachingAudioRange(Node *node, const TimeRange &range, PlaybackCache::RequestType type) { - AudioCacheData &d = audio_cache_data_[node]; - - StartCachingRange(range, &d.invalidated, &d.job_tracker); + pending_audio_jobs_.push_back({node, range, type}); + audio_cache_data_[node].job_tracker.insert(range, graph_changed_time_); TryRender(); } -void PreviewAutoCacher::VideoInvalidatedFromNode(Node *node, const TimeRange &range) +void PreviewAutoCacher::VideoInvalidatedFromNode(Node *node, const TimeRange &range, PlaybackCache::RequestType type) { // 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(node); + //CancelVideoTasks(node); // If auto-cache is enabled and a slider is not being dragged, queue up to hash these frames if (!NodeInputDragger::IsInputBeingDragged()) { - StartCachingVideoRange(node, range); + StartCachingVideoRange(node, range, type); } } -void PreviewAutoCacher::AudioInvalidatedFromNode(Node *node, const TimeRange &range) +void PreviewAutoCacher::AudioInvalidatedFromNode(Node *node, const TimeRange &range, PlaybackCache::RequestType type) { // 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(); // If we're auto-caching audio or require realtime waveforms, we'll have to render this - StartCachingAudioRange(node, range); + StartCachingAudioRange(node, range, type); } void PreviewAutoCacher::VideoAutoCacheEnableChangedFromNode(Node *node, bool e) @@ -501,8 +511,9 @@ void PreviewAutoCacher::CancelAudioTasks(bool and_wait_for_them_to_finish) bool PreviewAutoCacher::IsRenderingCustomRange() const { - const VideoCacheData &d = video_cache_data_.value(viewer_node_); - return d.iterator.IsCustomRange() && d.iterator.HasNext(); + /*const VideoCacheData &d = video_cache_data_.value(viewer_node_); + return d.iterator.IsCustomRange() && d.iterator.HasNext();*/ + return false; } void PreviewAutoCacher::SetRendersPaused(bool e) @@ -553,10 +564,6 @@ void PreviewAutoCacher::TryRender() { delayed_requeue_timer_.stop(); - if (pause_renders_) { - return; - } - if (!graph_update_queue_.isEmpty()) { // 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 @@ -572,7 +579,9 @@ void PreviewAutoCacher::TryRender() if (single_frame_render_) { // Check if already caching this - RenderTicketWatcher *watcher = RenderFrame(single_frame_render_->property("time").value(), + RenderTicketWatcher *watcher = RenderFrame(copied_viewer_node_->GetConnectedTextureOutput(), + single_frame_render_->property("time").value(), + PlaybackCache::kCacheOnly, RenderTicketPriority(single_frame_render_->property("priority").toInt()), nullptr); video_immediate_passthroughs_[watcher].append(single_frame_render_); @@ -580,102 +589,111 @@ void PreviewAutoCacher::TryRender() single_frame_render_ = nullptr; } - // Ensure we are running tasks if we have any - const int max_tasks = RenderManager::GetNumberOfIdealConcurrentJobs(); + if (!pause_renders_) { + // Ensure we are running tasks if we have any + const int max_tasks = RenderManager::GetNumberOfIdealConcurrentJobs(); - // Handle video tasks - for (auto it=video_cache_data_.begin(); it!=video_cache_data_.end(); it++) { - VideoCacheData &d = it.value(); + // Handle video tasks + while (!pending_video_jobs_.empty()) { + VideoJob &d = pending_video_jobs_.front(); - // Check for newly invalidated video - if (!d.invalidated.isEmpty()) { - if (d.iterator.HasNext()) { - d.iterator.insert(d.invalidated); + if (Node *copy = copy_map_.value(d.node)) { + // Queue next frames + rational t; + while (video_tasks_.size() < max_tasks && d.iterator.GetNext(&t)) { + RenderTicketWatcher* render_task = video_tasks_.key(t); + + // We want this hash, if we're not already rendering, start render now + if (!render_task) { + // Don't render any hash more than once + RenderFrame(copy, t, d.type, RenderTicketPriority::kNormal, d.node->video_frame_cache()); + } + + emit SignalCacheProxyTaskProgress(double(d.iterator.frame_index()) / double(d.iterator.size())); + + if (!d.iterator.HasNext()) { + emit StopCacheProxyTasks(); + } + } } else { - d.iterator = TimeRangeListFrameIterator(d.invalidated, viewer_node_->GetVideoParams().frame_rate_as_time_base()); - } - d.invalidated.clear(); - } - - // Queue next frames - rational t; - while (video_tasks_.size() < max_tasks && d.iterator.GetNext(&t)) { - RenderTicketWatcher* render_task = video_tasks_.key(t); - - // We want this hash, if we're not already rendering, start render now - if (!render_task) { - // Don't render any hash more than once - RenderFrame(it.key(), t, RenderTicketPriority::kNormal, it.key()->video_frame_cache()); + qCritical() << "Failed to find node copy for video job"; } - emit SignalCacheProxyTaskProgress(double(d.iterator.frame_index()) / double(d.iterator.size())); - - if (!d.iterator.HasNext()) { - emit StopCacheProxyTasks(); + if (d.iterator.HasNext()) { + break; + } else { + pending_video_jobs_.pop_front(); } } - } - - // Handle audio tasks - for (auto it=audio_cache_data_.begin(); it!=audio_cache_data_.end(); it++) { - AudioCacheData &d = it.value(); - - if (!d.invalidated.isEmpty()) { - // Add newly invalidated audio to iterator - d.iterator.insert(d.invalidated); - d.invalidated.clear(); - } - - while (!d.iterator.isEmpty() && audio_tasks_.size() < max_tasks) { - // Copy first range in list - TimeRange r = d.iterator.first(); - - // Limit to the minimum sample rate supported by AudioVisualWaveform - we use this value so that - // whatever chunk we render can be summed down to the smallest mipmap whole - r.set_out(qMin(r.out(), r.in() + AudioVisualWaveform::kMinimumSampleRate.flipped())); + // Handle audio tasks + while (!pending_audio_jobs_.empty()) { + AudioJob &d = pending_audio_jobs_.front(); // Start job - RenderAudio(it.key(), r, true, RenderTicketPriority::kNormal); + if (Node *copy = copy_map_.value(d.node)) { + RenderAudio(copy, d.range, d.type, RenderTicketPriority::kNormal); + } else { + qCritical() << "Failed to find node copy for audio job"; + } - d.iterator.remove(r); + pending_audio_jobs_.pop_front(); } } } -RenderTicketWatcher* PreviewAutoCacher::RenderFrame(Node *node, const rational& time, RenderTicketPriority priority, FrameHashCache *cache) +RenderTicketWatcher* PreviewAutoCacher::RenderFrame(Node *node, const rational& time, PlaybackCache::RequestType type, RenderTicketPriority priority, FrameHashCache *cache) { RenderTicketWatcher* watcher = new RenderTicketWatcher(); watcher->setProperty("job", QVariant::fromValue(last_update_time_)); watcher->setProperty("cache", Node::PtrToValue(cache)); - if (cache) { - cache->SetTimebase(viewer_node_->GetVideoParams().frame_rate_as_time_base()); - } + watcher->setProperty("type", type); connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::VideoRendered); video_tasks_.insert(watcher, time); - watcher->SetTicket(RenderManager::instance()->RenderFrame(node, - copied_viewer_node_->GetVideoParams(), - copied_viewer_node_->GetAudioParams(), - copied_color_manager_, - time, - RenderMode::kOffline, - cache, - priority, - RenderManager::kTexture)); + + RenderManager::RenderVideoParams rvp(node, + copied_viewer_node_->GetVideoParams(), + copied_viewer_node_->GetAudioParams(), + time, + copied_color_manager_); + + if (cache) { + cache->SetTimebase(viewer_node_->GetVideoParams().frame_rate_as_time_base()); + rvp.AddCache(cache); + + if (type == PlaybackCache::kPreviewsOnly) { + rvp.video_params.set_divider(VideoParams::GetDividerForTargetResolution(rvp.video_params.width(), rvp.video_params.height(), 160, 120)); + rvp.force_color_output = display_color_processor_; + rvp.force_format = VideoParams::kFormatUnsigned8; + } + } + + rvp.priority = priority; + rvp.return_type = RenderManager::kTexture; + rvp.use_cache = true; + + watcher->SetTicket(RenderManager::instance()->RenderFrame(rvp)); + return watcher; } -RenderTicketPtr PreviewAutoCacher::RenderAudio(Node *node, const TimeRange &r, bool generate_waveforms, RenderTicketPriority priority) +RenderTicketPtr PreviewAutoCacher::RenderAudio(Node *node, const TimeRange &r, PlaybackCache::RequestType type, RenderTicketPriority priority) { - qDebug() << "Rendering" << r << "for" << node; - RenderTicketWatcher* watcher = new RenderTicketWatcher(); watcher->setProperty("job", QVariant::fromValue(last_update_time_)); watcher->setProperty("node", Node::PtrToValue(node)); + watcher->setProperty("type", type); connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); audio_tasks_.insert(watcher, r); - RenderTicketPtr ticket = RenderManager::instance()->RenderAudio(node, r, copied_viewer_node_->GetAudioParams(), RenderMode::kOffline, generate_waveforms, priority); + RenderManager::RenderAudioParams rap(node, + r, + copied_viewer_node_->GetAudioParams()); + + rap.generate_waveforms = (type == PlaybackCache::kPreviewsOnly); + rap.priority = priority; + + RenderTicketPtr ticket = RenderManager::instance()->RenderAudio(rap); watcher->SetTicket(ticket); return ticket; } @@ -714,9 +732,7 @@ void PreviewAutoCacher::AudioAutoCacheEnableChanged(bool e) void PreviewAutoCacher::CacheProxyTaskCancelled() { - for (auto it=video_cache_data_.begin(); it!=video_cache_data_.end(); it++) { - it->iterator.reset(); - } + pending_video_jobs_.clear(); TryRender(); } @@ -727,7 +743,7 @@ void PreviewAutoCacher::ForceCacheRange(const TimeRange &range) custom_autocache_range_ = range; // Re-hash these frames and start rendering - StartCachingVideoRange(viewer_node_, range); + StartCachingVideoRange(viewer_node_, range, PlaybackCache::kCacheOnly); } void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 0572bbd31..933ac5033 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -89,6 +89,12 @@ public: void SetRendersPaused(bool e); +public slots: + void SetDisplayColorProcessor(ColorProcessorPtr processor) + { + display_color_processor_ = processor; + } + signals: void StopCacheProxyTasks(); @@ -97,17 +103,9 @@ signals: private: void TryRender(); - RenderTicketWatcher *RenderFrame(Node *node, const rational &time, RenderTicketPriority priority, FrameHashCache *cache); - RenderTicketWatcher *RenderFrame(const rational &time, RenderTicketPriority priority, FrameHashCache *cache) - { - return RenderFrame(copied_viewer_node_->GetConnectedTextureOutput(), time, priority, cache); - } + RenderTicketWatcher *RenderFrame(Node *node, const rational &time, PlaybackCache::RequestType type, RenderTicketPriority priority, FrameHashCache *cache); - RenderTicketPtr RenderAudio(Node *node, const TimeRange &range, bool generate_waveforms, RenderTicketPriority priority); - RenderTicketPtr RenderAudio(const TimeRange &range, bool generate_waveforms, RenderTicketPriority priority) - { - return RenderAudio(copied_viewer_node_->GetConnectedSampleOutput(), range, generate_waveforms, priority); - } + RenderTicketPtr RenderAudio(Node *node, const TimeRange &range, PlaybackCache::RequestType type, RenderTicketPriority priority); /** * @brief Process all changes to internal NodeGraph copy @@ -138,11 +136,11 @@ private: void AudioInvalidatedList(Node *node, const TimeRangeList &list); void StartCachingRange(const TimeRange &range, TimeRangeList *range_list, RenderJobTracker *tracker); - void StartCachingVideoRange(Node *node, const TimeRange &range); - void StartCachingAudioRange(Node *node, const TimeRange &range); + void StartCachingVideoRange(Node *node, const TimeRange &range, PlaybackCache::RequestType type); + void StartCachingAudioRange(Node *node, const TimeRange &range, PlaybackCache::RequestType type); - void VideoInvalidatedFromNode(Node *node, const olive::TimeRange &range); - void AudioInvalidatedFromNode(Node *node, const olive::TimeRange &range); + void VideoInvalidatedFromNode(Node *node, const olive::TimeRange &range, PlaybackCache::RequestType type); + void AudioInvalidatedFromNode(Node *node, const olive::TimeRange &range, PlaybackCache::RequestType type); void VideoAutoCacheEnableChangedFromNode(Node *node, bool e); void AudioAutoCacheEnableChangedFromNode(Node *node, bool e); @@ -195,32 +193,46 @@ private: QMap audio_tasks_; QMap video_tasks_; - struct VideoCacheData { - TimeRangeList invalidated; - RenderJobTracker job_tracker; + struct VideoJob { + Node *node; + TimeRange range; TimeRangeListFrameIterator iterator; + PlaybackCache::RequestType type; + }; + + struct VideoCacheData { + RenderJobTracker job_tracker; + }; + + struct AudioJob { + Node *node; + TimeRange range; + PlaybackCache::RequestType type; }; struct AudioCacheData { - TimeRangeList invalidated; TimeRangeList needing_conform; RenderJobTracker job_tracker; - TimeRangeList iterator; }; + std::list pending_video_jobs_; + std::list pending_audio_jobs_; + QHash video_cache_data_; QHash audio_cache_data_; + ColorProcessorPtr display_color_processor_; + private slots: /** * @brief Handler for when the NodeGraph reports a video change over a certain time range */ - void VideoInvalidatedFromCache(const olive::TimeRange &range); + void VideoInvalidatedFromCache(const olive::TimeRange &range, olive::PlaybackCache::RequestType type); /** * @brief Handler for when the NodeGraph reports a audio change over a certain time range */ - void AudioInvalidatedFromCache(const olive::TimeRange &range); + void AudioInvalidatedFromCache(const olive::TimeRange &range, olive::PlaybackCache::RequestType type); /** * @brief Handler for when the RenderManager has returned rendered audio diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp index c34b050d8..09d20df00 100644 --- a/app/render/rendermanager.cpp +++ b/app/render/rendermanager.cpp @@ -76,73 +76,45 @@ RenderManager::~RenderManager() } } -RenderTicketPtr RenderManager::RenderFrame(Node *node, const VideoParams &vparam, const AudioParams ¶m, - ColorManager* color_manager, const rational& time, RenderMode::Mode mode, - FrameHashCache* cache, RenderTicketPriority priority, ReturnType return_type) -{ - return RenderFrame(node, - color_manager, - time, - mode, - vparam, - param, - QSize(0, 0), - QMatrix4x4(), - VideoParams::kFormatInvalid, - nullptr, - cache, - priority, - return_type); -} - -RenderTicketPtr RenderManager::RenderFrame(Node *node, ColorManager* color_manager, - const rational& time, RenderMode::Mode mode, - const VideoParams &video_params, const AudioParams &audio_params, - const QSize& force_size, - const QMatrix4x4& force_matrix, VideoParams::Format force_format, - ColorProcessorPtr force_color_output, - FrameHashCache* cache, RenderTicketPriority priority, ReturnType return_type) +RenderTicketPtr RenderManager::RenderFrame(const RenderVideoParams ¶ms) { // Create ticket RenderTicketPtr ticket = std::make_shared(); - ticket->setProperty("node", Node::PtrToValue(node)); - ticket->setProperty("time", QVariant::fromValue(time)); - ticket->setProperty("size", force_size); - ticket->setProperty("matrix", force_matrix); - ticket->setProperty("format", force_format); - ticket->setProperty("mode", mode); + ticket->setProperty("node", Node::PtrToValue(params.node)); + ticket->setProperty("time", QVariant::fromValue(params.time)); + ticket->setProperty("size", params.force_size); + ticket->setProperty("matrix", params.force_matrix); + ticket->setProperty("format", params.force_format); + ticket->setProperty("usecache", params.use_cache); ticket->setProperty("type", kTypeVideo); - ticket->setProperty("colormanager", Node::PtrToValue(color_manager)); - ticket->setProperty("coloroutput", QVariant::fromValue(force_color_output)); - ticket->setProperty("vparam", QVariant::fromValue(video_params)); - ticket->setProperty("aparam", QVariant::fromValue(audio_params)); - ticket->setProperty("return", return_type); + ticket->setProperty("colormanager", Node::PtrToValue(params.color_manager)); + ticket->setProperty("coloroutput", QVariant::fromValue(params.force_color_output)); + Q_ASSERT(params.video_params.is_valid()); + ticket->setProperty("vparam", QVariant::fromValue(params.video_params)); + ticket->setProperty("aparam", QVariant::fromValue(params.audio_params)); + ticket->setProperty("return", params.return_type); + ticket->setProperty("cache", params.cache_dir); + ticket->setProperty("cachetimebase", QVariant::fromValue(params.cache_timebase)); + ticket->setProperty("cacheid", QVariant::fromValue(params.cache_id)); - if (cache) { - ticket->setProperty("cache", cache->GetCacheDirectory()); - ticket->setProperty("cachetimebase", QVariant::fromValue(cache->GetTimebase())); - ticket->setProperty("cacheuuid", QVariant::fromValue(cache->GetUuid())); - } - - AddTicket(ticket, priority); + AddTicket(ticket, params.priority); return ticket; } -RenderTicketPtr RenderManager::RenderAudio(Node *node, const TimeRange &r, const AudioParams ¶ms, RenderMode::Mode mode, bool generate_waveforms, RenderTicketPriority priority) +RenderTicketPtr RenderManager::RenderAudio(const RenderAudioParams ¶ms) { // Create ticket RenderTicketPtr ticket = std::make_shared(); - ticket->setProperty("node", Node::PtrToValue(node)); - ticket->setProperty("time", QVariant::fromValue(r)); + ticket->setProperty("node", Node::PtrToValue(params.node)); + ticket->setProperty("time", QVariant::fromValue(params.range)); ticket->setProperty("type", kTypeAudio); - ticket->setProperty("mode", mode); - ticket->setProperty("enablewaveforms", generate_waveforms); - ticket->setProperty("aparam", QVariant::fromValue(params)); + ticket->setProperty("enablewaveforms", params.generate_waveforms); + ticket->setProperty("aparam", QVariant::fromValue(params.audio_params)); - AddTicket(ticket, priority); + AddTicket(ticket, params.priority); return ticket; } diff --git a/app/render/rendermanager.h b/app/render/rendermanager.h index 8753f7c90..6ef2a503a 100644 --- a/app/render/rendermanager.h +++ b/app/render/rendermanager.h @@ -68,6 +68,49 @@ public: kFrame }; + struct RenderVideoParams { + RenderVideoParams(Node *n, const VideoParams &vparam, const AudioParams &aparam, const rational &t, + ColorManager *colorman) + { + node = n; + video_params = vparam; + audio_params = aparam; + time = t; + color_manager = colorman; + use_cache = false; + priority = RenderTicketPriority::kNormal; + return_type = kFrame; + force_format = VideoParams::kFormatInvalid; + force_color_output = nullptr; + force_size = QSize(0, 0); + } + + void AddCache(FrameHashCache *cache) + { + cache_dir = cache->GetCacheDirectory(); + cache_timebase = cache->GetTimebase(); + cache_id = cache->GetUuid().toString(); + } + + Node *node; + VideoParams video_params; + AudioParams audio_params; + rational time; + ColorManager *color_manager; + bool use_cache; + RenderTicketPriority priority; + ReturnType return_type; + + QString cache_dir; + rational cache_timebase; + QString cache_id; + + QSize force_size; + QMatrix4x4 force_matrix; + VideoParams::Format force_format; + ColorProcessorPtr force_color_output; + }; + /** * @brief Asynchronously generate a frame at a given time * @@ -76,16 +119,24 @@ public: * * This function is thread-safe. */ - RenderTicketPtr RenderFrame(Node *node, const VideoParams &vparam, const AudioParams ¶m, ColorManager* color_manager, - const rational& time, RenderMode::Mode mode, - FrameHashCache* cache = nullptr, RenderTicketPriority priority = RenderTicketPriority::kNormal, ReturnType return_type = kFrame); - RenderTicketPtr RenderFrame(Node *node, ColorManager* color_manager, - const rational& time, RenderMode::Mode mode, - const VideoParams& video_params, const AudioParams& audio_params, - const QSize& force_size, - const QMatrix4x4& force_matrix, VideoParams::Format force_format, - ColorProcessorPtr force_color_output, - FrameHashCache* cache = nullptr, RenderTicketPriority priority = RenderTicketPriority::kNormal, ReturnType return_type = kFrame); + RenderTicketPtr RenderFrame(const RenderVideoParams ¶ms); + + struct RenderAudioParams { + RenderAudioParams(Node *n, const TimeRange &time, const AudioParams &aparam) + { + node = n; + range = time; + audio_params = aparam; + generate_waveforms = false; + priority = RenderTicketPriority::kNormal; + } + + Node *node; + TimeRange range; + AudioParams audio_params; + bool generate_waveforms; + RenderTicketPriority priority; + }; /** * @brief Asynchronously generate a chunk of audio @@ -94,7 +145,7 @@ public: * * This function is thread-safe. */ - RenderTicketPtr RenderAudio(Node *viewer, const TimeRange& r, const AudioParams& params, RenderMode::Mode mode, bool generate_waveforms, RenderTicketPriority priority = RenderTicketPriority::kNormal); + RenderTicketPtr RenderAudio(const RenderAudioParams ¶ms); virtual void RunTicket(RenderTicketPtr ticket) const override; diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 67e88843d..aa38a9745 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -91,32 +91,32 @@ FramePtr RenderProcessor::GenerateFrame(TexturePtr texture, const rational& time ColorProcessorPtr output_color_transform = ticket_->property("coloroutput").value(); const VideoParams& tex_params = texture->params(); + if (output_color_transform) { + TexturePtr transform_tex = render_ctx_->CreateTexture(tex_params); + ColorTransformJob job; + + job.SetColorProcessor(output_color_transform); + job.SetInputTexture(texture); + job.SetInputAlphaAssociation(OLIVE_CONFIG("ReassocLinToNonLin").toBool() ? kAlphaAssociated : kAlphaNone); + + render_ctx_->BlitColorManaged(job, transform_tex.get()); + + texture = transform_tex; + } + if (tex_params.effective_width() != frame_params.effective_width() || tex_params.effective_height() != frame_params.effective_height() - || tex_params.format() != frame_params.format() - || output_color_transform) { + || tex_params.format() != frame_params.format()) { TexturePtr blit_tex = render_ctx_->CreateTexture(frame_params); QMatrix4x4 matrix = ticket_->property("matrix").value(); - if (output_color_transform) { - // Yes color transform, blit color managed - ColorTransformJob job; + // No color transform, just blit + ShaderJob job; + job.Insert(QStringLiteral("ove_maintex"), NodeValue(NodeValue::kTexture, QVariant::fromValue(texture))); + job.Insert(QStringLiteral("ove_mvpmat"), NodeValue(NodeValue::kMatrix, matrix)); - job.SetColorProcessor(output_color_transform); - job.SetInputTexture(texture); - job.SetInputAlphaAssociation(OLIVE_CONFIG("ReassocLinToNonLin").toBool() ? kAlphaAssociated : kAlphaNone); - job.SetTransformMatrix(matrix); - - render_ctx_->BlitColorManaged(job, blit_tex.get()); - } else { - // No color transform, just blit - ShaderJob job; - job.Insert(QStringLiteral("ove_maintex"), NodeValue(NodeValue::kTexture, QVariant::fromValue(texture))); - job.Insert(QStringLiteral("ove_mvpmat"), NodeValue(NodeValue::kMatrix, matrix)); - - render_ctx_->BlitToTexture(default_shader_, job, blit_tex.get()); - } + render_ctx_->BlitToTexture(default_shader_, job, blit_tex.get()); // Replace texture that we're going to download in the next step texture = blit_tex; @@ -179,8 +179,8 @@ void RenderProcessor::Run() // Save to cache if requested if (!cache.isEmpty()) { rational timebase = ticket_->property("cachetimebase").value(); - QUuid uuid = ticket_->property("cacheuuid").value(); - bool cache_result = FrameHashCache::SaveCacheFrame(cache, uuid, time, timebase, frame); + QString id = ticket_->property("cacheid").toString(); + bool cache_result = FrameHashCache::SaveCacheFrame(cache, id, time, timebase, frame); ticket_->setProperty("cached", cache_result); } } @@ -438,7 +438,7 @@ void RenderProcessor::ProcessVideoFootage(TexturePtr destination, const FootageJ bool frame = decoder->RetrieveVideo(unmanaged_texture, (stream_data.video_type() == VideoParams::kVideoTypeVideo) ? input_time : Decoder::kAnyTimecode, p, GetCancelPointer()); - if (frame) { + if (!IsCancelled() && frame) { // We convert to our rendering pixel format, since that will always be float-based which // is necessary for correct color conversion ColorProcessorPtr processor = ColorProcessor::Create(color_manager, diff --git a/app/render/videoparams.cpp b/app/render/videoparams.cpp index c40ff3684..1cea23b29 100644 --- a/app/render/videoparams.cpp +++ b/app/render/videoparams.cpp @@ -235,6 +235,21 @@ QString VideoParams::GetFormatName(VideoParams::Format format) return QCoreApplication::translate("VideoParams", "Unknown (0x%1)").arg(format, 0, 16); } +int VideoParams::GetDividerForTargetResolution(int src_width, int src_height, int dst_width, int dst_height) +{ + int divider = 0; + int test_width, test_height; + + do { + divider++; + + test_width = VideoParams::GetScaledDimension(src_width, divider); + test_height = VideoParams::GetScaledDimension(src_height, divider); + } while (test_width > dst_width || test_height > dst_height); + + return divider; +} + void VideoParams::calculate_effective_size() { effective_width_ = GetScaledDimension(width(), divider_); diff --git a/app/render/videoparams.h b/app/render/videoparams.h index 564985dc4..fcb1e804c 100644 --- a/app/render/videoparams.h +++ b/app/render/videoparams.h @@ -235,6 +235,8 @@ public: static QString GetFormatName(Format format); + static int GetDividerForTargetResolution(int src_width, int src_height, int dst_width, int dst_height); + static const int kInternalChannelCount; static const rational kPixelAspectSquare; diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index 2968f6d5a..df2148cab 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -58,19 +58,15 @@ bool RenderTask::Render(ColorManager* manager, // 50%, which makes the progress bar look weird to the uninitiated //total_length += r.length().toDouble(); - rational r = range.in(); - while (r != range.out()) { - rational end = qMin(range.out(), r+1); - TimeRange this_range(r, end); + RenderManager::RenderAudioParams rap(viewer_->GetConnectedSampleOutput(), + range, + audio_params_); - RenderTicketWatcher* watcher = new RenderTicketWatcher(); - watcher->setProperty("range", QVariant::fromValue(this_range)); - PrepareWatcher(watcher, &watcher_thread); - IncrementRunningTickets(); - watcher->SetTicket(RenderManager::instance()->RenderAudio(viewer_->GetConnectedSampleOutput(), this_range, audio_params_, mode, false)); - - r = end; - } + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + watcher->setProperty("range", QVariant::fromValue(range)); + PrepareWatcher(watcher, &watcher_thread); + IncrementRunningTickets(); + watcher->SetTicket(RenderManager::instance()->RenderAudio(rap)); } // Look up hashes @@ -277,15 +273,23 @@ void RenderTask::StartTicket(QThread* watcher_thread, ColorManager* manager, const QSize &force_size, const QMatrix4x4 &force_matrix, VideoParams::Format force_format, ColorProcessorPtr force_color_output) { + RenderManager::RenderVideoParams rvp(viewer_->GetConnectedTextureOutput(), video_params_, audio_params_, + time, manager); + + rvp.force_size = force_size; + rvp.force_matrix = force_matrix; + rvp.force_format = force_format; + rvp.force_color_output = force_color_output; + + if (cache) { + rvp.AddCache(cache); + } + RenderTicketWatcher* watcher = new RenderTicketWatcher(); watcher->setProperty("time", QVariant::fromValue(time)); PrepareWatcher(watcher, watcher_thread); IncrementRunningTickets(); - watcher->SetTicket(RenderManager::instance()->RenderFrame(viewer_->GetConnectedTextureOutput(), manager, time, - mode, video_params_, audio_params_, - force_size, force_matrix, - force_format, force_color_output, - cache)); + watcher->SetTicket(RenderManager::instance()->RenderFrame(rvp)); } void RenderTask::TicketDone(RenderTicketWatcher* watcher) diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index aeb1a0678..59a61addc 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -1093,6 +1093,11 @@ void TimelineWidget::ShowContextMenu() toggle_audio_units->setChecked(use_audio_time_units_); connect(toggle_audio_units, &QAction::triggered, this, &TimelineWidget::SetUseAudioTimeUnits); + QAction* show_thumbnails = menu.addAction(tr("Show Thumbnails")); + show_thumbnails->setCheckable(true); + show_thumbnails->setChecked(views_.first()->view()->GetShowThumbnails()); + connect(show_thumbnails, &QAction::triggered, this, &TimelineWidget::SetViewThumbnailsEnabled); + QAction* show_waveforms = menu.addAction(tr("Show Waveforms")); show_waveforms->setCheckable(true); show_waveforms->setChecked(views_.first()->view()->GetShowWaveforms()); @@ -1167,6 +1172,13 @@ void TimelineWidget::SetViewWaveformsEnabled(bool e) } } +void TimelineWidget::SetViewThumbnailsEnabled(bool e) +{ + foreach (TimelineAndTrackView* tview, views_) { + tview->view()->SetShowThumbnails(e); + } +} + void TimelineWidget::FrameRateChanged() { SetTimebase(GetConnectedNode()->GetVideoParams().frame_rate_as_time_base()); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index c7a38dfaa..c1ec0b292 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -415,6 +415,8 @@ private slots: void SetViewWaveformsEnabled(bool e); + void SetViewThumbnailsEnabled(bool e); + void FrameRateChanged(); void SampleRateChanged(); diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index f7fd1a637..6ab6ea736 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -524,18 +524,27 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q // Draw clip thumbnails if (clip->GetTrackType() == Track::kVideo && show_thumbnails_ && preview_rect.height() > r.height()/3) { - const int kTempThumbWidth = 120; - const int kTempThumbHeight = 68; + if (const FrameHashCache *thumbs = clip->thumbnails()) { + QRect thumb_rect; + painter->setClipRect(preview_rect); + painter->setRenderHint(QPainter::SmoothPixmapTransform); + for (int i=preview_rect.left(); iparent()->GetAudioParams().sample_rate_as_time_base()) + media_in; + QString thumbnail = thumbs->GetValidCacheFilename(time_here); - QRect thumb_rect; - painter->setClipRect(preview_rect); - for (int i=preview_rect.left(); ifillRect(thumb_rect, Qt::red); + if (thumbnail.isEmpty()) { + break; + } else { + QImage img; + if (img.load(thumbnail, "jpg")) { + double scale = double(preview_rect.height())/double(img.height()); + thumb_rect = QRect(i, preview_rect.top(), img.width() * scale, preview_rect.height()); + painter->drawImage(thumb_rect, img); + } + } + } + painter->setClipping(false); } - painter->setClipping(false); } // Draw waveform diff --git a/app/widget/timelinewidget/view/timelineview.h b/app/widget/timelinewidget/view/timelineview.h index 28a8cb799..9dea755f2 100644 --- a/app/widget/timelinewidget/view/timelineview.h +++ b/app/widget/timelinewidget/view/timelineview.h @@ -84,6 +84,17 @@ public: viewport()->update(); } + bool GetShowThumbnails() const + { + return show_thumbnails_; + } + + void SetShowThumbnails(bool e) + { + show_thumbnails_ = e; + viewport()->update(); + } + signals: void MousePressed(TimelineViewMouseEvent* event); void MouseMoved(TimelineViewMouseEvent* event); diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 5d6980bcb..7ee96fc20 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -148,6 +148,7 @@ ViewerWidget::ViewerWidget(QWidget *parent) : setAcceptDrops(true); auto_cacher_ = new PreviewAutoCacher(this); + connect(display_widget_, &ViewerDisplayWidget::ColorProcessorChanged, auto_cacher_, &PreviewAutoCacher::SetDisplayColorProcessor); connect(Core::instance(), &Core::ColorPickerEnabled, this, &ViewerWidget::SetSignalCursorColorEnabled); connect(this, &ViewerWidget::CursorColor, Core::instance(), &Core::ColorPickerColorEmitted); @@ -686,11 +687,6 @@ void ViewerWidget::UpdateTextureFromNode() connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::RendererGeneratedFrame); nonqueue_watchers_.append(watcher); - // Clear queue because we want this frame more than any others - if (!GetConnectedNode()->video_frame_cache()->IsAutomatic() && !auto_cacher_->IsRenderingCustomRange()) { - ClearVideoAutoCacherQueue(); - } - watcher->SetTicket(GetFrame(time, RenderTicketPriority::kHigh)); } else { // There is definitely no frame here, we can immediately flip to showing nothing @@ -718,8 +714,8 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) foreach (ViewerWidget* viewer, instances_) { if (viewer != this) { viewer->PauseInternal(); - viewer->auto_cacher_->SetRendersPaused(true); } + viewer->auto_cacher_->SetRendersPaused(true); } // Disarm recording if armed