diff --git a/app/node/processor/renderer/renderer.cpp b/app/node/processor/renderer/renderer.cpp index 2fce8ae46..38788fa9d 100644 --- a/app/node/processor/renderer/renderer.cpp +++ b/app/node/processor/renderer/renderer.cpp @@ -36,7 +36,8 @@ RendererProcessor::RendererProcessor() : width_(0), height_(0), divider_(1), - caching_(false) + caching_(false), + starting_(false) { texture_input_ = new NodeInput("tex_in"); texture_input_->add_data_input(NodeInput::kTexture); diff --git a/app/node/processor/renderer/renderer.h b/app/node/processor/renderer/renderer.h index 637d229f9..521e0e82a 100644 --- a/app/node/processor/renderer/renderer.h +++ b/app/node/processor/renderer/renderer.h @@ -207,6 +207,8 @@ private: QList deferred_maps_; + bool starting_; + private slots: void ThreadCallback(RenderTexturePtr texture, const rational& time, const QByteArray& hash); diff --git a/app/node/processor/renderer/rendermanager.cpp b/app/node/processor/renderer/rendermanager.cpp new file mode 100644 index 000000000..09342e301 --- /dev/null +++ b/app/node/processor/renderer/rendermanager.cpp @@ -0,0 +1,477 @@ +/*** + + 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 "rendermanager.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "common/filefunctions.h" +#include "render/pixelservice.h" + +RenderManager* RenderManager::instance_ = nullptr; + +RenderManager::RenderManager() : + started_(false), + width_(0), + height_(0), + divider_(1), + caching_(false), + texture_input_(nullptr) +{ +} + +void RenderManager::CreateInstance() +{ + if (instance_ == nullptr) { + instance_ = new RenderManager(); + } +} + +void RenderManager::DestroyInstance() +{ + delete instance_; + instance_ = nullptr; +} + +RenderManager *RenderManager::instance() +{ + return instance_; +} + +void RenderManager::SetCacheName(const QString &s) +{ + cache_name_ = s; + cache_time_ = QDateTime::currentMSecsSinceEpoch(); + + GenerateCacheIDInternal(); +} + +void RenderManager::InvalidateCache(const rational &start_range, const rational &end_range, const rational& last_requested_time) +{ + if (timebase_.isNull()) { + return; + } + + + + qDebug() << "Cache invalidated between" + << start_range.toDouble() + << "and" + << end_range.toDouble(); + + // Snap start_range to timebase + double start_range_dbl = start_range.toDouble(); + double start_range_numf = start_range_dbl * static_cast(timebase_.denominator()); + int64_t start_range_numround = qFloor(start_range_numf/static_cast(timebase_.numerator())) * timebase_.numerator(); + rational true_start_range(start_range_numround, timebase_.denominator()); + + for (rational r=true_start_range;r<=end_range;r+=timebase_) { + // Try to order the queue from closest to the playhead to furthest + rational diff = r - last_requested_time; + + if (diff < 0) { + // FIXME: Hardcoded number + // If the number is before the playhead, we still prioritize its closeness but not nearly as much (5:1 in this + // example) + diff = qAbs(diff) * 5; + } + + bool contains = false; + bool added = false; + QLinkedList::iterator insert_iterator; + + for (QLinkedList::iterator i = cache_queue_.begin();i != cache_queue_.end();i++) { + rational compare = *i; + + if (!added) { + rational compare_diff = compare - last_requested_time; + + if (compare_diff > diff) { + insert_iterator = i; + added = true; + } + } + + if (compare == r) { + contains = true; + break; + } + } + + if (!contains) { + if (added) { + cache_queue_.insert(insert_iterator, r); + } else { + cache_queue_.append(r); + } + } + } + + CacheNext(); +} + +void RenderManager::SetTimebase(const rational &timebase) +{ + timebase_ = timebase; + timebase_dbl_ = timebase_.toDouble(); +} + +void RenderManager::SetParameters(const int &width, + const int &height, + const olive::PixelFormat &format, + const olive::RenderMode &mode, + NodeInput* input, + const int& divider) +{ + // Since we're changing parameters, all the existing threads are invalid and must be removed. They will start again + // next time this Node has to process anything. + Stop(); + + // Set new parameters + width_ = width; + height_ = height; + format_ = format; + mode_ = mode; + texture_input_ = input; + + // divider's default value is 0, so we can assume if it's 0 a divider wasn't specified + if (divider > 0) { + divider_ = divider; + } + + CalculateEffectiveDimensions(); + + // Regenerate the cache ID + GenerateCacheIDInternal(); +} + +void RenderManager::SetDivider(const int ÷r) +{ + Q_ASSERT(divider_ > 0); + + Stop(); + + divider_ = divider; + + CalculateEffectiveDimensions(); + + // Regenerate the cache ID + GenerateCacheIDInternal(); +} + +void RenderManager::Start() +{ + if (started_) { + return; + } + + QOpenGLContext* ctx = QOpenGLContext::currentContext(); + + int background_thread_count = QThread::idealThreadCount(); + + // Some OpenGL implementations (notably wgl) require the context not to be current before sharing + QSurface* old_surface = ctx->surface(); + ctx->doneCurrent(); + + threads_.resize(background_thread_count); + + for (int i=0;i(this, ctx, effective_width_, effective_height_, divider_, format_, mode_); + threads_[i]->StartThread(QThread::LowPriority); + + // Ensure this connection is "Queued" so that it always runs in this object's threaded rather than any of the + // other threads + connect(threads_.at(i).get(), + SIGNAL(RequestSibling(NodeDependency)), + this, + SLOT(ThreadRequestSibling(NodeDependency)), + Qt::QueuedConnection); + } + + // Connect first thread (master thread) to the callback + connect(threads_.first().get(), + SIGNAL(CachedFrame(RenderTexturePtr, const rational&, const QByteArray&)), + this, + SLOT(ThreadCallback(RenderTexturePtr, const rational&, const QByteArray&)), + Qt::QueuedConnection); + connect(threads_.first().get(), + SIGNAL(FrameSkipped(const rational&, const QByteArray&)), + this, + SLOT(ThreadSkippedFrame(const rational&, const QByteArray&)), + Qt::QueuedConnection); + + download_threads_.resize(background_thread_count); + + for (int i=0;i(ctx, effective_width_, effective_height_, divider_, format_, mode_); + download_threads_[i]->StartThread(QThread::LowPriority); + + connect(download_threads_[i].get(), + SIGNAL(Downloaded(const QByteArray&)), + this, + SLOT(DownloadThreadComplete(const QByteArray&)), + Qt::QueuedConnection); + } + + last_download_thread_ = 0; + + // Restore context now that thread creation is complete + ctx->makeCurrent(old_surface); + + cache_frame_load_buffer_.resize(PixelService::GetBufferSize(format_, effective_width_, effective_height_)); + + started_ = true; +} + +void RenderManager::Stop() +{ + if (!started_) { + return; + } + + started_ = false; + + foreach (RendererDownloadThreadPtr download_thread_, download_threads_) { + download_thread_->Cancel(); + } + download_threads_.clear(); + + foreach (RendererProcessThreadPtr process_thread, threads_) { + process_thread->Cancel(); + } + threads_.clear(); + + cache_frame_load_buffer_.clear(); +} + +void RenderManager::GenerateCacheIDInternal() +{ + if (cache_name_.isEmpty() || effective_width_ == 0 || effective_height_ == 0) { + return; + } + + // Generate an ID that is more or less guaranteed to be unique to this Sequence + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(cache_name_.toUtf8()); + hash.addData(QString::number(cache_time_).toUtf8()); + hash.addData(QString::number(width_).toUtf8()); + hash.addData(QString::number(height_).toUtf8()); + hash.addData(QString::number(format_).toUtf8()); + hash.addData(QString::number(divider_).toUtf8()); + + QByteArray bytes = hash.result(); + cache_id_ = bytes.toHex(); +} + +void RenderManager::CacheNext() +{ + if (texture_input_ == nullptr || cache_queue_.isEmpty() || !texture_input_->IsConnected() || caching_) { + return; + } + + // Make sure cache has started + Start(); + + rational cache_frame = cache_queue_.takeFirst(); + + qDebug() << "Caching" << cache_frame.toDouble(); + + threads_.first()->Queue(NodeDependency(texture_input_->get_connected_output(), cache_frame), true, false); + + caching_ = true; +} + +QString RenderManager::CachePathName(const QByteArray &hash) +{ + QDir this_cache_dir = QDir(GetMediaCacheLocation()).filePath(cache_id_); + this_cache_dir.mkpath("."); + + QString filename = QString("%1.exr").arg(QString(hash.toHex())); + + return this_cache_dir.filePath(filename); +} + +void RenderManager::DeferMap(const rational &time, const QByteArray &hash) +{ + deferred_maps_.append({time, hash}); +} + +bool RenderManager::HasHash(const QByteArray &hash) +{ + return QFileInfo::exists(CachePathName(hash)); +} + +bool RenderManager::IsCaching(const QByteArray &hash) +{ + cache_hash_list_mutex_.lock(); + + bool is_caching = cache_hash_list_.contains(hash); + + cache_hash_list_mutex_.unlock(); + + return is_caching; +} + +bool RenderManager::TryCache(const QByteArray &hash) +{ + cache_hash_list_mutex_.lock(); + + bool is_caching = cache_hash_list_.contains(hash); + + if (!is_caching) { + cache_hash_list_.append(hash); + } + + cache_hash_list_mutex_.unlock(); + + return !is_caching; +} + +void RenderManager::RetrieveImage(RenderTexturePtr destination_texture, const rational &time) +{ + if (cache_id_.isEmpty()) { + qWarning() << "RendererProcessor has no cache ID"; + return; + } + + if (timebase_.isNull()) { + qWarning() << "RendererProcessor has no timebase"; + return; + } + + // Find frame in map + if (time_hash_map_.contains(time)) { + QString fn = CachePathName(time_hash_map_[time]); + + if (QFileInfo::exists(fn)) { + auto in = OIIO::ImageInput::open(fn.toStdString()); + + if (in) { + in->read_image(PixelService::GetPixelFormatInfo(format_).oiio_desc, cache_frame_load_buffer_.data()); + + in->close(); + + destination_texture->Upload(cache_frame_load_buffer_.data()); + } else { + qWarning() << "OIIO Error:" << OIIO::geterror().c_str(); + } + } + } +} + +void RenderManager::CalculateEffectiveDimensions() +{ + effective_width_ = width_ / divider_; + effective_height_ = height_ / divider_; +} + +void RenderManager::ThreadCallback(RenderTexturePtr texture, const rational& time, const QByteArray& hash) +{ + // Threads are all done now, time to proceed + caching_ = false; + + DeferMap(time, hash); + + if (texture != nullptr) { + // We received a texture, time to start downloading it + QString fn = CachePathName(hash); + + download_threads_[last_download_thread_%download_threads_.size()]->Queue(texture, + fn, + hash); + + last_download_thread_++; + } else { + // There was no texture here, we must update the viewer + DownloadThreadComplete(hash); + } + + emit FrameReadyWithTexture(time, texture); + + CacheNext(); +} + +void RenderManager::ThreadRequestSibling(NodeDependency dep) +{ + // Try to queue another thread to run this dep in advance + for (int i=1;iQueue(dep, false, true)) { + return; + } + } +} + +void RenderManager::ThreadSkippedFrame(const rational& time, const QByteArray& hash) +{ + caching_ = false; + + DeferMap(time, hash); + + if (!IsCaching(hash)) { + DownloadThreadComplete(hash); + + // Signal output to update value + emit FrameReady(time); + } + + CacheNext(); +} + +void RenderManager::DownloadThreadComplete(const QByteArray &hash) +{ + cache_hash_list_mutex_.lock(); + cache_hash_list_.removeAll(hash); + cache_hash_list_mutex_.unlock(); + + for (int i=0;i(QThread::currentThread()); +} + +RenderInstance *RenderManager::CurrentInstance() +{ + RendererThreadBase* thread = CurrentThread(); + + if (thread != nullptr) { + return thread->render_instance(); + } + + return nullptr; +} diff --git a/app/node/processor/renderer/rendermanager.h b/app/node/processor/renderer/rendermanager.h new file mode 100644 index 000000000..f25ce2e59 --- /dev/null +++ b/app/node/processor/renderer/rendermanager.h @@ -0,0 +1,218 @@ +/*** + + 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 RENDERMANAGER_H +#define RENDERMANAGER_H + +#include +#include + +#include "node/node.h" +#include "render/pixelformat.h" +#include "render/rendermodes.h" +#include "rendererdownloadthread.h" +#include "rendererprocessthread.h" + +/** + * @brief A multithreaded OpenGL based renderer for node systems + */ +class RenderManager : public QObject +{ + Q_OBJECT +public: + /** + * @brief Renderer Constructor + * + * Constructing a Renderer object will not start any threads/backend on its own. Use Start() to do this and Stop() + * when the Renderer is about to be destroyed. + */ + RenderManager(); + + static void CreateInstance(); + + static void DestroyInstance(); + + static RenderManager* instance(); + + void SetCacheName(const QString& s); + + void SetTimebase(const rational& timebase); + + void InvalidateCache(const rational &start_range, const rational &end_range, const rational &last_requested_time); + + /** + * @brief Set parameters of the Renderer + * + * The Renderer owns the buffers that are used in the rendering process and this function sets the kind of buffers + * to use. The Renderer must be stopped when calling this function. + * + * @param width + * + * Buffer width + * + * @param height + * + * Buffer height + * + * @param format + * + * Buffer pixel format + */ + void SetParameters(const int& width, + const int& height, + const olive::PixelFormat& format, + const olive::RenderMode& mode, + NodeInput *input, + const int ÷r = 0); + + void SetDivider(const int& divider); + + /** + * @brief Return whether a frame with this hash already exists + */ + bool HasHash(const QByteArray& hash); + + /** + * @brief Return whether a frame is currently being cached + */ + bool IsCaching(const QByteArray& hash); + + /** + * @brief Check if a frame is currently being cached, and if not reserve it + */ + bool TryCache(const QByteArray& hash); + + /** + * @brief Retrieve the cached frame at `time` and upload it to `destination_texture` + */ + void RetrieveImage(RenderTexturePtr destination_texture, const rational& time); + + /** + * @brief Allocate and start the multithreaded backend + */ + void Start(); + + /** + * @brief Terminate and deallocate the multithreaded backend + */ + void Stop(); + + /** + * @brief Return current instance of a RenderThread (or nullptr if there is none) + * + * This function attempts a dynamic_cast on QThread::currentThread() to RendererThread, which will return nullptr if + * the cast fails (e.g. if this function is called from the main thread rather than a RendererThread). + */ + static RendererThreadBase* CurrentThread(); + + static RenderInstance* CurrentInstance(); + +signals: + void FrameReady(rational time); + + void FrameReadyWithTexture(rational time, RenderTexturePtr texture); + +private: + static RenderManager* instance_; + + struct HashTimeMapping { + rational time; + QByteArray hash; + }; + + /** + * @brief Internal function for generating the cache ID + */ + void GenerateCacheIDInternal(); + + /** + * @brief Function called when there are frames in the queue to cache + * + * This function is NOT thread-safe and should only be called in the main thread. + */ + void CacheNext(); + + bool ShouldPushTexture(const rational &time); + + /** + * @brief Return the path of the cached image at this time + */ + QString CachePathName(const QByteArray &hash); + + void DeferMap(const rational &time, const QByteArray &hash); + + /** + * @brief Internal list of RenderProcessThreads + */ + QVector threads_; + + /** + * @brief Internal variable that contains whether the Renderer has started or not + */ + bool started_; + + int width_; + int height_; + + void CalculateEffectiveDimensions(); + + int divider_; + int effective_width_; + int effective_height_; + + olive::PixelFormat format_; + + olive::RenderMode mode_; + + rational timebase_; + double timebase_dbl_; + + QLinkedList cache_queue_; + QString cache_name_; + qint64 cache_time_; + QString cache_id_; + + bool caching_; + QVector cache_frame_load_buffer_; + + QVector download_threads_; + int last_download_thread_; + + QMap time_hash_map_; + + QMutex cache_hash_list_mutex_; + QVector cache_hash_list_; + + QList deferred_maps_; + + NodeInput* texture_input_; + +private slots: + void ThreadCallback(RenderTexturePtr texture, const rational& time, const QByteArray& hash); + + void ThreadRequestSibling(NodeDependency dep); + + void ThreadSkippedFrame(const rational &time, const QByteArray &hash); + + void DownloadThreadComplete(const QByteArray &hash); + +}; + +#endif // RENDERMANAGER_H diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index 51b9e0f44..1ea68b2a0 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -47,6 +47,8 @@ TimelinePanel::TimelinePanel(QWidget *parent) : connect(view_, SIGNAL(TimebaseChanged(const rational&)), this, SLOT(SetTimebase(const rational&))); connect(ruler_, SIGNAL(TimeChanged(const int64_t&)), view_, SLOT(SetTime(const int64_t&))); connect(view_, SIGNAL(TimeChanged(const int64_t&)), ruler_, SLOT(SetTime(const int64_t&))); + connect(view_, SIGNAL(TimeChanged(const int64_t&)), this, SIGNAL(TimeChanged(const int64_t&))); + connect(ruler_, SIGNAL(TimeChanged(const int64_t&)), this, SIGNAL(TimeChanged(const int64_t&))); // FIXME: Magic number SetScale(90.0); @@ -67,6 +69,12 @@ void TimelinePanel::SetTimebase(const rational &timebase) view_->SetTimebase(timebase); } +void TimelinePanel::SetTime(const int64_t ×tamp) +{ + ruler_->SetTime(timestamp); + view_->SetTime(timestamp); +} + TimelineView *TimelinePanel::view() { return view_; diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index 8c9aad050..2e38ec277 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -62,10 +62,13 @@ public: public slots: void SetTimebase(const rational& timebase); + void SetTime(const int64_t& timestamp); + protected: virtual void changeEvent(QEvent* e) override; signals: + void TimeChanged(const int64_t& time); private: void Retranslate(); diff --git a/app/panel/viewer/viewer.cpp b/app/panel/viewer/viewer.cpp index 8064f602a..4f65e070d 100644 --- a/app/panel/viewer/viewer.cpp +++ b/app/panel/viewer/viewer.cpp @@ -28,7 +28,7 @@ ViewerPanel::ViewerPanel(QWidget *parent) : // QObject system handles deleting this viewer_ = new ViewerWidget(this); - connect(viewer_, SIGNAL(TimeChanged(const rational&)), this, SIGNAL(TimeChanged(const rational&))); + connect(viewer_, SIGNAL(TimeChanged(const int64_t&)), this, SIGNAL(TimeChanged(const int64_t&))); // Set ViewerWidget as the central widget setWidget(viewer_); @@ -107,6 +107,11 @@ rational ViewerPanel::GetTime() return viewer_->GetTime(); } +void ViewerPanel::SetTime(const int64_t ×tamp) +{ + viewer_->SetTime(timestamp); +} + void ViewerPanel::SetTexture(RenderTexturePtr tex) { viewer_->SetTexture(tex); diff --git a/app/panel/viewer/viewer.h b/app/panel/viewer/viewer.h index a6780a505..0a4e159c1 100644 --- a/app/panel/viewer/viewer.h +++ b/app/panel/viewer/viewer.h @@ -72,11 +72,13 @@ public slots: */ void SetTexture(RenderTexturePtr tex); + void SetTime(const int64_t& timestamp); + protected: virtual void changeEvent(QEvent* e) override; signals: - void TimeChanged(const rational&); + void TimeChanged(const int64_t&); private: void Retranslate(); diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index a9c0326db..db24c99f9 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -40,9 +40,18 @@ Sequence::Sequence() : void Sequence::Open(SequencePtr sequence) { - olive::panel_manager->MostRecentlyFocused()->ConnectViewerNode(sequence->viewer_output_); - olive::panel_manager->MostRecentlyFocused()->ConnectTimelineNode(sequence->timeline_output_); - olive::panel_manager->MostRecentlyFocused()->SetGraph(sequence.get()); + // FIXME: This is fairly "hardcoded" behavior + + ViewerPanel* viewer_panel = olive::panel_manager->MostRecentlyFocused(); + TimelinePanel* timeline_panel = olive::panel_manager->MostRecentlyFocused(); + NodePanel* node_panel = olive::panel_manager->MostRecentlyFocused(); + + viewer_panel->ConnectViewerNode(sequence->viewer_output_); + timeline_panel->ConnectTimelineNode(sequence->timeline_output_); + node_panel->SetGraph(sequence.get()); + + connect(timeline_panel, SIGNAL(TimeChanged(const int64_t&)), viewer_panel, SLOT(SetTime(const int64_t&))); + connect(viewer_panel, SIGNAL(TimeChanged(const int64_t&)), timeline_panel, SLOT(SetTime(const int64_t&))); } void Sequence::AddDefaultNodes() @@ -76,6 +85,7 @@ void Sequence::AddDefaultNodes() NodeParam::ConnectEdge(timeline_output_->length_output(), renderer_processor_->length_input()); // Update the timebase on these nodes + renderer_processor_->SetCacheName(name()); set_video_time_base(video_time_base_); update_video_parameters(); } diff --git a/app/render/CMakeLists.txt b/app/render/CMakeLists.txt index 1d783b95f..3a0a6a23b 100644 --- a/app/render/CMakeLists.txt +++ b/app/render/CMakeLists.txt @@ -29,8 +29,6 @@ set(OLIVE_SOURCES render/rendermodes.h render/renderframebuffer.h render/renderframebuffer.cpp - render/renderservice.h - render/renderservice.cpp render/rendertexture.h render/rendertexture.cpp render/sampleformat.h diff --git a/app/render/renderservice.cpp b/app/render/renderservice.cpp deleted file mode 100644 index 29ec74a37..000000000 --- a/app/render/renderservice.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include "renderservice.h" - -RenderService* RenderService::render_service_; - -RenderService::RenderService() -{ - -} diff --git a/app/render/renderservice.h b/app/render/renderservice.h deleted file mode 100644 index fc55c1cbe..000000000 --- a/app/render/renderservice.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef RENDERSERVICE_H -#define RENDERSERVICE_H - - -class RenderService -{ -public: - RenderService(); - - static RenderService* CreateInstance(); - static RenderService* GetInstance(); - static RenderService* DestroyInstance(); - -private: - static RenderService* render_service_; - -}; - -#endif // RENDERSERVICE_H diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 9f4a7478d..4c27d1427 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -174,7 +174,7 @@ void ViewerWidget::UpdateTimeInternal(int64_t i) UpdateTextureFromNode(time_set); } - emit TimeChanged(time_set); + emit TimeChanged(i); } void ViewerWidget::UpdateTextureFromNode(const rational& time) diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 6ff0e9d4c..362f973d2 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -94,7 +94,7 @@ public slots: void ShuttleRight(); signals: - void TimeChanged(const rational&); + void TimeChanged(const int64_t&); protected: virtual void resizeEvent(QResizeEvent *event) override;