Previously we had no disk management whatsoever, so we cleared the cache on every close just to prevent clogging up tester disk space. Now that we are implementing disk management, there are better things to do on close regarding disk cache. However, some users may still wish for the app to delete the cache on close, so it's provided as an option.
317 lines
9.1 KiB
C++
317 lines
9.1 KiB
C++
/***
|
|
|
|
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 <http://www.gnu.org/licenses/>.
|
|
|
|
***/
|
|
|
|
#include "videorenderbackend.h"
|
|
|
|
#include <OpenImageIO/imageio.h>
|
|
#include <QApplication>
|
|
#include <QCryptographicHash>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QtMath>
|
|
|
|
#include "common/timecodefunctions.h"
|
|
#include "render/diskmanager.h"
|
|
#include "render/pixelservice.h"
|
|
#include "videorenderworker.h"
|
|
|
|
VideoRenderBackend::VideoRenderBackend(QObject *parent) :
|
|
RenderBackend(parent),
|
|
operating_mode_(VideoRenderWorker::kHashRenderCache),
|
|
only_signal_last_frame_requested_(true)
|
|
{
|
|
}
|
|
|
|
bool VideoRenderBackend::InitInternal()
|
|
{
|
|
cache_frame_load_buffer_.resize(PixelService::GetBufferSize(params_.format(), params_.effective_width(), params_.effective_height()));
|
|
return true;
|
|
}
|
|
|
|
void VideoRenderBackend::CloseInternal()
|
|
{
|
|
cache_frame_load_buffer_.clear();
|
|
}
|
|
|
|
void VideoRenderBackend::ConnectViewer(ViewerOutput *node)
|
|
{
|
|
connect(node, &ViewerOutput::VideoChangedBetween, this, &VideoRenderBackend::InvalidateCache);
|
|
connect(node, &ViewerOutput::VideoGraphChanged, this, &VideoRenderBackend::QueueRecompile);
|
|
connect(node, &ViewerOutput::LengthChanged, this, &VideoRenderBackend::TruncateFrameCacheLength);
|
|
}
|
|
|
|
void VideoRenderBackend::DisconnectViewer(ViewerOutput *node)
|
|
{
|
|
disconnect(node, &ViewerOutput::VideoChangedBetween, this, &VideoRenderBackend::InvalidateCache);
|
|
disconnect(node, &ViewerOutput::VideoGraphChanged, this, &VideoRenderBackend::QueueRecompile);
|
|
disconnect(node, &ViewerOutput::LengthChanged, this, &VideoRenderBackend::TruncateFrameCacheLength);
|
|
|
|
frame_cache_.Clear();
|
|
}
|
|
|
|
const VideoRenderingParams &VideoRenderBackend::params() const
|
|
{
|
|
return params_;
|
|
}
|
|
|
|
void VideoRenderBackend::SetParameters(const VideoRenderingParams& params)
|
|
{
|
|
if (!AllProcessorsAreAvailable()) {
|
|
qCritical() << "Attempted to set parameters on a backend whose workers are still busy";
|
|
return;
|
|
}
|
|
|
|
// Set new parameters
|
|
params_ = params;
|
|
|
|
// Set params on all processors
|
|
foreach (RenderWorker* worker, processors_) {
|
|
static_cast<VideoRenderWorker*>(worker)->SetParameters(params_);
|
|
}
|
|
|
|
// Regenerate the cache ID
|
|
RegenerateCacheID();
|
|
}
|
|
|
|
void VideoRenderBackend::SetOperatingMode(const VideoRenderWorker::OperatingMode &mode)
|
|
{
|
|
if (!AllProcessorsAreAvailable()) {
|
|
qCritical() << "Attempted to set operating mode on a backend whose workers are still busy";
|
|
return;
|
|
}
|
|
|
|
operating_mode_ = mode;
|
|
|
|
foreach (RenderWorker* worker, processors_) {
|
|
static_cast<VideoRenderWorker*>(worker)->SetOperatingMode(operating_mode_);
|
|
}
|
|
}
|
|
|
|
void VideoRenderBackend::SetOnlySignalLastFrameRequested(bool enabled)
|
|
{
|
|
only_signal_last_frame_requested_ = enabled;
|
|
}
|
|
|
|
bool VideoRenderBackend::IsRendered(const rational &time) const
|
|
{
|
|
TimeRange range(time, time);
|
|
|
|
return !TimeIsQueued(range) && !render_job_info_.contains(range);
|
|
}
|
|
|
|
bool VideoRenderBackend::GenerateCacheIDInternal(QCryptographicHash& hash)
|
|
{
|
|
if (!params_.is_valid()) {
|
|
return false;
|
|
}
|
|
|
|
// Generate an ID that is more or less guaranteed to be unique to this Sequence
|
|
hash.addData(QString::number(params_.width()).toUtf8());
|
|
hash.addData(QString::number(params_.height()).toUtf8());
|
|
hash.addData(QString::number(params_.format()).toUtf8());
|
|
hash.addData(QString::number(params_.divider()).toUtf8());
|
|
|
|
return true;
|
|
}
|
|
|
|
void VideoRenderBackend::CacheIDChangedEvent(const QString &id)
|
|
{
|
|
frame_cache_.SetCacheID(id);
|
|
}
|
|
|
|
void VideoRenderBackend::ConnectWorkerToThis(RenderWorker *processor)
|
|
{
|
|
VideoRenderWorker* video_processor = static_cast<VideoRenderWorker*>(processor);
|
|
|
|
video_processor->SetOperatingMode(operating_mode_);
|
|
|
|
connect(video_processor, &VideoRenderWorker::CompletedFrame, this, &VideoRenderBackend::ThreadCompletedFrame);
|
|
connect(video_processor, &VideoRenderWorker::HashAlreadyBeingCached, this, &VideoRenderBackend::ThreadSkippedFrame);
|
|
connect(video_processor, &VideoRenderWorker::CompletedDownload, this, &VideoRenderBackend::ThreadCompletedDownload);
|
|
connect(video_processor, &VideoRenderWorker::HashAlreadyExists, this, &VideoRenderBackend::ThreadHashAlreadyExists);
|
|
}
|
|
|
|
VideoRenderFrameCache *VideoRenderBackend::frame_cache()
|
|
{
|
|
return &frame_cache_;
|
|
}
|
|
|
|
const char *VideoRenderBackend::GetCachedFrame(const rational &time)
|
|
{
|
|
last_time_requested_ = time;
|
|
|
|
if (viewer_node() == nullptr) {
|
|
// Nothing is connected - nothing to show or render
|
|
return nullptr;
|
|
}
|
|
|
|
if (cache_id().isEmpty()) {
|
|
qWarning() << "No cache ID";
|
|
return nullptr;
|
|
}
|
|
|
|
if (!params_.is_valid()) {
|
|
qWarning() << "Invalid parameters";
|
|
return nullptr;
|
|
}
|
|
|
|
// Find frame in map
|
|
QByteArray frame_hash = frame_cache_.TimeToHash(time);
|
|
|
|
if (!frame_hash.isEmpty()) {
|
|
QString fn = frame_cache_.CachePathName(frame_hash);
|
|
|
|
if (QFileInfo::exists(fn)) {
|
|
auto in = OIIO::ImageInput::open(fn.toStdString());
|
|
|
|
if (in) {
|
|
DiskManager::instance()->Accessed(frame_hash);
|
|
|
|
in->read_image(PixelService::GetPixelFormatInfo(params_.format()).oiio_desc, cache_frame_load_buffer_.data());
|
|
|
|
in->close();
|
|
|
|
return cache_frame_load_buffer_.constData();
|
|
} else {
|
|
qWarning() << "OIIO Error:" << OIIO::geterror().c_str();
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
NodeInput *VideoRenderBackend::GetDependentInput()
|
|
{
|
|
return viewer_node()->texture_input();
|
|
}
|
|
|
|
bool VideoRenderBackend::CanRender()
|
|
{
|
|
return params_.is_valid();
|
|
}
|
|
|
|
TimeRange VideoRenderBackend::PopNextFrameFromQueue()
|
|
{
|
|
TimeRange range = cache_queue_.first();
|
|
|
|
// Snap the range to a single discrete frame
|
|
rational snapped_in = Timecode::snap_time_to_timebase(range.in(), params_.time_base());
|
|
|
|
// Check if the range starts earlier, in which case we should render that frame instead
|
|
if (range.in() < snapped_in) {
|
|
snapped_in -= params_.time_base();
|
|
}
|
|
|
|
// Remove this particular frame from the queue
|
|
cache_queue_.RemoveTimeRange(TimeRange(snapped_in, snapped_in + params_.time_base()));
|
|
|
|
// Return the snapped frame
|
|
return TimeRange(snapped_in, snapped_in);
|
|
}
|
|
|
|
void VideoRenderBackend::ThreadCompletedFrame(NodeDependency path, qint64 job_time, QByteArray hash, QVariant value)
|
|
{
|
|
if (!only_signal_last_frame_requested_ || last_time_requested_ == path.in() || frame_cache_.TimeToHash(last_time_requested_) == hash) {
|
|
EmitCachedFrameReady(path.in(), value, job_time);
|
|
}
|
|
|
|
if (!(operating_mode_ & VideoRenderWorker::kDownloadOnly)) {
|
|
// If we're not downloading, the worker is done here
|
|
SetWorkerBusyState(static_cast<RenderWorker*>(sender()), false);
|
|
|
|
CacheNext();
|
|
}
|
|
}
|
|
|
|
void VideoRenderBackend::ThreadCompletedDownload(NodeDependency dep, qint64 job_time, QByteArray hash)
|
|
{
|
|
SetWorkerBusyState(static_cast<RenderWorker*>(sender()), false);
|
|
|
|
SetFrameHash(dep, hash, job_time);
|
|
|
|
// Register frame with the disk manager
|
|
if (operating_mode_ & VideoRenderWorker::kDownloadOnly) {
|
|
DiskManager::instance()->CreatedFile(frame_cache()->CachePathName(hash), hash);
|
|
}
|
|
|
|
QList<rational> hashes_with_time = frame_cache()->FramesWithHash(hash);
|
|
|
|
foreach (const rational& t, hashes_with_time) {
|
|
emit CachedTimeReady(t, job_time);
|
|
}
|
|
|
|
// Queue up a new frame for this worker
|
|
CacheNext();
|
|
}
|
|
|
|
void VideoRenderBackend::ThreadSkippedFrame(NodeDependency dep, qint64 job_time, QByteArray hash)
|
|
{
|
|
SetWorkerBusyState(static_cast<RenderWorker*>(sender()), false);
|
|
|
|
if (SetFrameHash(dep, hash, job_time)
|
|
&& frame_cache_.HasHash(hash)) {
|
|
emit CachedTimeReady(dep.in(), job_time);
|
|
}
|
|
|
|
// Queue up a new frame for this worker
|
|
CacheNext();
|
|
}
|
|
|
|
void VideoRenderBackend::ThreadHashAlreadyExists(NodeDependency dep, qint64 job_time, QByteArray hash)
|
|
{
|
|
SetWorkerBusyState(static_cast<RenderWorker*>(sender()), false);
|
|
|
|
if (SetFrameHash(dep, hash, job_time)) {
|
|
emit CachedTimeReady(dep.in(), job_time);
|
|
}
|
|
|
|
// Queue up a new frame for this worker
|
|
CacheNext();
|
|
}
|
|
|
|
void VideoRenderBackend::TruncateFrameCacheLength(const rational &length)
|
|
{
|
|
// Remove frames after this time code if it's changed
|
|
frame_cache_.Truncate(length);
|
|
}
|
|
|
|
bool VideoRenderBackend::TimeIsQueued(const TimeRange &time) const
|
|
{
|
|
return cache_queue_.ContainsTimeRange(time);
|
|
}
|
|
|
|
bool VideoRenderBackend::JobIsCurrent(const NodeDependency &dep, const qint64& job_time) const
|
|
{
|
|
return (render_job_info_.value(dep.range()) == job_time && !TimeIsQueued(dep.range()));
|
|
}
|
|
|
|
bool VideoRenderBackend::SetFrameHash(const NodeDependency &dep, const QByteArray &hash, const qint64& job_time)
|
|
{
|
|
if (JobIsCurrent(dep, job_time)) {
|
|
frame_cache_.SetHash(dep.in(), hash);
|
|
render_job_info_.remove(dep.range());
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|