a first attempt at a new render system
This commit is contained in:
@@ -21,6 +21,8 @@ set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
render/audioparams.h
|
||||
render/audioparams.cpp
|
||||
render/audioplaybackcache.h
|
||||
render/audioplaybackcache.cpp
|
||||
render/color.h
|
||||
render/color.cpp
|
||||
render/colormanager.h
|
||||
@@ -29,10 +31,14 @@ set(OLIVE_SOURCES
|
||||
render/colorprocessor.cpp
|
||||
render/diskmanager.h
|
||||
render/diskmanager.cpp
|
||||
render/framehashcache.h
|
||||
render/framehashcache.cpp
|
||||
render/managedcolor.h
|
||||
render/managedcolor.cpp
|
||||
render/pixelformat.h
|
||||
render/pixelformat.cpp
|
||||
render/playbackcache.h
|
||||
render/playbackcache.cpp
|
||||
render/rendermodes.h
|
||||
render/videoparams.h
|
||||
render/videoparams.cpp
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/***
|
||||
|
||||
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 "audioplaybackcache.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include "common/filefunctions.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
AudioPlaybackCache::AudioPlaybackCache()
|
||||
{
|
||||
// FIXME: We just use a randomly generated number, one day this should be matchable to the
|
||||
// project so it can be reused.
|
||||
quint32 r = QRandomGenerator::global()->generate();
|
||||
filename_ = QDir(FileFunctions::GetMediaCacheLocation()).filePath(QString::number(r));
|
||||
filename_.append(QStringLiteral(".pcm"));
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::SetParameters(const AudioRenderingParams ¶ms)
|
||||
{
|
||||
if (params_ == params) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Our current audio cache is unusable, so we truncate it automatically
|
||||
InvalidateAll();
|
||||
|
||||
emit ParametersChanged();
|
||||
}
|
||||
|
||||
const QString &AudioPlaybackCache::GetCacheFilename() const
|
||||
{
|
||||
return filename_;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -18,35 +18,41 @@
|
||||
|
||||
***/
|
||||
|
||||
#ifndef AUDIOBACKEND_H
|
||||
#define AUDIOBACKEND_H
|
||||
#ifndef AUDIOPLAYBACKCACHE_H
|
||||
#define AUDIOPLAYBACKCACHE_H
|
||||
|
||||
#include <QFile>
|
||||
|
||||
#include "../audiorenderbackend.h"
|
||||
#include "common/timerange.h"
|
||||
#include "codec/samplebuffer.h"
|
||||
#include "render/playbackcache.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class AudioBackend : public AudioRenderBackend
|
||||
class AudioPlaybackCache : public PlaybackCache
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioBackend(QObject* parent = nullptr);
|
||||
AudioPlaybackCache();
|
||||
|
||||
virtual ~AudioBackend() override;
|
||||
const AudioRenderingParams& GetParameters() const {
|
||||
return params_;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual bool InitInternal() override;
|
||||
void SetParameters(const AudioRenderingParams& params);
|
||||
|
||||
virtual void CloseInternal() override;
|
||||
void WritePCM(SampleBufferPtr samples);
|
||||
|
||||
virtual void ConnectWorkerToThis(RenderWorker* worker) override;
|
||||
const QString& GetCacheFilename() const;
|
||||
|
||||
private slots:
|
||||
void ThreadCompletedCache(NodeDependency dep, NodeValueTable data, qint64 job_time);
|
||||
signals:
|
||||
void ParametersChanged();
|
||||
|
||||
private:
|
||||
QString filename_;
|
||||
|
||||
AudioRenderingParams params_;
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // AUDIOBACKEND_H
|
||||
#endif // AUDIOPLAYBACKCACHE_H
|
||||
@@ -14,7 +14,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
add_subdirectory(audio)
|
||||
add_subdirectory(opengl)
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
@@ -27,20 +26,6 @@ set(OLIVE_SOURCES
|
||||
|
||||
render/backend/renderbackend.h
|
||||
render/backend/renderbackend.cpp
|
||||
render/backend/renderworker.h
|
||||
render/backend/renderworker.cpp
|
||||
|
||||
render/backend/audiorenderbackend.h
|
||||
render/backend/audiorenderbackend.cpp
|
||||
render/backend/audiorenderworker.h
|
||||
render/backend/audiorenderworker.cpp
|
||||
|
||||
render/backend/videorenderbackend.h
|
||||
render/backend/videorenderbackend.cpp
|
||||
render/backend/videorenderframecache.h
|
||||
render/backend/videorenderframecache.cpp
|
||||
render/backend/videorenderworker.h
|
||||
render/backend/videorenderworker.cpp
|
||||
|
||||
render/backend/rendercache.h
|
||||
render/backend/colorprocessorcache.h
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# 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/>.
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
render/backend/audio/audiobackend.h
|
||||
render/backend/audio/audiobackend.cpp
|
||||
render/backend/audio/audioworker.h
|
||||
render/backend/audio/audioworker.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -1,116 +0,0 @@
|
||||
/***
|
||||
|
||||
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 "audiobackend.h"
|
||||
|
||||
#include "audioworker.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
AudioBackend::AudioBackend(QObject *parent) :
|
||||
AudioRenderBackend(parent)
|
||||
{
|
||||
}
|
||||
|
||||
AudioBackend::~AudioBackend()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
bool AudioBackend::InitInternal()
|
||||
{
|
||||
// Initiate one thread per CPU core
|
||||
for (int i=0;i<threads().size();i++) {
|
||||
// Create one processor object for each thread
|
||||
AudioWorker* processor = new AudioWorker(&node_copy_map_);
|
||||
processor->SetParameters(params());
|
||||
processors_.append(processor);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioBackend::CloseInternal()
|
||||
{
|
||||
}
|
||||
|
||||
void AudioBackend::ConnectWorkerToThis(RenderWorker *worker)
|
||||
{
|
||||
AudioRenderBackend::ConnectWorkerToThis(worker);
|
||||
|
||||
connect(worker, &RenderWorker::CompletedCache, this, &AudioBackend::ThreadCompletedCache);
|
||||
}
|
||||
|
||||
void AudioBackend::ThreadCompletedCache(NodeDependency dep, NodeValueTable data, qint64 job_time)
|
||||
{
|
||||
SetWorkerBusyState(static_cast<RenderWorker*>(sender()), false);
|
||||
|
||||
if (job_time == render_job_info_.value(dep.range())) {
|
||||
render_job_info_.remove(dep.range());
|
||||
|
||||
SampleBufferPtr cached_sample_ptr = data.Get(NodeParam::kSamples).value<SampleBufferPtr>();
|
||||
|
||||
if (cached_sample_ptr) {
|
||||
QByteArray cached_samples = cached_sample_ptr->toPackedData();
|
||||
|
||||
int offset = params().time_to_bytes(dep.in());
|
||||
int length = params().time_to_bytes(dep.range().length());
|
||||
int out_point = qMin(offset + length, params().time_to_bytes(GetSequenceLength()));
|
||||
|
||||
if (offset < out_point) {
|
||||
if (offset + length > out_point) {
|
||||
length = out_point - offset;
|
||||
}
|
||||
|
||||
QFile f(CachePathName());
|
||||
if (f.open(QFile::ReadWrite)) {
|
||||
|
||||
if (f.size() < out_point && !f.resize(out_point)) {
|
||||
qCritical() << "Failed to resize file" << CachePathName();
|
||||
}
|
||||
|
||||
if (!f.seek(offset)) {
|
||||
qCritical() << "Failed to seek file" << CachePathName();
|
||||
}
|
||||
|
||||
// Replace data with this data
|
||||
int copy_length = qMin(length, cached_samples.size());
|
||||
|
||||
f.write(cached_samples.data(), copy_length);
|
||||
|
||||
if (copy_length < length) {
|
||||
|
||||
// Fill in remainder with silence
|
||||
QByteArray empty_space(length - copy_length, 0);
|
||||
f.write(empty_space);
|
||||
}
|
||||
|
||||
f.close();
|
||||
} else {
|
||||
qWarning() << "Failed to write to cached PCM file";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CacheNext();
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,105 +0,0 @@
|
||||
/***
|
||||
|
||||
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 "audioworker.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
AudioWorker::AudioWorker(QHash<Node *, Node *> *copy_map, QObject *parent) :
|
||||
AudioRenderWorker(copy_map, parent)
|
||||
{
|
||||
}
|
||||
|
||||
NodeValue AudioWorker::FrameToValue(DecoderPtr decoder, StreamPtr stream, const TimeRange &range)
|
||||
{
|
||||
if (decoder->HasConformedVersion(audio_params())) {
|
||||
SampleBufferPtr frame = decoder->RetrieveAudio(range.in(), range.out() - range.in(), audio_params());
|
||||
|
||||
if (frame) {
|
||||
return NodeValue(NodeParam::kSamples, QVariant::fromValue(frame));
|
||||
}
|
||||
} else {
|
||||
emit ConformUnavailable(decoder->stream(), CurrentPath().range(), range.out(), audio_params());
|
||||
}
|
||||
|
||||
return NodeValue();
|
||||
}
|
||||
|
||||
void AudioWorker::RunNodeAccelerated(const Node *node, const TimeRange &range, NodeValueDatabase &input_params_in, NodeValueTable &output_params)
|
||||
{
|
||||
// Check if node processes samples
|
||||
if (!(node->GetCapabilities(input_params_in) & Node::kSampleProcessor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy database so we can make some temporary modifications to it
|
||||
NodeValueDatabase input_params = input_params_in;
|
||||
NodeInput* sample_input = node->ProcessesSamplesFrom(input_params);
|
||||
|
||||
// Try to find the sample buffer in the table
|
||||
QVariant samples_var = input_params[sample_input].Get(NodeParam::kSamples);
|
||||
|
||||
// If there isn't one, there's nothing to do
|
||||
if (samples_var.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SampleBufferPtr input_buffer = samples_var.value<SampleBufferPtr>();
|
||||
|
||||
if (!input_buffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
SampleBufferPtr output_buffer = SampleBuffer::CreateAllocated(input_buffer->audio_params(), input_buffer->sample_count_per_channel());
|
||||
|
||||
int sample_count = input_buffer->sample_count_per_channel();
|
||||
|
||||
// FIXME: Hardcoded float sample format
|
||||
for (int i=0;i<sample_count;i++) {
|
||||
// Calculate the exact rational time at this sample
|
||||
int sample_out_of_channel = i / audio_params().channel_count();
|
||||
double sample_to_second = static_cast<double>(sample_out_of_channel) / static_cast<double>(audio_params().sample_rate());
|
||||
|
||||
rational this_sample_time = rational::fromDouble(range.in().toDouble() + sample_to_second);
|
||||
|
||||
// Update all non-sample and non-footage inputs
|
||||
foreach (NodeParam* param, node->parameters()) {
|
||||
if (param->type() == NodeParam::kInput
|
||||
&& param != sample_input) {
|
||||
NodeInput* input = static_cast<NodeInput*>(param);
|
||||
|
||||
// If the input isn't keyframing, we don't need to update it unless it's connected, in which case it may change
|
||||
if (input->IsConnected() || input->is_keyframing()) {
|
||||
input_params.Insert(input, ProcessInput(input, TimeRange(this_sample_time, this_sample_time)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node->ProcessSamples(input_params,
|
||||
audio_params(),
|
||||
input_buffer,
|
||||
output_buffer,
|
||||
i);
|
||||
}
|
||||
|
||||
output_params.Push(NodeParam::kSamples, QVariant::fromValue(output_buffer));
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,275 +0,0 @@
|
||||
/***
|
||||
|
||||
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 "audiorenderbackend.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QtMath>
|
||||
|
||||
#include "audiorenderworker.h"
|
||||
#include "common/filefunctions.h"
|
||||
#include "task/conform/conform.h"
|
||||
#include "task/taskmanager.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
AudioRenderBackend::AudioRenderBackend(QObject *parent) :
|
||||
RenderBackend(parent),
|
||||
ic_from_conform_(false)
|
||||
{
|
||||
connect(this, &AudioRenderBackend::QueueComplete, this, &AudioRenderBackend::FilterQueueCompleteSignal);
|
||||
}
|
||||
|
||||
void AudioRenderBackend::SetParameters(const AudioRenderingParams ¶ms)
|
||||
{
|
||||
CancelQueue();
|
||||
|
||||
// Set new parameters
|
||||
params_ = params;
|
||||
|
||||
// Set params on all processors
|
||||
foreach (RenderWorker* worker, processors_) {
|
||||
static_cast<AudioRenderWorker*>(worker)->SetParameters(params_);
|
||||
}
|
||||
|
||||
// Regenerate the cache ID
|
||||
RegenerateCacheID();
|
||||
|
||||
emit ParamsChanged();
|
||||
}
|
||||
|
||||
void AudioRenderBackend::ConnectViewer(ViewerOutput *node)
|
||||
{
|
||||
connect(node, &ViewerOutput::AudioChangedBetween, this, &AudioRenderBackend::InvalidateCache);
|
||||
connect(node, &ViewerOutput::LengthChanged, this, &AudioRenderBackend::TruncateCache);
|
||||
}
|
||||
|
||||
void AudioRenderBackend::DisconnectViewer(ViewerOutput *node)
|
||||
{
|
||||
disconnect(node, &ViewerOutput::AudioChangedBetween, this, &AudioRenderBackend::InvalidateCache);
|
||||
disconnect(node, &ViewerOutput::LengthChanged, this, &AudioRenderBackend::TruncateCache);
|
||||
|
||||
conform_wait_info_.clear();
|
||||
}
|
||||
|
||||
bool AudioRenderBackend::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_.sample_rate()).toUtf8());
|
||||
hash.addData(QString::number(params_.channel_layout()).toUtf8());
|
||||
hash.addData(QString::number(params_.format()).toUtf8());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const AudioRenderingParams &AudioRenderBackend::params() const
|
||||
{
|
||||
return params_;
|
||||
}
|
||||
|
||||
NodeInput *AudioRenderBackend::GetDependentInput()
|
||||
{
|
||||
return viewer_node()->samples_input();
|
||||
}
|
||||
|
||||
QString AudioRenderBackend::CachePathName() const
|
||||
{
|
||||
QString cache_fn = cache_id();
|
||||
cache_fn.append(".pcm");
|
||||
return QDir(FileFunctions::GetMediaCacheLocation()).filePath(cache_fn);
|
||||
}
|
||||
|
||||
bool AudioRenderBackend::CanRender()
|
||||
{
|
||||
return params_.is_valid();
|
||||
}
|
||||
|
||||
void AudioRenderBackend::ConnectWorkerToThis(RenderWorker *worker)
|
||||
{
|
||||
AudioRenderWorker* arw = static_cast<AudioRenderWorker*>(worker);
|
||||
|
||||
connect(arw, &AudioRenderWorker::ConformUnavailable, this, &AudioRenderBackend::ConformUnavailable, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
TimeRange AudioRenderBackend::PopNextFrameFromQueue()
|
||||
{
|
||||
TimeRange range = cache_queue_.first();
|
||||
|
||||
// Limit range per worker to 2 seconds (FIXME: arbitrary, should be tweaked, maybe even in config?)
|
||||
range.set_out(qMin(range.out(), range.in() + rational(2)));
|
||||
|
||||
cache_queue_.RemoveTimeRange(range);
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
void AudioRenderBackend::InvalidateCacheInternal(const rational &start_range, const rational &end_range)
|
||||
{
|
||||
if (!ic_from_conform_) {
|
||||
// Cancel any ranges waiting on a conform here since obviously the contents have changed
|
||||
TimeRange range(start_range, end_range);
|
||||
|
||||
for (int i=0;i<conform_wait_info_.size();i++) {
|
||||
ConformWaitInfo& info = conform_wait_info_[i];
|
||||
|
||||
// FIXME: Code copied from TimeRangeList::RemoveTimeRange()
|
||||
|
||||
if (range.Contains(info.affected_range)) {
|
||||
conform_wait_info_.removeAt(i);
|
||||
i--;
|
||||
} else if (info.affected_range.Contains(range, false, false)) {
|
||||
ConformWaitInfo copy = info;
|
||||
|
||||
info.affected_range.set_out(start_range);
|
||||
copy.affected_range.set_in(end_range);
|
||||
|
||||
conform_wait_info_.append(copy);
|
||||
} else if (info.affected_range.in() < start_range && info.affected_range.out() > start_range) {
|
||||
info.affected_range.set_out(start_range);
|
||||
} else if (info.affected_range.in() < end_range && info.affected_range.out() > end_range) {
|
||||
info.affected_range.set_in(end_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderBackend::InvalidateCacheInternal(start_range, end_range);
|
||||
}
|
||||
|
||||
void AudioRenderBackend::ListenForConformSignal(AudioStreamPtr s)
|
||||
{
|
||||
foreach (const ConformWaitInfo& info, conform_wait_info_) {
|
||||
if (info.stream == s) {
|
||||
// We've probably already connected to this one
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
connect(s.get(), &AudioStream::ConformAppended, this, &AudioRenderBackend::ConformUpdated);
|
||||
}
|
||||
|
||||
void AudioRenderBackend::StopListeningForConformSignal(AudioStream* s)
|
||||
{
|
||||
foreach (const ConformWaitInfo& info, conform_wait_info_) {
|
||||
if (info.stream.get() == s) {
|
||||
// There are still conforms we're waiting for, don't disconnect
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(s, &AudioStream::ConformAppended, this, &AudioRenderBackend::ConformUpdated);
|
||||
}
|
||||
|
||||
void AudioRenderBackend::ConformUnavailable(StreamPtr stream, TimeRange range, rational stream_time, AudioRenderingParams params)
|
||||
{
|
||||
ConformWaitInfo info = {stream, params, range, stream_time};
|
||||
|
||||
if (conform_wait_info_.contains(info)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AudioStreamPtr audio_stream = std::static_pointer_cast<AudioStream>(stream);
|
||||
|
||||
if (audio_stream->try_start_conforming(params)) {
|
||||
|
||||
// Start indexing process
|
||||
ListenForConformSignal(audio_stream);
|
||||
|
||||
conform_wait_info_.append(info);
|
||||
|
||||
ConformTask* conform_task = new ConformTask(audio_stream, params);
|
||||
|
||||
TaskManager::instance()->AddTask(conform_task);
|
||||
|
||||
} else if (audio_stream->has_conformed_version(params)) {
|
||||
|
||||
// Conform JUST finished, requeue this time
|
||||
ic_from_conform_ = true;
|
||||
InvalidateCache(range, nullptr);
|
||||
ic_from_conform_ = false;
|
||||
|
||||
} else {
|
||||
|
||||
// A conform task is already running, so we'll just wait for it
|
||||
ListenForConformSignal(audio_stream);
|
||||
|
||||
conform_wait_info_.append(info);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void AudioRenderBackend::ConformUpdated(AudioRenderingParams params)
|
||||
{
|
||||
AudioStream *stream = static_cast<AudioStream*>(sender());
|
||||
|
||||
for (int i=0;i<conform_wait_info_.size();i++) {
|
||||
const ConformWaitInfo& info = conform_wait_info_.at(i);
|
||||
|
||||
if (info.stream.get() == stream
|
||||
&& info.params == params) {
|
||||
|
||||
// Make a copy so the values we use aren't corrupt
|
||||
ConformWaitInfo copy = info;
|
||||
|
||||
// Remove this entry from the list
|
||||
conform_wait_info_.removeAt(i);
|
||||
i--;
|
||||
|
||||
// Send invalidate cache signal
|
||||
ic_from_conform_ = true;
|
||||
InvalidateCache(copy.affected_range, nullptr);
|
||||
ic_from_conform_ = false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
StopListeningForConformSignal(stream);
|
||||
}
|
||||
|
||||
void AudioRenderBackend::TruncateCache(const rational &r)
|
||||
{
|
||||
int seq_length = params_.time_to_bytes(r);
|
||||
|
||||
QFile cache_pcm(CachePathName());
|
||||
|
||||
if (cache_pcm.size() > seq_length) {
|
||||
cache_pcm.resize(seq_length);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioRenderBackend::FilterQueueCompleteSignal()
|
||||
{
|
||||
if (conform_wait_info_.isEmpty()) {
|
||||
emit AudioComplete();
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioRenderBackend::ConformWaitInfo::operator==(const AudioRenderBackend::ConformWaitInfo &rhs) const
|
||||
{
|
||||
return rhs.params == params
|
||||
&& rhs.stream == stream
|
||||
&& rhs.stream_time == stream_time
|
||||
&& rhs.affected_range == affected_range;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,105 +0,0 @@
|
||||
/***
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef AUDIORENDERBACKEND_H
|
||||
#define AUDIORENDERBACKEND_H
|
||||
|
||||
#include "common/timerange.h"
|
||||
#include "renderbackend.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class AudioRenderBackend : public RenderBackend
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioRenderBackend(QObject* parent = nullptr);
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
void SetParameters(const AudioRenderingParams ¶ms);
|
||||
|
||||
const AudioRenderingParams& params() const;
|
||||
|
||||
QString CachePathName() const;
|
||||
|
||||
signals:
|
||||
void ParamsChanged();
|
||||
|
||||
void AudioComplete();
|
||||
|
||||
protected:
|
||||
virtual void ConnectViewer(ViewerOutput* node) override;
|
||||
|
||||
virtual void DisconnectViewer(ViewerOutput* node) override;
|
||||
|
||||
/**
|
||||
* @brief Internal function for generating the cache ID
|
||||
*/
|
||||
virtual bool GenerateCacheIDInternal(QCryptographicHash& hash) override;
|
||||
|
||||
virtual NodeInput* GetDependentInput() override;
|
||||
|
||||
virtual bool CanRender() override;
|
||||
|
||||
virtual void ConnectWorkerToThis(RenderWorker* worker) override;
|
||||
|
||||
virtual TimeRange PopNextFrameFromQueue() override;
|
||||
|
||||
virtual void InvalidateCacheInternal(const rational &start_range, const rational &end_range) override;
|
||||
|
||||
private:
|
||||
struct ConformWaitInfo {
|
||||
StreamPtr stream;
|
||||
AudioRenderingParams params;
|
||||
TimeRange affected_range;
|
||||
rational stream_time;
|
||||
|
||||
bool operator==(const ConformWaitInfo& rhs) const;
|
||||
};
|
||||
|
||||
void ListenForConformSignal(AudioStreamPtr s);
|
||||
|
||||
void StopListeningForConformSignal(AudioStream *s);
|
||||
|
||||
QList<ConformWaitInfo> conform_wait_info_;
|
||||
|
||||
AudioRenderingParams params_;
|
||||
|
||||
bool ic_from_conform_;
|
||||
|
||||
private slots:
|
||||
void ConformUnavailable(StreamPtr stream, TimeRange range, rational stream_time, AudioRenderingParams params);
|
||||
|
||||
void ConformUpdated(OLIVE_NAMESPACE::AudioRenderingParams params);
|
||||
|
||||
void TruncateCache(const rational& r);
|
||||
|
||||
void FilterQueueCompleteSignal();
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // AUDIORENDERBACKEND_H
|
||||
@@ -1,173 +0,0 @@
|
||||
/***
|
||||
|
||||
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 "audiorenderworker.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFloat16>
|
||||
|
||||
#include "audio/audiomanager.h"
|
||||
#include "audio/sumsamples.h"
|
||||
#include "config/config.h"
|
||||
#include "node/block/clip/clip.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
AudioRenderWorker::AudioRenderWorker(QHash<Node *, Node *> *copy_map, QObject *parent) :
|
||||
RenderWorker(parent),
|
||||
copy_map_(copy_map)
|
||||
{
|
||||
}
|
||||
|
||||
void AudioRenderWorker::SetParameters(const AudioRenderingParams &audio_params)
|
||||
{
|
||||
audio_params_ = audio_params;
|
||||
}
|
||||
|
||||
bool AudioRenderWorker::InitInternal()
|
||||
{
|
||||
// Nothing to init yet
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioRenderWorker::CloseInternal()
|
||||
{
|
||||
// Nothing to init yet
|
||||
}
|
||||
|
||||
NodeValueTable AudioRenderWorker::RenderBlock(const TrackOutput *track, const TimeRange &range)
|
||||
{
|
||||
QList<Block*> active_blocks = track->BlocksAtTimeRange(range);
|
||||
|
||||
// All these blocks will need to output to a buffer so we create one here
|
||||
SampleBufferPtr block_range_buffer = SampleBuffer::CreateAllocated(audio_params_, audio_params_.time_to_samples(range.length()));
|
||||
block_range_buffer->fill(0);
|
||||
|
||||
NodeValueTable merged_table;
|
||||
|
||||
// Loop through active blocks retrieving their audio
|
||||
foreach (Block* b, active_blocks) {
|
||||
TimeRange range_for_block(qMax(b->in(), range.in()),
|
||||
qMin(b->out(), range.out()));
|
||||
|
||||
int destination_offset = audio_params_.time_to_samples(range_for_block.in() - range.in());
|
||||
int max_dest_sz = audio_params_.time_to_samples(range_for_block.length());
|
||||
|
||||
// Destination buffer
|
||||
NodeValueTable table = ProcessNode(NodeDependency(b, range_for_block));
|
||||
QVariant sample_val = table.Take(NodeParam::kSamples);
|
||||
SampleBufferPtr samples_from_this_block;
|
||||
|
||||
if (sample_val.isNull()
|
||||
|| !(samples_from_this_block = sample_val.value<SampleBufferPtr>())) {
|
||||
// If we retrieved no samples from this block, do nothing
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stretch samples here
|
||||
rational abs_speed = qAbs(b->speed());
|
||||
|
||||
if (abs_speed != 1) {
|
||||
samples_from_this_block->speed(abs_speed.toDouble());
|
||||
}
|
||||
|
||||
if (b->is_reversed()) {
|
||||
// Reverse the audio buffer
|
||||
samples_from_this_block->reverse();
|
||||
}
|
||||
|
||||
int copy_length = qMin(max_dest_sz, samples_from_this_block->sample_count_per_channel());
|
||||
|
||||
// Copy samples into destination buffer
|
||||
block_range_buffer->set(samples_from_this_block->const_data(), destination_offset, copy_length);
|
||||
|
||||
{
|
||||
// Save waveform to file
|
||||
Block* src_block = static_cast<Block*>(copy_map_->key(b));
|
||||
QDir local_appdata_dir(Config::Current()["DiskCachePath"].toString());
|
||||
QDir waveform_loc = local_appdata_dir.filePath(QStringLiteral("waveform"));
|
||||
waveform_loc.mkpath(".");
|
||||
QString wave_fn(waveform_loc.filePath(QString::number(reinterpret_cast<quintptr>(src_block))));
|
||||
QFile wave_file(wave_fn);
|
||||
|
||||
if (wave_file.open(QFile::ReadWrite)) {
|
||||
// We use S32 as a size-compatible substitute for SampleSummer::Sum which is 4 bytes in size
|
||||
AudioRenderingParams waveform_params(SampleSummer::kSumSampleRate, audio_params_.channel_layout(), SampleFormat::SAMPLE_FMT_S32);
|
||||
int chunk_size = (audio_params().sample_rate() / waveform_params.sample_rate());
|
||||
|
||||
{
|
||||
// Write metadata header
|
||||
SampleSummer::Info info;
|
||||
info.channels = audio_params_.channel_count();
|
||||
wave_file.write(reinterpret_cast<char*>(&info), sizeof(SampleSummer::Info));
|
||||
}
|
||||
|
||||
qint64 start_offset = sizeof(SampleSummer::Info) + waveform_params.time_to_bytes(range_for_block.in() - b->in());
|
||||
qint64 length_offset = waveform_params.time_to_bytes(range_for_block.length());
|
||||
qint64 end_offset = start_offset + length_offset;
|
||||
|
||||
if (wave_file.size() < end_offset) {
|
||||
wave_file.resize(end_offset);
|
||||
}
|
||||
|
||||
wave_file.seek(start_offset);
|
||||
|
||||
for (int i=0;i<samples_from_this_block->sample_count_per_channel();i+=chunk_size) {
|
||||
QVector<SampleSummer::Sum> summary = SampleSummer::SumSamples(samples_from_this_block,
|
||||
i,
|
||||
qMin(chunk_size, samples_from_this_block->sample_count_per_channel() - i));
|
||||
|
||||
wave_file.write(reinterpret_cast<const char*>(summary.constData()),
|
||||
summary.size() * sizeof(SampleSummer::Sum));
|
||||
}
|
||||
|
||||
wave_file.close();
|
||||
|
||||
if (src_block->type() == Block::kClip) {
|
||||
emit static_cast<ClipBlock*>(src_block)->PreviewUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NodeValueTable::Merge({merged_table, table});
|
||||
}
|
||||
|
||||
merged_table.Push(NodeParam::kSamples, QVariant::fromValue(block_range_buffer));
|
||||
|
||||
return merged_table;
|
||||
}
|
||||
|
||||
void AudioRenderWorker::FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable *table)
|
||||
{
|
||||
if (stream->type() != Stream::kAudio) {
|
||||
return;
|
||||
}
|
||||
|
||||
NodeValue value = GetDataFromStream(stream, input_time);
|
||||
|
||||
table->Push(value);
|
||||
}
|
||||
|
||||
const AudioRenderingParams &AudioRenderWorker::audio_params() const
|
||||
{
|
||||
return audio_params_;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,59 +0,0 @@
|
||||
/***
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef AUDIORENDERWORKER_H
|
||||
#define AUDIORENDERWORKER_H
|
||||
|
||||
#include "renderworker.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class AudioRenderWorker : public RenderWorker
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioRenderWorker(QHash<Node*, Node*>* copy_map, QObject* parent = nullptr);
|
||||
|
||||
void SetParameters(const AudioRenderingParams& audio_params);
|
||||
|
||||
signals:
|
||||
void ConformUnavailable(StreamPtr stream, TimeRange range, rational stream_time, AudioRenderingParams params);
|
||||
|
||||
protected:
|
||||
virtual bool InitInternal() override;
|
||||
|
||||
virtual void CloseInternal() override;
|
||||
|
||||
virtual NodeValueTable RenderBlock(const TrackOutput *track, const TimeRange& range) override;
|
||||
|
||||
virtual void FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable* table) override;
|
||||
|
||||
const AudioRenderingParams& audio_params() const;
|
||||
|
||||
private:
|
||||
AudioRenderingParams audio_params_;
|
||||
|
||||
QHash<Node*, Node*>* copy_map_;
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // AUDIORENDERWORKER_H
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#include "render/backend/audio/audiobackend.h"
|
||||
#include "render/backend/opengl/openglbackend.h"
|
||||
#include "render/colormanager.h"
|
||||
#include "render/pixelformat.h"
|
||||
@@ -36,8 +35,7 @@ Exporter::Exporter(ViewerOutput *viewer_node,
|
||||
QObject(parent),
|
||||
viewer_node_(viewer_node),
|
||||
params_(params),
|
||||
video_backend_(nullptr),
|
||||
audio_backend_(nullptr),
|
||||
renderer_(nullptr),
|
||||
export_status_(false),
|
||||
export_msg_(tr("Export hasn't started yet"))
|
||||
{
|
||||
@@ -54,7 +52,7 @@ Exporter::Exporter(ViewerOutput *viewer_node,
|
||||
if (params_.has_custom_range()) {
|
||||
export_range_ = params_.custom_range();
|
||||
} else {
|
||||
export_range_ = TimeRange(0, viewer_node_->Length());
|
||||
export_range_ = TimeRange(0, viewer_node_->GetLength());
|
||||
}
|
||||
|
||||
if (params_.video_enabled()) {
|
||||
@@ -87,18 +85,10 @@ const QString &Exporter::GetExportError() const
|
||||
|
||||
void Exporter::Cancel()
|
||||
{
|
||||
if (video_backend_) {
|
||||
video_backend_->CancelQueue();
|
||||
video_backend_->Close();
|
||||
video_backend_->deleteLater();
|
||||
video_backend_ = nullptr;
|
||||
}
|
||||
|
||||
if (audio_backend_) {
|
||||
audio_backend_->CancelQueue();
|
||||
audio_backend_->Close();
|
||||
audio_backend_->deleteLater();
|
||||
audio_backend_ = nullptr;
|
||||
if (renderer_) {
|
||||
renderer_->CancelQueue();
|
||||
renderer_->deleteLater();
|
||||
renderer_ = nullptr;
|
||||
}
|
||||
|
||||
SetExportMessage(tr("User cancelled export"));
|
||||
@@ -111,25 +101,18 @@ void Exporter::StartExporting()
|
||||
export_status_ = false;
|
||||
|
||||
// Create renderers
|
||||
if (!video_done_) {
|
||||
video_backend_ = new OpenGLBackend();
|
||||
renderer_ = new OpenGLBackend();
|
||||
renderer_->SetViewerNode(viewer_node_);
|
||||
|
||||
video_backend_->SetLimitCaching(false);
|
||||
video_backend_->SetViewerNode(viewer_node_);
|
||||
video_backend_->SetParameters(VideoRenderingParams(viewer_node_->video_params().width(),
|
||||
viewer_node_->video_params().height(),
|
||||
params_.video_params().time_base(),
|
||||
params_.video_params().format(),
|
||||
params_.video_params().mode()));
|
||||
if (!video_done_) {
|
||||
renderer_->SetPixelFormat(params_.video_params().format());
|
||||
renderer_->SetMode(params_.video_params().mode());
|
||||
|
||||
waiting_for_frame_ = 0;
|
||||
}
|
||||
|
||||
if (!audio_done_) {
|
||||
audio_backend_ = new AudioBackend();
|
||||
|
||||
audio_backend_->SetViewerNode(viewer_node_);
|
||||
audio_backend_->SetParameters(params_.audio_params());
|
||||
renderer_->SetSampleFormat(params_.audio_params().format());
|
||||
}
|
||||
|
||||
// Open encoder and wait for result
|
||||
@@ -153,10 +136,9 @@ void Exporter::ExportSucceeded()
|
||||
return;
|
||||
}
|
||||
|
||||
if (video_backend_) {
|
||||
video_backend_->Close();
|
||||
video_backend_->deleteLater();
|
||||
video_backend_ = nullptr;
|
||||
if (renderer_) {
|
||||
renderer_->deleteLater();
|
||||
renderer_ = nullptr;
|
||||
}
|
||||
|
||||
export_status_ = true;
|
||||
@@ -189,10 +171,10 @@ void Exporter::EncodeFrame()
|
||||
waiting_for_frame_ += params_.video_params().time_base();
|
||||
|
||||
// Calculate progress
|
||||
emit ProgressChanged(waiting_for_frame_.toDouble() / viewer_node_->Length().toDouble());
|
||||
emit ProgressChanged(waiting_for_frame_.toDouble() / viewer_node_->GetLength().toDouble());
|
||||
}
|
||||
|
||||
if (waiting_for_frame_ >= viewer_node_->Length()) {
|
||||
if (waiting_for_frame_ >= viewer_node_->GetLength()) {
|
||||
video_done_ = true;
|
||||
debug_timer_.stop();
|
||||
|
||||
@@ -261,6 +243,7 @@ void Exporter::FrameRendered(FramePtr frame)
|
||||
|
||||
void Exporter::AudioRendered()
|
||||
{
|
||||
/*
|
||||
// Retrieve the audio filename
|
||||
QString cache_fn = audio_backend_->CachePathName();
|
||||
|
||||
@@ -270,11 +253,7 @@ void Exporter::AudioRendered()
|
||||
OLIVE_NS_ARG(AudioRenderingParams, audio_backend_->params()),
|
||||
Q_ARG(const QString&, cache_fn),
|
||||
OLIVE_NS_ARG(TimeRange, export_range_));
|
||||
|
||||
// We don't need the audio backend anymorea
|
||||
audio_backend_->Close();
|
||||
audio_backend_->deleteLater();
|
||||
audio_backend_ = nullptr;
|
||||
*/
|
||||
}
|
||||
|
||||
void Exporter::AudioEncodeComplete()
|
||||
@@ -286,6 +265,7 @@ void Exporter::AudioEncodeComplete()
|
||||
|
||||
void Exporter::EncoderOpenedSuccessfully()
|
||||
{
|
||||
/*
|
||||
// Invalidate caches
|
||||
if (!video_done_) {
|
||||
// First we generate the hashes so we know exactly how many frames we need
|
||||
@@ -301,6 +281,7 @@ void Exporter::EncoderOpenedSuccessfully()
|
||||
|
||||
audio_backend_->InvalidateCache(export_range_, nullptr);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
void Exporter::EncoderOpenFailed()
|
||||
@@ -317,12 +298,13 @@ void Exporter::EncoderClosed()
|
||||
|
||||
void Exporter::VideoHashesComplete()
|
||||
{
|
||||
/*
|
||||
// We've got our hashes, time to kick off actual rendering
|
||||
disconnect(video_backend_, &VideoRenderBackend::QueueComplete, this, &Exporter::VideoHashesComplete);
|
||||
|
||||
// Determine what frames will be hashed
|
||||
TimeRangeList ranges;
|
||||
ranges.append(TimeRange(0, viewer_node_->Length()));
|
||||
ranges.append(TimeRange(0, viewer_node_->GetLength()));
|
||||
|
||||
// Set video backend to render mode but NOT hash or download
|
||||
video_backend_->SetOperatingMode(VideoRenderWorker::kRenderOnly);
|
||||
@@ -347,6 +329,7 @@ void Exporter::VideoHashesComplete()
|
||||
foreach (const TimeRange& range, ranges) {
|
||||
video_backend_->InvalidateCache(range, nullptr);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
void Exporter::DebugTimerMessage()
|
||||
@@ -356,7 +339,7 @@ void Exporter::DebugTimerMessage()
|
||||
|
||||
void Exporter::FrameColorFinished()
|
||||
{
|
||||
if (!video_backend_ && !audio_backend_) {
|
||||
if (!renderer_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -366,7 +349,7 @@ void Exporter::FrameColorFinished()
|
||||
|
||||
debug_timer_.stop();
|
||||
|
||||
const QMap<rational, QByteArray>& time_hash_map = video_backend_->frame_cache()->time_hash_map();
|
||||
const QMap<rational, QByteArray>& time_hash_map = viewer_node_->video_frame_cache()->time_hash_map();
|
||||
|
||||
QByteArray this_hash = time_hash_map.value(frame->timestamp());
|
||||
|
||||
|
||||
@@ -28,9 +28,8 @@
|
||||
|
||||
#include "codec/encoder.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "render/backend/audiorenderbackend.h"
|
||||
#include "render/backend/exportparams.h"
|
||||
#include "render/backend/videorenderbackend.h"
|
||||
#include "render/backend/renderbackend.h"
|
||||
#include "render/colorprocessor.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
@@ -76,8 +75,7 @@ private:
|
||||
ExportParams params_;
|
||||
|
||||
// Renderers
|
||||
VideoRenderBackend* video_backend_;
|
||||
AudioRenderBackend* audio_backend_;
|
||||
RenderBackend* renderer_;
|
||||
|
||||
// Export transform
|
||||
QMatrix4x4 transform_;
|
||||
|
||||
@@ -33,7 +33,5 @@ set(OLIVE_SOURCES
|
||||
render/backend/opengl/opengltexture.cpp
|
||||
render/backend/opengl/opengltexturecache.h
|
||||
render/backend/opengl/opengltexturecache.cpp
|
||||
render/backend/opengl/openglworker.h
|
||||
render/backend/opengl/openglworker.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -20,77 +20,12 @@
|
||||
|
||||
#include "openglbackend.h"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QThread>
|
||||
|
||||
#include "openglrenderfunctions.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
OpenGLBackend::OpenGLBackend(QObject *parent) :
|
||||
VideoRenderBackend(parent),
|
||||
proxy_(nullptr)
|
||||
OpenGLBackend::OpenGLBackend(QObject* parent) :
|
||||
RenderBackend(parent)
|
||||
{
|
||||
}
|
||||
|
||||
OpenGLBackend::~OpenGLBackend()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
bool OpenGLBackend::InitInternal()
|
||||
{
|
||||
if (!VideoRenderBackend::InitInternal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
proxy_ = new OpenGLProxy();
|
||||
proxy_->SetParameters(params());
|
||||
QThread* proxy_thread = new QThread();
|
||||
proxy_thread->start(QThread::IdlePriority);
|
||||
proxy_->moveToThread(proxy_thread);
|
||||
|
||||
if (!proxy_->Init()) {
|
||||
proxy_thread->quit();
|
||||
proxy_thread->wait();
|
||||
proxy_thread->deleteLater();
|
||||
|
||||
proxy_->deleteLater();
|
||||
proxy_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initiate one thread per CPU core
|
||||
for (int i=0;i<threads().size();i++) {
|
||||
// Create one processor object for each thread
|
||||
OpenGLWorker* processor = new OpenGLWorker(frame_cache(), proxy_);
|
||||
processor->SetParameters(params());
|
||||
processors_.append(processor);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLBackend::CloseInternal()
|
||||
{
|
||||
if (proxy_) {
|
||||
proxy_->thread()->quit();
|
||||
proxy_->thread()->wait();
|
||||
proxy_->thread()->deleteLater();
|
||||
|
||||
proxy_->deleteLater();
|
||||
proxy_ = nullptr;
|
||||
}
|
||||
|
||||
VideoRenderBackend::CloseInternal();
|
||||
}
|
||||
|
||||
void OpenGLBackend::ParamsChangedEvent()
|
||||
{
|
||||
// If we're initiated, we need to recreate the texture. Otherwise this backend isn't active so it doesn't matter.
|
||||
if (IsInitiated()) {
|
||||
proxy_->SetParameters(params());
|
||||
}
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
@@ -21,32 +21,21 @@
|
||||
#ifndef OPENGLBACKEND_H
|
||||
#define OPENGLBACKEND_H
|
||||
|
||||
#include "../videorenderbackend.h"
|
||||
#include "openglframebuffer.h"
|
||||
#include "openglproxy.h"
|
||||
#include "openglshader.h"
|
||||
#include "opengltexture.h"
|
||||
#include "openglworker.h"
|
||||
#include "render/backend/renderbackend.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class OpenGLBackend : public VideoRenderBackend
|
||||
class OpenGLBackend : public RenderBackend
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
OpenGLBackend(QObject* parent = nullptr);
|
||||
|
||||
virtual ~OpenGLBackend() override;
|
||||
|
||||
protected:
|
||||
virtual bool InitInternal() override;
|
||||
virtual void TextureToFrame(const QVariant& texture, FramePtr frame) const override;
|
||||
|
||||
virtual void CloseInternal() override;
|
||||
virtual QVariant FrameToTexture(FramePtr frame) const override;
|
||||
|
||||
virtual void ParamsChangedEvent() override;
|
||||
|
||||
private:
|
||||
OpenGLProxy* proxy_;
|
||||
virtual void ProcessNodeEvent(const Node *node, const TimeRange &range, NodeValueDatabase &input_params, NodeValueTable &output_params) const override;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#define OPENGLCOLORPROCESSOR_H
|
||||
|
||||
#include "openglshader.h"
|
||||
#include "render/backend/rendercache.h"
|
||||
#include "render/colorprocessor.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
@@ -62,6 +63,8 @@ private slots:
|
||||
|
||||
};
|
||||
|
||||
using OpenGLColorProcessorCache = RenderCache<QString, OpenGLColorProcessorPtr>;
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // OPENGLCOLORPROCESSOR_H
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
#include <QOffscreenSurface>
|
||||
#include <QOpenGLContext>
|
||||
|
||||
#include "../videorenderworker.h"
|
||||
#include "common/timerange.h"
|
||||
#include "node/value.h"
|
||||
#include "openglcolorprocessor.h"
|
||||
#include "openglframebuffer.h"
|
||||
#include "openglshadercache.h"
|
||||
#include "opengltexturecache.h"
|
||||
@@ -81,7 +83,7 @@ private:
|
||||
|
||||
OpenGLFramebuffer buffer_;
|
||||
|
||||
ColorProcessorCache color_cache_;
|
||||
OpenGLColorProcessorCache color_cache_;
|
||||
|
||||
VideoRenderingParams video_params_;
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/***
|
||||
|
||||
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 "openglworker.h"
|
||||
|
||||
#include "common/clamp.h"
|
||||
#include "core.h"
|
||||
#include "node/block/transition/transition.h"
|
||||
#include "node/node.h"
|
||||
#include "openglcolorprocessor.h"
|
||||
#include "openglrenderfunctions.h"
|
||||
#include "render/colormanager.h"
|
||||
#include "render/pixelformat.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
OpenGLWorker::OpenGLWorker(VideoRenderFrameCache *frame_cache, OpenGLProxy *proxy, QObject *parent) :
|
||||
VideoRenderWorker(frame_cache, parent),
|
||||
proxy_(proxy)
|
||||
{
|
||||
}
|
||||
|
||||
NodeValue OpenGLWorker::FrameToValue(DecoderPtr decoder, StreamPtr stream, const TimeRange &range)
|
||||
{
|
||||
FramePtr frame = decoder->RetrieveVideo(range.in(),
|
||||
video_params().divider(),
|
||||
video_params().mode() == RenderMode::kOffline);
|
||||
|
||||
NodeValue value;
|
||||
|
||||
if (frame) {
|
||||
QMetaObject::invokeMethod(proxy_,
|
||||
"FrameToValue",
|
||||
Qt::BlockingQueuedConnection,
|
||||
OLIVE_NS_RETURN_ARG(NodeValue, value),
|
||||
OLIVE_NS_ARG(FramePtr, frame),
|
||||
OLIVE_NS_ARG(StreamPtr, stream));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
void OpenGLWorker::RunNodeAccelerated(const Node *node, const TimeRange &range, NodeValueDatabase &input_params, NodeValueTable &output_params)
|
||||
{
|
||||
QMetaObject::invokeMethod(proxy_,
|
||||
"RunNodeAccelerated",
|
||||
Qt::BlockingQueuedConnection,
|
||||
OLIVE_NS_CONST_ARG(Node*, node),
|
||||
OLIVE_NS_CONST_ARG(TimeRange&, range),
|
||||
OLIVE_NS_ARG(NodeValueDatabase&, input_params),
|
||||
OLIVE_NS_ARG(NodeValueTable&, output_params));
|
||||
}
|
||||
|
||||
void OpenGLWorker::TextureToBuffer(const QVariant &tex_in, int width, int height, const QMatrix4x4& matrix, void *buffer, int linesize)
|
||||
{
|
||||
QMetaObject::invokeMethod(proxy_,
|
||||
"TextureToBuffer",
|
||||
Qt::BlockingQueuedConnection,
|
||||
Q_ARG(const QVariant&, tex_in),
|
||||
Q_ARG(int, width),
|
||||
Q_ARG(int, height),
|
||||
Q_ARG(const QMatrix4x4&, matrix),
|
||||
Q_ARG(void*, buffer),
|
||||
Q_ARG(int, linesize));
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,56 +0,0 @@
|
||||
/***
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OPENGLPROCESSOR_H
|
||||
#define OPENGLPROCESSOR_H
|
||||
|
||||
#include <QOffscreenSurface>
|
||||
#include <QOpenGLContext>
|
||||
|
||||
#include "../videorenderworker.h"
|
||||
#include "openglframebuffer.h"
|
||||
#include "openglproxy.h"
|
||||
#include "openglshadercache.h"
|
||||
#include "opengltexturecache.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class OpenGLWorker : public VideoRenderWorker {
|
||||
Q_OBJECT
|
||||
public:
|
||||
OpenGLWorker(VideoRenderFrameCache* frame_cache,
|
||||
OpenGLProxy* proxy,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
protected:
|
||||
virtual NodeValue FrameToValue(DecoderPtr decoder, StreamPtr stream, const TimeRange &range) override;
|
||||
|
||||
virtual void RunNodeAccelerated(const Node *node, const TimeRange &range, NodeValueDatabase &input_params, NodeValueTable& output_params) override;
|
||||
|
||||
virtual void TextureToBuffer(const QVariant& texture, int width, int height, const QMatrix4x4& matrix, void *buffer, int linesize) override;
|
||||
|
||||
private:
|
||||
OpenGLProxy* proxy_;
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // OPENGLPROCESSOR_H
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <QDateTime>
|
||||
#include <QThread>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "core.h"
|
||||
#include "window/mainwindow/mainwindow.h"
|
||||
|
||||
@@ -30,331 +31,409 @@ OLIVE_NAMESPACE_ENTER
|
||||
|
||||
RenderBackend::RenderBackend(QObject *parent) :
|
||||
QObject(parent),
|
||||
started_(false),
|
||||
viewer_node_(nullptr),
|
||||
copied_viewer_node_(nullptr)
|
||||
divider_(1),
|
||||
render_mode_(RenderMode::kOnline),
|
||||
pix_fmt_(PixelFormat::PIX_FMT_RGBA32F),
|
||||
sample_fmt_(SampleFormat::SAMPLE_FMT_FLT)
|
||||
{
|
||||
// FIXME: Don't create in CLI mode
|
||||
cancel_dialog_ = new RenderCancelDialog(Core::instance()->main_window());
|
||||
}
|
||||
|
||||
bool RenderBackend::Init()
|
||||
void RenderBackend::SetViewerNode(ViewerOutput *viewer_node)
|
||||
{
|
||||
if (started_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
threads_.resize(QThread::idealThreadCount());
|
||||
|
||||
for (int i=0;i<threads_.size();i++) {
|
||||
QThread* thread = new QThread(this);
|
||||
threads_.replace(i, thread);
|
||||
|
||||
// We use low priority to keep the app responsive at all times (GUI thread should always prioritize over this one)
|
||||
thread->start(QThread::IdlePriority);
|
||||
}
|
||||
|
||||
cancel_dialog_->SetWorkerCount(threads_.size());
|
||||
|
||||
started_ = InitInternal();
|
||||
|
||||
// Connects workers and moves them to their respective threads
|
||||
InitWorkers();
|
||||
|
||||
if (!started_) {
|
||||
Close();
|
||||
}
|
||||
|
||||
return started_;
|
||||
}
|
||||
|
||||
void RenderBackend::Close()
|
||||
{
|
||||
if (!started_) {
|
||||
if (viewer_node_ == viewer_node) {
|
||||
return;
|
||||
}
|
||||
|
||||
started_ = false;
|
||||
|
||||
CancelQueue();
|
||||
|
||||
SetViewerNode(nullptr);
|
||||
|
||||
CloseInternal();
|
||||
|
||||
for (int i=0;i<processors_.size();i++) {
|
||||
// Invoke close and quit signals on processor and thread
|
||||
QMetaObject::invokeMethod(processors_.at(i),
|
||||
"Close",
|
||||
Qt::QueuedConnection);
|
||||
|
||||
threads_.at(i)->quit();
|
||||
}
|
||||
|
||||
for (int i=0;i<processors_.size();i++) {
|
||||
threads_.at(i)->wait(); // FIXME: Maximum time in case a thread is stuck?
|
||||
delete threads_.at(i);
|
||||
delete processors_.at(i);
|
||||
}
|
||||
|
||||
threads_.clear();
|
||||
processors_.clear();
|
||||
}
|
||||
|
||||
const QString &RenderBackend::GetError() const
|
||||
{
|
||||
return error_;
|
||||
}
|
||||
|
||||
void RenderBackend::SetViewerNode(ViewerOutput *viewer_node)
|
||||
{
|
||||
if (viewer_node_) {
|
||||
// Clear queue and wait for any currently running actions to complete
|
||||
CancelQueue();
|
||||
|
||||
DisconnectViewer(viewer_node_);
|
||||
// Delete all of our copied nodes
|
||||
video_copy_map_.Clear();
|
||||
audio_copy_map_.Clear();
|
||||
|
||||
copied_graph_.Clear();
|
||||
copied_viewer_node_ = nullptr;
|
||||
node_copy_map_.clear();
|
||||
disconnect(viewer_node_,
|
||||
&ViewerOutput::GraphChangedFrom,
|
||||
this,
|
||||
&RenderBackend::NodeGraphChanged);
|
||||
|
||||
disconnect(viewer_node_->audio_playback_cache(),
|
||||
&AudioPlaybackCache::Invalidated,
|
||||
this,
|
||||
&RenderBackend::AudioCallback);
|
||||
}
|
||||
|
||||
// Set viewer node
|
||||
viewer_node_ = viewer_node;
|
||||
|
||||
if (viewer_node_) {
|
||||
ConnectViewer(viewer_node_);
|
||||
// Start copying viewer
|
||||
video_copy_map_.Init(viewer_node_);
|
||||
audio_copy_map_.Init(viewer_node_);
|
||||
|
||||
RegenerateCacheID();
|
||||
video_copy_map_.Queue(viewer_node_->texture_input());
|
||||
audio_copy_map_.Queue(viewer_node_->samples_input());
|
||||
|
||||
copied_viewer_node_ = static_cast<ViewerOutput*>(viewer_node_->copy());
|
||||
copied_graph_.AddNode(copied_viewer_node_);
|
||||
node_copy_map_.insert(viewer_node_, copied_viewer_node_);
|
||||
video_copy_map_.ProcessQueue();
|
||||
audio_copy_map_.ProcessQueue();
|
||||
|
||||
InvalidateCache(TimeRange(0, RATIONAL_MAX),
|
||||
static_cast<NodeInput*>(viewer_node_->GetInputWithID(GetDependentInput()->id())));
|
||||
connect(viewer_node_,
|
||||
&ViewerOutput::GraphChangedFrom,
|
||||
this,
|
||||
&RenderBackend::NodeGraphChanged);
|
||||
|
||||
connect(viewer_node_->audio_playback_cache(),
|
||||
&AudioPlaybackCache::Invalidated,
|
||||
this,
|
||||
&RenderBackend::AudioCallback);
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderBackend::IsInitiated()
|
||||
{
|
||||
return started_;
|
||||
}
|
||||
|
||||
void RenderBackend::RegenerateCacheID()
|
||||
{
|
||||
QCryptographicHash hash(QCryptographicHash::Sha1);
|
||||
|
||||
if (!viewer_node_
|
||||
|| !GenerateCacheIDInternal(hash)) {
|
||||
cache_id_.clear();
|
||||
CacheIDChangedEvent(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
hash.addData(viewer_node_->uuid().toByteArray());
|
||||
|
||||
QByteArray bytes = hash.result();
|
||||
cache_id_ = bytes.toHex();
|
||||
CacheIDChangedEvent(cache_id_);
|
||||
}
|
||||
|
||||
bool RenderBackend::InitInternal()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderBackend::CloseInternal()
|
||||
{
|
||||
}
|
||||
|
||||
bool RenderBackend::CanRender()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
TimeRange RenderBackend::PopNextFrameFromQueue()
|
||||
{
|
||||
return cache_queue_.takeFirst();
|
||||
}
|
||||
|
||||
rational RenderBackend::GetSequenceLength()
|
||||
{
|
||||
if (viewer_node_ == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return viewer_node_->Length();
|
||||
}
|
||||
|
||||
void RenderBackend::SetError(const QString &error)
|
||||
{
|
||||
error_ = error;
|
||||
}
|
||||
|
||||
void RenderBackend::ConnectViewer(ViewerOutput *node)
|
||||
{
|
||||
Q_UNUSED(node)
|
||||
}
|
||||
|
||||
void RenderBackend::DisconnectViewer(ViewerOutput *node)
|
||||
{
|
||||
Q_UNUSED(node)
|
||||
}
|
||||
|
||||
void RenderBackend::CacheNext()
|
||||
{
|
||||
if (cache_queue_.isEmpty()) {
|
||||
if (AllProcessorsAreAvailable()) {
|
||||
emit QueueComplete();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ViewerIsConnected()
|
||||
|| !CanRender()
|
||||
|| !Init()) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (!input_update_queued_.isEmpty()) {
|
||||
if (!AllProcessorsAreAvailable()) {
|
||||
// To update the inputs, we need all workers to stop
|
||||
return;
|
||||
}
|
||||
|
||||
CopyNodeInputValue(input_update_queued_.takeFirst());
|
||||
}
|
||||
|
||||
Node* node_connected_to_viewer = GetDependentInput()->get_connected_node();
|
||||
|
||||
if (!node_connected_to_viewer) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (RenderWorker* worker, processors_) {
|
||||
if (cache_queue_.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!WorkerIsBusy(worker)) {
|
||||
TimeRange cache_frame = PopNextFrameFromQueue();
|
||||
|
||||
NodeDependency dep = NodeDependency(node_connected_to_viewer,
|
||||
cache_frame);
|
||||
|
||||
// Timestamp this render job
|
||||
qint64 job_time = QDateTime::currentMSecsSinceEpoch();
|
||||
|
||||
// Ensure the job's time is unique (since that's the whole point)
|
||||
// NOTE: This value will be 0 if it doesn't exist, which will never be the result of currentMSecsSinceEpoch so we
|
||||
// can safely assume 0 means it doesn't exist.
|
||||
qint64 existing_job_time = render_job_info_.value(cache_frame);
|
||||
|
||||
if (existing_job_time == job_time) {
|
||||
job_time = existing_job_time + 1;
|
||||
}
|
||||
|
||||
render_job_info_.insert(cache_frame, job_time);
|
||||
|
||||
SetWorkerBusyState(worker, true);
|
||||
cancel_dialog_->WorkerStarted();
|
||||
|
||||
WorkerAboutToStartEvent(worker);
|
||||
|
||||
QMetaObject::invokeMethod(worker,
|
||||
"Render",
|
||||
Qt::QueuedConnection,
|
||||
OLIVE_NS_ARG(NodeDependency, dep),
|
||||
Q_ARG(qint64, job_time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ViewerOutput *RenderBackend::viewer_node() const
|
||||
{
|
||||
return copied_viewer_node_;
|
||||
}
|
||||
|
||||
void RenderBackend::CancelQueue()
|
||||
{
|
||||
cache_queue_.clear();
|
||||
// FIXME: Implement something better than this...
|
||||
video_copy_map_.thread_pool()->waitForDone();
|
||||
audio_copy_map_.thread_pool()->waitForDone();
|
||||
}
|
||||
|
||||
int busy = 0;
|
||||
for (int i=0;i<processor_busy_state_.size();i++) {
|
||||
if (processor_busy_state_.at(i))
|
||||
busy++;
|
||||
QByteArray HashInternal(Node *node,
|
||||
const VideoRenderingParams ¶ms,
|
||||
const rational &time)
|
||||
{
|
||||
QCryptographicHash hasher(QCryptographicHash::Sha1);
|
||||
|
||||
// Embed video parameters into this hash
|
||||
hasher.addData(reinterpret_cast<const char*>(¶ms.effective_width()), sizeof(int));
|
||||
hasher.addData(reinterpret_cast<const char*>(¶ms.effective_height()), sizeof(int));
|
||||
hasher.addData(reinterpret_cast<const char*>(¶ms.format()), sizeof(PixelFormat::Format));
|
||||
hasher.addData(reinterpret_cast<const char*>(¶ms.mode()), sizeof(RenderMode::Mode));
|
||||
|
||||
node->Hash(hasher, time);
|
||||
|
||||
return hasher.result();
|
||||
}
|
||||
|
||||
QFuture<QByteArray> RenderBackend::Hash(const rational &time)
|
||||
{
|
||||
if (!viewer_node_) {
|
||||
return QFuture<QByteArray>();
|
||||
}
|
||||
|
||||
if (busy) {
|
||||
qDebug() << this << "is waiting for" << busy << "busy workers";
|
||||
return QtConcurrent::run(video_copy_map_.thread_pool(),
|
||||
HashInternal,
|
||||
viewer_node_,
|
||||
video_params(),
|
||||
time);
|
||||
}
|
||||
|
||||
QFuture<FramePtr> RenderBackend::RenderFrame(const rational &time)
|
||||
{
|
||||
if (!viewer_node_) {
|
||||
return QFuture<FramePtr>();
|
||||
}
|
||||
|
||||
cancel_dialog_->RunIfWorkersAreBusy();
|
||||
return QtConcurrent::run(video_copy_map_.thread_pool(),
|
||||
this,
|
||||
&RenderBackend::RenderFrameInternal,
|
||||
time);
|
||||
}
|
||||
|
||||
void RenderBackend::InvalidateCache(const TimeRange &range, NodeInput *from)
|
||||
void RenderBackend::SetDivider(const int ÷r)
|
||||
{
|
||||
// Adjust range to min/max values
|
||||
rational start_range_adj = qMax(rational(0), range.in());
|
||||
rational end_range_adj = qMin(GetSequenceLength(), range.out());
|
||||
divider_ = divider;
|
||||
}
|
||||
|
||||
qDebug() << "Cache invalidated between"
|
||||
<< start_range_adj.toDouble()
|
||||
<< "and"
|
||||
<< end_range_adj.toDouble();
|
||||
void RenderBackend::SetMode(const RenderMode::Mode &mode)
|
||||
{
|
||||
render_mode_ = mode;
|
||||
}
|
||||
|
||||
if (from) {
|
||||
// Queue value update
|
||||
qDebug() << " from" << from->parentNode()->id() << "::" << from->id();
|
||||
QueueValueUpdate(from);
|
||||
void RenderBackend::SetPixelFormat(const PixelFormat::Format &pix_fmt)
|
||||
{
|
||||
pix_fmt_ = pix_fmt;
|
||||
}
|
||||
|
||||
void RenderBackend::SetSampleFormat(const SampleFormat::Format &sample_fmt)
|
||||
{
|
||||
sample_fmt_ = sample_fmt;
|
||||
}
|
||||
|
||||
void RenderBackend::NodeGraphChanged(NodeInput *from, NodeInput *source)
|
||||
{
|
||||
if (from == viewer_node_->texture_input()) {
|
||||
video_copy_map_.Queue(source);
|
||||
} else if (from == viewer_node_->samples_input()) {
|
||||
audio_copy_map_.Queue(source);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderBackend::FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable *table) const
|
||||
{
|
||||
if (stream->type() == Stream::kVideo || stream->type() == Stream::kImage) {
|
||||
|
||||
ImageStreamPtr video_stream = std::static_pointer_cast<ImageStream>(stream);
|
||||
rational time_match = (stream->type() == Stream::kImage) ? rational() : input_time.in();
|
||||
QString colorspace_match = video_stream->get_colorspace_match_string();
|
||||
|
||||
NodeValue value;
|
||||
bool found_cache = false;
|
||||
|
||||
if (still_image_cache_.Has(stream.get())) {
|
||||
CachedStill cs = still_image_cache_.Get(stream.get());
|
||||
|
||||
if (cs.colorspace == colorspace_match
|
||||
&& cs.alpha_is_associated == video_stream->premultiplied_alpha()
|
||||
&& cs.divider == video_params_.divider()
|
||||
&& cs.time == time_match) {
|
||||
value = cs.texture;
|
||||
found_cache = true;
|
||||
} else {
|
||||
still_image_cache_.Remove(stream.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_cache) {
|
||||
|
||||
value = GetDataFromStream(stream, input_time);
|
||||
|
||||
still_image_cache_.Add(stream.get(), {value,
|
||||
colorspace_match,
|
||||
video_stream->premultiplied_alpha(),
|
||||
video_params_.divider(),
|
||||
time_match});
|
||||
|
||||
}
|
||||
|
||||
table->Push(value);
|
||||
|
||||
} else if (stream->type() != Stream::kAudio) {
|
||||
|
||||
NodeValue value = GetDataFromStream(stream, input_time);
|
||||
|
||||
table->Push(value);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
NodeValueTable RenderBackend::GenerateBlockTable(const TrackOutput *track, const TimeRange &range) const
|
||||
{
|
||||
if (track->track_type() == Timeline::kTrackTypeAudio) {
|
||||
|
||||
QList<Block*> active_blocks = track->BlocksAtTimeRange(range);
|
||||
|
||||
// All these blocks will need to output to a buffer so we create one here
|
||||
SampleBufferPtr block_range_buffer = SampleBuffer::CreateAllocated(audio_params_,
|
||||
audio_params_.time_to_samples(range.length()));
|
||||
block_range_buffer->fill(0);
|
||||
|
||||
NodeValueTable merged_table;
|
||||
|
||||
// Loop through active blocks retrieving their audio
|
||||
foreach (Block* b, active_blocks) {
|
||||
TimeRange range_for_block(qMax(b->in(), range.in()),
|
||||
qMin(b->out(), range.out()));
|
||||
|
||||
int destination_offset = audio_params_.time_to_samples(range_for_block.in() - range.in());
|
||||
int max_dest_sz = audio_params_.time_to_samples(range_for_block.length());
|
||||
|
||||
// Destination buffer
|
||||
NodeValueTable table = ProcessNode(NodeDependency(b, range_for_block));
|
||||
QVariant sample_val = table.Take(NodeParam::kSamples);
|
||||
SampleBufferPtr samples_from_this_block;
|
||||
|
||||
if (sample_val.isNull()
|
||||
|| !(samples_from_this_block = sample_val.value<SampleBufferPtr>())) {
|
||||
// If we retrieved no samples from this block, do nothing
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stretch samples here
|
||||
rational abs_speed = qAbs(b->speed());
|
||||
|
||||
if (abs_speed != 1) {
|
||||
samples_from_this_block->speed(abs_speed.toDouble());
|
||||
}
|
||||
|
||||
if (b->is_reversed()) {
|
||||
// Reverse the audio buffer
|
||||
samples_from_this_block->reverse();
|
||||
}
|
||||
|
||||
int copy_length = qMin(max_dest_sz, samples_from_this_block->sample_count_per_channel());
|
||||
|
||||
// Copy samples into destination buffer
|
||||
block_range_buffer->set(samples_from_this_block->const_data(), destination_offset, copy_length);
|
||||
|
||||
{
|
||||
// Save waveform to file
|
||||
Block* src_block = static_cast<Block*>(copy_map_->key(b));
|
||||
QDir local_appdata_dir(Config::Current()["DiskCachePath"].toString());
|
||||
QDir waveform_loc = local_appdata_dir.filePath(QStringLiteral("waveform"));
|
||||
waveform_loc.mkpath(".");
|
||||
QString wave_fn(waveform_loc.filePath(QString::number(reinterpret_cast<quintptr>(src_block))));
|
||||
QFile wave_file(wave_fn);
|
||||
|
||||
if (wave_file.open(QFile::ReadWrite)) {
|
||||
// We use S32 as a size-compatible substitute for SampleSummer::Sum which is 4 bytes in size
|
||||
AudioRenderingParams waveform_params(SampleSummer::kSumSampleRate, audio_params_.channel_layout(), SampleFormat::SAMPLE_FMT_S32);
|
||||
int chunk_size = (audio_params().sample_rate() / waveform_params.sample_rate());
|
||||
|
||||
{
|
||||
// Write metadata header
|
||||
SampleSummer::Info info;
|
||||
info.channels = audio_params_.channel_count();
|
||||
wave_file.write(reinterpret_cast<char*>(&info), sizeof(SampleSummer::Info));
|
||||
}
|
||||
|
||||
qint64 start_offset = sizeof(SampleSummer::Info) + waveform_params.time_to_bytes(range_for_block.in() - b->in());
|
||||
qint64 length_offset = waveform_params.time_to_bytes(range_for_block.length());
|
||||
qint64 end_offset = start_offset + length_offset;
|
||||
|
||||
if (wave_file.size() < end_offset) {
|
||||
wave_file.resize(end_offset);
|
||||
}
|
||||
|
||||
wave_file.seek(start_offset);
|
||||
|
||||
for (int i=0;i<samples_from_this_block->sample_count_per_channel();i+=chunk_size) {
|
||||
QVector<SampleSummer::Sum> summary = SampleSummer::SumSamples(samples_from_this_block,
|
||||
i,
|
||||
qMin(chunk_size, samples_from_this_block->sample_count_per_channel() - i));
|
||||
|
||||
wave_file.write(reinterpret_cast<const char*>(summary.constData()),
|
||||
summary.size() * sizeof(SampleSummer::Sum));
|
||||
}
|
||||
|
||||
wave_file.close();
|
||||
|
||||
if (src_block->type() == Block::kClip) {
|
||||
emit static_cast<ClipBlock*>(src_block)->PreviewUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NodeValueTable::Merge({merged_table, table});
|
||||
}
|
||||
|
||||
merged_table.Push(NodeParam::kSamples, QVariant::fromValue(block_range_buffer));
|
||||
|
||||
return merged_table;
|
||||
|
||||
} else {
|
||||
return NodeTraverser::GenerateBlockTable(track, range);
|
||||
}
|
||||
}
|
||||
|
||||
FramePtr RenderBackend::RenderFrameInternal(const rational &time) const
|
||||
{
|
||||
NodeValueTable table = GenerateTable(viewer_node_,
|
||||
TimeRange(time, time + viewer_node_->video_params().time_base()));
|
||||
|
||||
QVariant texture = table.Get(NodeParam::kTexture);
|
||||
|
||||
FramePtr frame = Frame::Create();
|
||||
frame->set_video_params(video_params());
|
||||
frame->allocate();
|
||||
|
||||
if (texture.isNull()) {
|
||||
memset(frame->data(), 0, frame->allocated_size());
|
||||
} else {
|
||||
TextureToFrame(texture, frame);
|
||||
}
|
||||
|
||||
InvalidateCacheInternal(start_range_adj, end_range_adj);
|
||||
return frame;
|
||||
}
|
||||
|
||||
bool RenderBackend::ViewerIsConnected() const
|
||||
VideoRenderingParams RenderBackend::video_params() const
|
||||
{
|
||||
return viewer_node_;
|
||||
return VideoRenderingParams(viewer_node_->video_params(),
|
||||
pix_fmt_,
|
||||
render_mode_,
|
||||
divider_);
|
||||
}
|
||||
|
||||
const QString &RenderBackend::cache_id() const
|
||||
void RenderBackend::AudioCallback()
|
||||
{
|
||||
return cache_id_;
|
||||
qDebug() << "STUB";
|
||||
/*
|
||||
AudioPlaybackCache* pb_cache = viewer_node_->audio_playback_cache();
|
||||
QThreadPool* thread_pool = audio_copy_map_.thread_pool();
|
||||
|
||||
while (!pb_cache->IsFullyValidated()
|
||||
&& thread_pool->activeThreadCount() < thread_pool->maxThreadCount()) {
|
||||
// FIXME: Trigger a background audio thread
|
||||
TimeRange r = viewer_node_->audio_playback_cache()->GetInvalidatedRanges().first();
|
||||
|
||||
qDebug() << "FIXME: Start rendering audio at:" << r;
|
||||
|
||||
break;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
void RenderBackend::QueueValueUpdate(NodeInput* from)
|
||||
bool RenderBackend::ConformWaitInfo::operator==(const RenderBackend::ConformWaitInfo &rhs) const
|
||||
{
|
||||
if (!input_update_queued_.isEmpty()) {
|
||||
return rhs.stream == stream
|
||||
&& rhs.stream_time == stream_time
|
||||
&& rhs.affected_range == affected_range;
|
||||
}
|
||||
|
||||
RenderBackend::CopyMap::CopyMap() :
|
||||
original_viewer_(nullptr),
|
||||
copied_viewer_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
void RenderBackend::CopyMap::Init(ViewerOutput *viewer)
|
||||
{
|
||||
original_viewer_ = viewer;
|
||||
|
||||
copied_viewer_ = static_cast<ViewerOutput*>(original_viewer_->copy());
|
||||
copy_map_.insert(original_viewer_, copied_viewer_);
|
||||
}
|
||||
|
||||
void RenderBackend::CopyMap::Queue(NodeInput *input)
|
||||
{
|
||||
if (!queued_updates_.isEmpty()) {
|
||||
// Remove any inputs that are dependents of this input since they may have been removed since
|
||||
// it was queued
|
||||
QList<Node*> deps = from->GetDependencies();
|
||||
QList<Node*> deps = input->GetDependencies();
|
||||
|
||||
for (int i=0;i<input_update_queued_.size();i++) {
|
||||
if (deps.contains(input_update_queued_.at(i)->parentNode())) {
|
||||
for (int i=0;i<queued_updates_.size();i++) {
|
||||
if (deps.contains(queued_updates_.at(i)->parentNode())) {
|
||||
// We don't need to queue this value since this input supersedes it
|
||||
input_update_queued_.removeAt(i);
|
||||
queued_updates_.removeAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input_update_queued_.append(from);
|
||||
queued_updates_.append(input);
|
||||
}
|
||||
|
||||
bool RenderBackend::WorkerIsBusy(RenderWorker *worker) const
|
||||
void RenderBackend::CopyMap::ProcessQueue()
|
||||
{
|
||||
return processor_busy_state_.at(processors_.indexOf(worker));
|
||||
while (!queued_updates_.isEmpty()) {
|
||||
CopyNodeInputValue(queued_updates_.takeFirst());
|
||||
}
|
||||
}
|
||||
|
||||
void RenderBackend::SetWorkerBusyState(RenderWorker *worker, bool busy)
|
||||
void RenderBackend::CopyMap::Clear()
|
||||
{
|
||||
processor_busy_state_.replace(processors_.indexOf(worker), busy);
|
||||
qDeleteAll(copy_map_);
|
||||
copy_map_.clear();
|
||||
|
||||
original_viewer_ = nullptr;
|
||||
copied_viewer_ = nullptr;
|
||||
}
|
||||
|
||||
void RenderBackend::CopyNodeInputValue(NodeInput *input)
|
||||
void RenderBackend::CopyMap::CopyNodeInputValue(NodeInput *input)
|
||||
{
|
||||
// Find our copy of this parameter
|
||||
Node* our_copy_node = node_copy_map_.value(input->parentNode());
|
||||
Node* our_copy_node = copy_map_.value(input->parentNode());
|
||||
NodeInput* our_copy = our_copy_node->GetInputWithID(input->id());
|
||||
|
||||
// Copy the standard/keyframe values between these two inputs
|
||||
@@ -372,8 +451,7 @@ void RenderBackend::CopyNodeInputValue(NodeInput *input)
|
||||
QList<Node*> old_deps = our_copy->GetExclusiveDependencies();
|
||||
|
||||
foreach (Node* i, old_deps) {
|
||||
Node* n = node_copy_map_.take(node_copy_map_.key(i));
|
||||
copied_graph_.TakeNode(n);
|
||||
Node* n = copy_map_.take(copy_map_.key(i));
|
||||
delete n;
|
||||
}
|
||||
|
||||
@@ -395,16 +473,15 @@ void RenderBackend::CopyNodeInputValue(NodeInput *input)
|
||||
}
|
||||
}
|
||||
|
||||
Node* RenderBackend::CopyNodeConnections(Node* src_node)
|
||||
Node* RenderBackend::CopyMap::CopyNodeConnections(Node* src_node)
|
||||
{
|
||||
// Check if this node is already in the map
|
||||
Node* dst_node = node_copy_map_.value(src_node);
|
||||
Node* dst_node = copy_map_.value(src_node);
|
||||
|
||||
// If not, create it now
|
||||
if (!dst_node) {
|
||||
dst_node = src_node->copy();
|
||||
copied_graph_.AddNode(dst_node);
|
||||
node_copy_map_.insert(src_node, dst_node);
|
||||
copy_map_.insert(src_node, dst_node);
|
||||
}
|
||||
|
||||
// Make sure its values are copied
|
||||
@@ -423,10 +500,8 @@ Node* RenderBackend::CopyNodeConnections(Node* src_node)
|
||||
return dst_node;
|
||||
}
|
||||
|
||||
void RenderBackend::CopyNodeMakeConnection(NodeInput* src_input, NodeInput* dst_input)
|
||||
void RenderBackend::CopyMap::CopyNodeMakeConnection(NodeInput* src_input, NodeInput* dst_input)
|
||||
{
|
||||
//qDebug() << "Copying input" << src_input->id() << "from" << src_input->parentNode()->id();
|
||||
|
||||
if (src_input->IsConnected()) {
|
||||
Node* dst_node = CopyNodeConnections(src_input->get_connected_node());
|
||||
|
||||
@@ -437,68 +512,4 @@ void RenderBackend::CopyNodeMakeConnection(NodeInput* src_input, NodeInput* dst_
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderBackend::AllProcessorsAreAvailable() const
|
||||
{
|
||||
foreach (bool busy, processor_busy_state_) {
|
||||
if (busy) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const QVector<QThread *> &RenderBackend::threads()
|
||||
{
|
||||
return threads_;
|
||||
}
|
||||
|
||||
void RenderBackend::InvalidateCacheInternal(const rational &start_range, const rational &end_range)
|
||||
{
|
||||
// Add the range to the list
|
||||
cache_queue_.InsertTimeRange(TimeRange(start_range, end_range));
|
||||
|
||||
CacheNext();
|
||||
}
|
||||
|
||||
void RenderBackend::CacheIDChangedEvent(const QString &id)
|
||||
{
|
||||
Q_UNUSED(id)
|
||||
}
|
||||
|
||||
void RenderBackend::WorkerAboutToStartEvent(RenderWorker *worker)
|
||||
{
|
||||
Q_UNUSED(worker)
|
||||
}
|
||||
|
||||
void RenderBackend::InitWorkers()
|
||||
{
|
||||
for (int i=0;i<processors_.size();i++) {
|
||||
RenderWorker* processor = processors_.at(i);
|
||||
QThread* thread = threads().at(i);
|
||||
|
||||
// Connect to it
|
||||
ConnectWorkerToThis(processor);
|
||||
|
||||
// Connect cancel dialog to it
|
||||
connect(processor, &RenderWorker::CompletedCache, cancel_dialog_, &RenderCancelDialog::WorkerDone, Qt::QueuedConnection);
|
||||
|
||||
// Finally, we can move it to its own thread
|
||||
processor->moveToThread(thread);
|
||||
|
||||
// This function blocks the main thread intentionally. See the documentation for this function to see why.
|
||||
processor->Init();
|
||||
}
|
||||
|
||||
processor_busy_state_.resize(processors_.size());
|
||||
processor_busy_state_.fill(false);
|
||||
}
|
||||
|
||||
bool RenderBackend::FootageWaitInfo::operator==(const RenderBackend::FootageWaitInfo &rhs) const
|
||||
{
|
||||
return rhs.stream == stream
|
||||
&& rhs.stream_time == stream_time
|
||||
&& rhs.affected_range == affected_range;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
@@ -21,153 +21,126 @@
|
||||
#ifndef RENDERBACKEND_H
|
||||
#define RENDERBACKEND_H
|
||||
|
||||
#include <QLinkedList>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#include "dialog/rendercancel/rendercancel.h"
|
||||
#include "decodercache.h"
|
||||
#include "node/graph.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "renderworker.h"
|
||||
#include "node/traverser.h"
|
||||
#include "render/backend/colorprocessorcache.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class RenderBackend : public QObject
|
||||
class RenderBackend : public QObject, public NodeTraverser
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
RenderBackend(QObject* parent = nullptr);
|
||||
|
||||
bool Init();
|
||||
|
||||
void Close();
|
||||
|
||||
const QString& GetError() const;
|
||||
|
||||
void SetViewerNode(ViewerOutput* viewer_node);
|
||||
|
||||
bool IsInitiated();
|
||||
|
||||
ViewerOutput* viewer_node() const;
|
||||
|
||||
void CancelQueue();
|
||||
|
||||
public slots:
|
||||
void InvalidateCache(const TimeRange &range, NodeInput *from);
|
||||
QFuture<QByteArray> Hash(const rational& time);
|
||||
|
||||
signals:
|
||||
void QueueComplete();
|
||||
QFuture<FramePtr> RenderFrame(const rational& time);
|
||||
|
||||
void SetDivider(const int& divider);
|
||||
|
||||
void SetMode(const RenderMode::Mode& mode);
|
||||
|
||||
void SetPixelFormat(const PixelFormat::Format& pix_fmt);
|
||||
|
||||
void SetSampleFormat(const SampleFormat::Format& sample_fmt);
|
||||
|
||||
public slots:
|
||||
void NodeGraphChanged(NodeInput *from, NodeInput *source);
|
||||
|
||||
protected:
|
||||
void RegenerateCacheID();
|
||||
virtual void TextureToFrame(const QVariant& texture, FramePtr frame) const = 0;
|
||||
|
||||
virtual bool InitInternal();
|
||||
virtual QVariant FrameToTexture(FramePtr frame) const = 0;
|
||||
|
||||
virtual void CloseInternal();
|
||||
virtual void FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable* table) const override;
|
||||
|
||||
virtual bool CanRender();
|
||||
|
||||
virtual TimeRange PopNextFrameFromQueue();
|
||||
|
||||
rational GetSequenceLength();
|
||||
|
||||
const QVector<QThread*>& threads();
|
||||
|
||||
/**
|
||||
* @brief Internal function for generating the cache ID
|
||||
*/
|
||||
virtual bool GenerateCacheIDInternal(QCryptographicHash& hash) = 0;
|
||||
|
||||
virtual void InvalidateCacheInternal(const rational &start_range, const rational &end_range);
|
||||
|
||||
virtual void CacheIDChangedEvent(const QString& id);
|
||||
|
||||
virtual void WorkerAboutToStartEvent(RenderWorker* worker);
|
||||
|
||||
void SetError(const QString& error);
|
||||
|
||||
virtual void ConnectViewer(ViewerOutput* node);
|
||||
virtual void DisconnectViewer(ViewerOutput* node);
|
||||
|
||||
/**
|
||||
* @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();
|
||||
|
||||
void InitWorkers();
|
||||
|
||||
virtual NodeInput* GetDependentInput() = 0;
|
||||
|
||||
virtual void ConnectWorkerToThis(RenderWorker* worker) = 0;
|
||||
|
||||
bool ViewerIsConnected() const;
|
||||
|
||||
const QString& cache_id() const;
|
||||
|
||||
void QueueValueUpdate(NodeInput *from);
|
||||
|
||||
bool AllProcessorsAreAvailable() const;
|
||||
bool WorkerIsBusy(RenderWorker* worker) const;
|
||||
void SetWorkerBusyState(RenderWorker* worker, bool busy);
|
||||
|
||||
TimeRangeList cache_queue_;
|
||||
|
||||
QVector<RenderWorker*> processors_;
|
||||
|
||||
QHash<TimeRange, qint64> render_job_info_;
|
||||
|
||||
QHash<Node*, Node*> node_copy_map_;
|
||||
|
||||
NodeGraph copied_graph_;
|
||||
virtual NodeValueTable GenerateBlockTable(const TrackOutput *track, const TimeRange &range) const override;
|
||||
|
||||
private:
|
||||
void CopyNodeInputValue(NodeInput* input);
|
||||
Node *CopyNodeConnections(Node *src_node);
|
||||
void CopyNodeMakeConnection(NodeInput *src_input, NodeInput *dst_input);
|
||||
|
||||
/**
|
||||
* @brief Internal list of RenderProcessThreads
|
||||
*/
|
||||
QVector<QThread*> threads_;
|
||||
|
||||
/**
|
||||
* @brief Internal variable that contains whether the Renderer has started or not
|
||||
*/
|
||||
bool started_;
|
||||
FramePtr RenderFrameInternal(const rational& time) const;
|
||||
|
||||
/**
|
||||
* @brief Internal reference to attached viewer node
|
||||
*/
|
||||
ViewerOutput* viewer_node_;
|
||||
|
||||
/**
|
||||
* @brief Internal reference to the copied viewer node we made in the compilation process
|
||||
*/
|
||||
ViewerOutput* copied_viewer_node_;
|
||||
|
||||
/**
|
||||
* @brief Error string that can be set in SetError() to handle failures
|
||||
*/
|
||||
QString error_;
|
||||
|
||||
QString cache_id_;
|
||||
|
||||
QList<NodeInput*> input_update_queued_;
|
||||
|
||||
QVector<bool> processor_busy_state_;
|
||||
|
||||
RenderCancelDialog* cancel_dialog_;
|
||||
|
||||
struct FootageWaitInfo {
|
||||
VideoRenderingParams video_params() const;
|
||||
|
||||
class CopyMap {
|
||||
public:
|
||||
CopyMap();
|
||||
|
||||
void Init(ViewerOutput* viewer);
|
||||
|
||||
void Queue(NodeInput* input);
|
||||
|
||||
void ProcessQueue();
|
||||
|
||||
void Clear();
|
||||
|
||||
QThreadPool* thread_pool() {
|
||||
return &thread_pool_;
|
||||
}
|
||||
|
||||
private:
|
||||
void CopyNodeInputValue(NodeInput* input);
|
||||
Node *CopyNodeConnections(Node *src_node);
|
||||
void CopyNodeMakeConnection(NodeInput *src_input, NodeInput *dst_input);
|
||||
|
||||
ViewerOutput* original_viewer_;
|
||||
ViewerOutput* copied_viewer_;
|
||||
QList<NodeInput*> queued_updates_;
|
||||
QHash<Node*, Node*> copy_map_;
|
||||
QThreadPool thread_pool_;
|
||||
|
||||
};
|
||||
|
||||
// VIDEO MEMBERS
|
||||
CopyMap video_copy_map_;
|
||||
int divider_;
|
||||
RenderMode::Mode render_mode_;
|
||||
PixelFormat::Format pix_fmt_;
|
||||
|
||||
ColorProcessorCache color_cache_;
|
||||
|
||||
struct CachedStill {
|
||||
NodeValue texture;
|
||||
QString colorspace;
|
||||
bool alpha_is_associated;
|
||||
int divider;
|
||||
rational time;
|
||||
};
|
||||
|
||||
RenderCache<Stream*, CachedStill> still_image_cache_;
|
||||
|
||||
// AUDIO MEMBERS
|
||||
CopyMap audio_copy_map_;
|
||||
SampleFormat::Format sample_fmt_;
|
||||
|
||||
struct ConformWaitInfo {
|
||||
StreamPtr stream;
|
||||
TimeRange affected_range;
|
||||
rational stream_time;
|
||||
|
||||
bool operator==(const FootageWaitInfo& rhs) const;
|
||||
bool operator==(const ConformWaitInfo& rhs) const;
|
||||
};
|
||||
|
||||
QList<FootageWaitInfo> footage_wait_info_;
|
||||
QList<ConformWaitInfo> footage_wait_info_;
|
||||
|
||||
private slots:
|
||||
void AudioCallback();
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/***
|
||||
|
||||
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 "renderworker.h"
|
||||
|
||||
#include <QThread>
|
||||
|
||||
#include "node/block/block.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
RenderWorker::RenderWorker(QObject *parent) :
|
||||
QObject(parent),
|
||||
started_(false)
|
||||
{
|
||||
}
|
||||
|
||||
bool RenderWorker::Init()
|
||||
{
|
||||
if (started_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(started_ = InitInternal())) {
|
||||
Close();
|
||||
}
|
||||
|
||||
return started_;
|
||||
}
|
||||
|
||||
void RenderWorker::Close()
|
||||
{
|
||||
CloseInternal();
|
||||
|
||||
decoder_cache_.Clear();
|
||||
|
||||
started_ = false;
|
||||
}
|
||||
|
||||
void RenderWorker::Render(NodeDependency path, qint64 job_time)
|
||||
{
|
||||
path_ = path;
|
||||
|
||||
emit CompletedCache(path, RenderInternal(path, job_time), job_time);
|
||||
}
|
||||
|
||||
NodeValueTable RenderWorker::RenderInternal(const NodeDependency &path, const qint64 &job_time)
|
||||
{
|
||||
Q_UNUSED(job_time)
|
||||
|
||||
return ProcessNode(path);
|
||||
}
|
||||
|
||||
void RenderWorker::RunNodeAccelerated(const Node *node, const TimeRange &range, NodeValueDatabase &input_params, NodeValueTable& output_params)
|
||||
{
|
||||
Q_UNUSED(node)
|
||||
Q_UNUSED(range)
|
||||
Q_UNUSED(input_params)
|
||||
Q_UNUSED(output_params)
|
||||
}
|
||||
|
||||
DecoderPtr RenderWorker::ResolveDecoderFromInput(StreamPtr stream)
|
||||
{
|
||||
// Access a map of Node inputs and decoder instances and retrieve a frame!
|
||||
|
||||
DecoderPtr decoder = decoder_cache_.Get(stream.get());
|
||||
|
||||
if (!decoder && stream) {
|
||||
// Create a new Decoder here
|
||||
decoder = Decoder::CreateFromID(stream->footage()->decoder());
|
||||
decoder->set_stream(stream);
|
||||
|
||||
if (decoder->Open()) {
|
||||
decoder_cache_.Add(stream.get(), decoder);
|
||||
} else {
|
||||
decoder = nullptr;
|
||||
qWarning() << "Failed to open decoder for" << stream->footage()->filename() << "::" << stream->index();
|
||||
}
|
||||
}
|
||||
|
||||
return decoder;
|
||||
}
|
||||
|
||||
NodeValue RenderWorker::GetDataFromStream(StreamPtr stream, const TimeRange &input_time)
|
||||
{
|
||||
DecoderPtr decoder = ResolveDecoderFromInput(stream);
|
||||
|
||||
if (decoder) {
|
||||
return FrameToValue(decoder, stream, input_time);
|
||||
}
|
||||
|
||||
return NodeValue();
|
||||
}
|
||||
|
||||
bool RenderWorker::IsStarted()
|
||||
{
|
||||
return started_;
|
||||
}
|
||||
|
||||
void RenderWorker::ProcessNodeEvent(const Node *node, const TimeRange &range, NodeValueDatabase &input_params, NodeValueTable &output_params)
|
||||
{
|
||||
// Check if we have a shader for this output
|
||||
RunNodeAccelerated(node, range, input_params, output_params);
|
||||
}
|
||||
|
||||
const NodeDependency &RenderWorker::CurrentPath() const
|
||||
{
|
||||
return path_;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,81 +0,0 @@
|
||||
/***
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef RENDERWORKER_H
|
||||
#define RENDERWORKER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "decodercache.h"
|
||||
#include "node/node.h"
|
||||
#include "node/output/track/track.h"
|
||||
#include "node/traverser.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class RenderWorker : public QObject, public NodeTraverser
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
RenderWorker(QObject* parent = nullptr);
|
||||
|
||||
bool Init();
|
||||
|
||||
bool IsStarted();
|
||||
|
||||
public slots:
|
||||
void Close();
|
||||
|
||||
void Render(OLIVE_NAMESPACE::NodeDependency path, qint64 job_time);
|
||||
|
||||
signals:
|
||||
void CompletedCache(OLIVE_NAMESPACE::NodeDependency dep, OLIVE_NAMESPACE::NodeValueTable data, qint64 job_time);
|
||||
|
||||
protected:
|
||||
virtual bool InitInternal() = 0;
|
||||
|
||||
virtual void CloseInternal() = 0;
|
||||
|
||||
virtual NodeValueTable RenderInternal(const NodeDependency& CurrentPath, const qint64& job_time);
|
||||
|
||||
virtual void RunNodeAccelerated(const Node *node, const TimeRange& range, NodeValueDatabase &input_params, NodeValueTable &output_params);
|
||||
|
||||
virtual NodeValue FrameToValue(DecoderPtr decoder, StreamPtr stream, const TimeRange &range) = 0;
|
||||
|
||||
virtual void ProcessNodeEvent(const Node *node, const TimeRange &range, NodeValueDatabase &input_params, NodeValueTable &output_params) override;
|
||||
|
||||
DecoderPtr ResolveDecoderFromInput(StreamPtr stream);
|
||||
|
||||
NodeValue GetDataFromStream(StreamPtr stream, const TimeRange& input_time);
|
||||
|
||||
const NodeDependency& CurrentPath() const;
|
||||
|
||||
private:
|
||||
bool started_;
|
||||
|
||||
DecoderCache decoder_cache_;
|
||||
|
||||
NodeDependency path_;
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // RENDERWORKER_H
|
||||
@@ -1,354 +0,0 @@
|
||||
/***
|
||||
|
||||
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 "config/config.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/pixelformat.h"
|
||||
#include "videorenderworker.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
VideoRenderBackend::VideoRenderBackend(QObject *parent) :
|
||||
RenderBackend(parent),
|
||||
operating_mode_(VideoRenderWorker::kRenderOnly),
|
||||
only_signal_last_frame_requested_(true),
|
||||
limit_caching_(true)
|
||||
{
|
||||
connect(DiskManager::instance(), &DiskManager::DeletedFrame, this, &VideoRenderBackend::FrameRemovedFromDiskCache);
|
||||
}
|
||||
|
||||
void VideoRenderBackend::ConnectViewer(ViewerOutput *node)
|
||||
{
|
||||
connect(node, &ViewerOutput::VideoChangedBetween, this, &VideoRenderBackend::InvalidateCache);
|
||||
connect(node, &ViewerOutput::LengthChanged, this, &VideoRenderBackend::TruncateFrameCacheLength);
|
||||
}
|
||||
|
||||
void VideoRenderBackend::DisconnectViewer(ViewerOutput *node)
|
||||
{
|
||||
disconnect(node, &ViewerOutput::VideoChangedBetween, this, &VideoRenderBackend::InvalidateCache);
|
||||
disconnect(node, &ViewerOutput::LengthChanged, this, &VideoRenderBackend::TruncateFrameCacheLength);
|
||||
|
||||
frame_cache_.Clear();
|
||||
}
|
||||
|
||||
const VideoRenderingParams &VideoRenderBackend::params() const
|
||||
{
|
||||
return params_;
|
||||
}
|
||||
|
||||
void VideoRenderBackend::SetParameters(const VideoRenderingParams& params)
|
||||
{
|
||||
CancelQueue();
|
||||
|
||||
// Set new parameters
|
||||
params_ = params;
|
||||
|
||||
// Handle custom events from derivatives
|
||||
ParamsChangedEvent();
|
||||
|
||||
// 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)
|
||||
{
|
||||
CancelQueue();
|
||||
|
||||
operating_mode_ = mode;
|
||||
|
||||
foreach (RenderWorker* worker, processors_) {
|
||||
static_cast<VideoRenderWorker*>(worker)->SetOperatingMode(operating_mode_);
|
||||
}
|
||||
}
|
||||
|
||||
void VideoRenderBackend::SetFrameGenerationParams(int width, int height, const QMatrix4x4 &matrix)
|
||||
{
|
||||
foreach (RenderWorker* worker, processors_) {
|
||||
static_cast<VideoRenderWorker*>(worker)->SetFrameGenerationParams(width, height, matrix);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void VideoRenderBackend::SetLimitCaching(bool limit)
|
||||
{
|
||||
limit_caching_ = limit;
|
||||
}
|
||||
|
||||
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::HashAlreadyBeingCached, this, &VideoRenderBackend::ThreadSkippedFrame, Qt::QueuedConnection);
|
||||
connect(video_processor, &VideoRenderWorker::CompletedDownload, this, &VideoRenderBackend::ThreadCompletedDownload, Qt::QueuedConnection);
|
||||
connect(video_processor, &VideoRenderWorker::HashAlreadyExists, this, &VideoRenderBackend::ThreadHashAlreadyExists, Qt::QueuedConnection);
|
||||
connect(video_processor, &VideoRenderWorker::GeneratedFrame, this, &VideoRenderBackend::GeneratedFrame, Qt::QueuedConnection);
|
||||
connect(video_processor, &VideoRenderWorker::GeneratedFrame, this, &VideoRenderBackend::ThreadGeneratedFrame, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void VideoRenderBackend::InvalidateCacheInternal(const rational &start_range, const rational &end_range)
|
||||
{
|
||||
TimeRange invalidated(start_range, end_range);
|
||||
|
||||
if (limit_caching_) {
|
||||
invalidated_.InsertTimeRange(invalidated);
|
||||
} else {
|
||||
RenderBackend::InvalidateCacheInternal(start_range, end_range);
|
||||
}
|
||||
|
||||
emit RangeInvalidated(invalidated);
|
||||
}
|
||||
|
||||
void VideoRenderBackend::RenderFrame(const rational &time)
|
||||
{
|
||||
RenderBackend::InvalidateCacheInternal(time, time);
|
||||
}
|
||||
|
||||
VideoRenderFrameCache *VideoRenderBackend::frame_cache()
|
||||
{
|
||||
return &frame_cache_;
|
||||
}
|
||||
|
||||
QString VideoRenderBackend::GetCachedFrame(const rational &time)
|
||||
{
|
||||
UpdateLastRequestedTime(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()) {
|
||||
DiskManager::instance()->Accessed(frame_hash);
|
||||
|
||||
return frame_cache_.CachePathName(frame_hash, params_.format());
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
void VideoRenderBackend::UpdateLastRequestedTime(const rational &time)
|
||||
{
|
||||
last_time_requested_ = time;
|
||||
}
|
||||
|
||||
NodeInput *VideoRenderBackend::GetDependentInput()
|
||||
{
|
||||
return viewer_node()->texture_input();
|
||||
}
|
||||
|
||||
bool VideoRenderBackend::CanRender()
|
||||
{
|
||||
return params_.is_valid();
|
||||
}
|
||||
|
||||
TimeRange VideoRenderBackend::PopNextFrameFromQueue()
|
||||
{
|
||||
rational earliest_frame = RATIONAL_MAX;
|
||||
|
||||
// Find earliest frame in the cache queue
|
||||
foreach (const TimeRange& range, cache_queue_) {
|
||||
if (range.in() < earliest_frame) {
|
||||
earliest_frame = range.in();
|
||||
}
|
||||
}
|
||||
|
||||
// Snap this frame to the timebase
|
||||
rational snapped_frame = Timecode::snap_time_to_timebase(earliest_frame, params_.time_base());
|
||||
rational next_frame;
|
||||
|
||||
if (snapped_frame > earliest_frame) {
|
||||
next_frame = snapped_frame;
|
||||
snapped_frame -= params_.time_base();
|
||||
} else {
|
||||
next_frame = snapped_frame + params_.time_base();
|
||||
}
|
||||
|
||||
TimeRange frame_range(snapped_frame, next_frame);
|
||||
|
||||
// Remove this particular frame from the queue
|
||||
cache_queue_.RemoveTimeRange(frame_range);
|
||||
|
||||
// Remove this particular frame from missing frames
|
||||
invalidated_.RemoveTimeRange(frame_range);
|
||||
|
||||
// Return the snapped frame
|
||||
return TimeRange(frame_range.in(), frame_range.in());
|
||||
}
|
||||
|
||||
void VideoRenderBackend::ThreadCompletedDownload(NodeDependency dep, qint64 job_time, QByteArray hash)
|
||||
{
|
||||
SetWorkerBusyState(static_cast<RenderWorker*>(sender()), false);
|
||||
|
||||
SetFrameHash(dep, hash, job_time);
|
||||
|
||||
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, params_.format())) {
|
||||
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::ThreadGeneratedFrame()
|
||||
{
|
||||
SetWorkerBusyState(static_cast<RenderWorker*>(sender()), false);
|
||||
|
||||
CacheNext();
|
||||
}
|
||||
|
||||
void VideoRenderBackend::TruncateFrameCacheLength(const rational &length)
|
||||
{
|
||||
// Remove frames after this time code if it's changed
|
||||
frame_cache_.Truncate(length);
|
||||
|
||||
invalidated_.RemoveTimeRange(TimeRange(length, RATIONAL_MAX));
|
||||
|
||||
// If the playhead is past the length, update the viewer to a null texture because it won't be cached through the
|
||||
// queue, but will now be a null texture
|
||||
if (last_time_requested_ >= length) {
|
||||
emit CachedTimeReady(last_time_requested_, QDateTime::currentMSecsSinceEpoch());
|
||||
}
|
||||
}
|
||||
|
||||
void VideoRenderBackend::FrameRemovedFromDiskCache(const QByteArray &hash)
|
||||
{
|
||||
QList<rational> deleted_frames = frame_cache()->TakeFramesWithHash(hash);
|
||||
|
||||
foreach (const rational& frame, deleted_frames) {
|
||||
TimeRange invalidated(frame, frame+params_.time_base());
|
||||
|
||||
invalidated_.InsertTimeRange(invalidated);
|
||||
|
||||
emit RangeInvalidated(invalidated);
|
||||
}
|
||||
}
|
||||
|
||||
bool VideoRenderBackend::TimeIsQueued(const TimeRange &time) const
|
||||
{
|
||||
return cache_queue_.ContainsTimeRange(time, true, false);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,150 +0,0 @@
|
||||
/***
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef VIDEORENDERERBACKEND_H
|
||||
#define VIDEORENDERERBACKEND_H
|
||||
|
||||
#include <QLinkedList>
|
||||
|
||||
#include "colorprocessorcache.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "renderbackend.h"
|
||||
#include "render/pixelformat.h"
|
||||
#include "render/rendermodes.h"
|
||||
#include "videorenderframecache.h"
|
||||
#include "videorenderworker.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
/**
|
||||
* @brief A multithreaded OpenGL based renderer for node systems
|
||||
*/
|
||||
class VideoRenderBackend : public RenderBackend
|
||||
{
|
||||
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.
|
||||
*/
|
||||
VideoRenderBackend(QObject* parent = nullptr);
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
void SetParameters(const VideoRenderingParams ¶ms);
|
||||
|
||||
void SetOperatingMode(const VideoRenderWorker::OperatingMode& mode);
|
||||
|
||||
void SetFrameGenerationParams(int width, int height, const QMatrix4x4& matrix);
|
||||
|
||||
void SetOnlySignalLastFrameRequested(bool enabled);
|
||||
|
||||
bool IsRendered(const rational& time) const;
|
||||
|
||||
void SetLimitCaching(bool limit);
|
||||
|
||||
QString GetCachedFrame(const rational& time);
|
||||
|
||||
void UpdateLastRequestedTime(const rational& time);
|
||||
|
||||
VideoRenderFrameCache* frame_cache();
|
||||
|
||||
const VideoRenderingParams& params() const;
|
||||
|
||||
void RenderFrame(const rational& time);
|
||||
|
||||
protected:
|
||||
struct HashTimeMapping {
|
||||
rational time;
|
||||
QByteArray hash;
|
||||
};
|
||||
|
||||
virtual void ConnectViewer(ViewerOutput* node) override;
|
||||
|
||||
virtual void DisconnectViewer(ViewerOutput* node) override;
|
||||
|
||||
virtual NodeInput* GetDependentInput() override;
|
||||
|
||||
virtual bool CanRender() override;
|
||||
|
||||
virtual TimeRange PopNextFrameFromQueue() override;
|
||||
|
||||
/**
|
||||
* @brief Internal function for generating the cache ID
|
||||
*/
|
||||
virtual bool GenerateCacheIDInternal(QCryptographicHash& hash) override;
|
||||
|
||||
virtual void CacheIDChangedEvent(const QString& id) override;
|
||||
|
||||
virtual void ConnectWorkerToThis(RenderWorker* processor) override;
|
||||
|
||||
virtual void InvalidateCacheInternal(const rational &start_range, const rational &end_range) override;
|
||||
|
||||
virtual void ParamsChangedEvent(){}
|
||||
|
||||
VideoRenderWorker::OperatingMode operating_mode_;
|
||||
|
||||
signals:
|
||||
void CachedTimeReady(const rational& time, qint64 job_time);
|
||||
|
||||
void RangeInvalidated(const TimeRange& range);
|
||||
|
||||
void GeneratedFrame(FramePtr frame);
|
||||
|
||||
private:
|
||||
bool TimeIsQueued(const TimeRange &time) const;
|
||||
|
||||
bool JobIsCurrent(const NodeDependency &dep, const qint64& job_time) const;
|
||||
|
||||
bool SetFrameHash(const NodeDependency& dep, const QByteArray& hash, const qint64& job_time);
|
||||
|
||||
VideoRenderingParams params_;
|
||||
|
||||
VideoRenderFrameCache frame_cache_;
|
||||
|
||||
TimeRangeList invalidated_;
|
||||
|
||||
rational last_time_requested_;
|
||||
|
||||
bool only_signal_last_frame_requested_;
|
||||
|
||||
bool limit_caching_;
|
||||
|
||||
private slots:
|
||||
void ThreadCompletedDownload(NodeDependency dep, qint64 job_time, QByteArray hash);
|
||||
void ThreadSkippedFrame(NodeDependency dep, qint64 job_time, QByteArray hash);
|
||||
void ThreadHashAlreadyExists(NodeDependency dep, qint64 job_time, QByteArray hash);
|
||||
void ThreadGeneratedFrame();
|
||||
|
||||
void TruncateFrameCacheLength(const rational& length);
|
||||
|
||||
void FrameRemovedFromDiskCache(const QByteArray& hash);
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // VIDEORENDERERBACKEND_H
|
||||
@@ -1,251 +0,0 @@
|
||||
/***
|
||||
|
||||
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 "videorenderworker.h"
|
||||
|
||||
#include "common/define.h"
|
||||
#include "common/functiontimer.h"
|
||||
#include "node/block/transition/transition.h"
|
||||
#include "node/node.h"
|
||||
#include "project/project.h"
|
||||
#include "render/pixelformat.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
VideoRenderWorker::VideoRenderWorker(VideoRenderFrameCache *frame_cache, QObject *parent) :
|
||||
RenderWorker(parent),
|
||||
frame_cache_(frame_cache),
|
||||
operating_mode_(kHashRenderCache)
|
||||
{
|
||||
}
|
||||
|
||||
const VideoRenderingParams &VideoRenderWorker::video_params()
|
||||
{
|
||||
return video_params_;
|
||||
}
|
||||
|
||||
void VideoRenderWorker::TextureToBuffer(const QVariant &texture, void *buffer, int linesize)
|
||||
{
|
||||
TextureToBuffer(texture,
|
||||
video_params_.effective_width(),
|
||||
video_params_.effective_height(),
|
||||
QMatrix4x4(),
|
||||
buffer,
|
||||
linesize);
|
||||
}
|
||||
|
||||
NodeValueTable VideoRenderWorker::RenderInternal(const NodeDependency& path, const qint64 &job_time)
|
||||
{
|
||||
// Get hash of node graph
|
||||
// We use SHA-1 for speed (benchmarks show it's the fastest hash available to us)
|
||||
QByteArray hash;
|
||||
if (operating_mode_ & kHashOnly) {
|
||||
QCryptographicHash hasher(QCryptographicHash::Sha1);
|
||||
|
||||
// Embed video parameters into this hash
|
||||
int vwidth = video_params_.effective_width();
|
||||
int vheight = video_params_.effective_height();
|
||||
PixelFormat::Format vfmt = video_params_.format();
|
||||
RenderMode::Mode vmode = video_params_.mode();
|
||||
|
||||
hasher.addData(reinterpret_cast<const char*>(&vwidth), sizeof(int));
|
||||
hasher.addData(reinterpret_cast<const char*>(&vheight), sizeof(int));
|
||||
hasher.addData(reinterpret_cast<const char*>(&vfmt), sizeof(PixelFormat::Format));
|
||||
hasher.addData(reinterpret_cast<const char*>(&vmode), sizeof(RenderMode::Mode));
|
||||
|
||||
path.node()->Hash(hasher, path.in());
|
||||
hash = hasher.result();
|
||||
}
|
||||
|
||||
NodeValueTable value;
|
||||
|
||||
if (!(operating_mode_ & kRenderOnly)) {
|
||||
|
||||
// Emit only the hash
|
||||
emit CompletedDownload(path, job_time, hash);
|
||||
|
||||
} else if ((operating_mode_ & kHashOnly) && frame_cache_->HasHash(hash, video_params_.format())) {
|
||||
|
||||
// We've already cached this hash, no need to continue
|
||||
emit HashAlreadyExists(path, job_time, hash);
|
||||
|
||||
} else if (!(operating_mode_ & kHashOnly) || frame_cache_->TryCache(hash)) {
|
||||
|
||||
// This hash is available for us to cache, start traversing graph
|
||||
value = ProcessNode(path);
|
||||
|
||||
// Find texture in hash
|
||||
QVariant texture = value.Get(NodeParam::kTexture);
|
||||
|
||||
// If we actually have a texture, download it into the disk cache
|
||||
if (!texture.isNull() || (!(operating_mode_ & kDownloadOnly))) {
|
||||
Download(hash, path.in(), texture);
|
||||
}
|
||||
|
||||
frame_cache_->RemoveHashFromCurrentlyCaching(hash);
|
||||
|
||||
// Signal that this job is complete
|
||||
if (operating_mode_ & kDownloadOnly) {
|
||||
emit CompletedDownload(path, job_time, hash);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Another thread must be caching this already, nothing to be done
|
||||
emit HashAlreadyBeingCached(path, job_time, hash);
|
||||
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
void VideoRenderWorker::FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable *table)
|
||||
{
|
||||
if (stream->type() != Stream::kVideo && stream->type() != Stream::kImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImageStreamPtr video_stream = std::static_pointer_cast<ImageStream>(stream);
|
||||
rational time_match = (stream->type() == Stream::kImage) ? rational() : input_time.in();
|
||||
QString colorspace_match = video_stream->get_colorspace_match_string();
|
||||
|
||||
NodeValue value;
|
||||
bool found_cache = false;
|
||||
|
||||
if (still_image_cache_.Has(stream.get())) {
|
||||
CachedStill cs = still_image_cache_.Get(stream.get());
|
||||
|
||||
if (cs.colorspace == colorspace_match
|
||||
&& cs.alpha_is_associated == video_stream->premultiplied_alpha()
|
||||
&& cs.divider == video_params_.divider()
|
||||
&& cs.time == time_match) {
|
||||
value = cs.texture;
|
||||
found_cache = true;
|
||||
} else {
|
||||
still_image_cache_.Remove(stream.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_cache) {
|
||||
|
||||
value = GetDataFromStream(stream, input_time);
|
||||
|
||||
still_image_cache_.Add(stream.get(), {value,
|
||||
colorspace_match,
|
||||
video_stream->premultiplied_alpha(),
|
||||
video_params_.divider(),
|
||||
time_match});
|
||||
|
||||
}
|
||||
|
||||
table->Push(value);
|
||||
}
|
||||
|
||||
void VideoRenderWorker::SetParameters(const VideoRenderingParams &video_params)
|
||||
{
|
||||
video_params_ = video_params;
|
||||
|
||||
if (IsStarted()) {
|
||||
ResizeDownloadBuffer();
|
||||
}
|
||||
|
||||
ParametersChangedEvent();
|
||||
}
|
||||
|
||||
void VideoRenderWorker::SetOperatingMode(const VideoRenderWorker::OperatingMode &mode)
|
||||
{
|
||||
operating_mode_ = mode;
|
||||
}
|
||||
|
||||
void VideoRenderWorker::SetFrameGenerationParams(int width, int height, const QMatrix4x4 &matrix)
|
||||
{
|
||||
frame_gen_params_ = VideoRenderingParams(width,
|
||||
height,
|
||||
video_params_.time_base(),
|
||||
video_params_.format(),
|
||||
video_params_.mode(),
|
||||
video_params_.divider());
|
||||
frame_gen_mat_ = matrix;
|
||||
}
|
||||
|
||||
bool VideoRenderWorker::InitInternal()
|
||||
{
|
||||
if (video_params_.is_valid()) {
|
||||
ResizeDownloadBuffer();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VideoRenderWorker::CloseInternal()
|
||||
{
|
||||
download_buffer_.clear();
|
||||
}
|
||||
|
||||
void VideoRenderWorker::Download(const QByteArray& hash, const rational& time, QVariant texture)
|
||||
{
|
||||
if (operating_mode_ & kDownloadOnly) {
|
||||
|
||||
TextureToBuffer(texture, download_buffer_.data(), 0);
|
||||
|
||||
frame_cache_->SaveCacheFrame(hash,
|
||||
download_buffer_.data(),
|
||||
VideoRenderingParams(video_params_.effective_width(),
|
||||
video_params_.effective_height(),
|
||||
video_params_.format()));
|
||||
|
||||
} else {
|
||||
|
||||
FramePtr frame = Frame::Create();
|
||||
|
||||
if (frame_gen_params_.is_valid()) {
|
||||
frame->set_video_params(frame_gen_params_);
|
||||
} else {
|
||||
frame->set_video_params(VideoRenderingParams(video_params_.effective_width(),
|
||||
video_params_.effective_height(),
|
||||
video_params_.format()));
|
||||
}
|
||||
|
||||
frame->allocate();
|
||||
|
||||
if (texture.isNull()) {
|
||||
memset(frame->data(), 0, frame->allocated_size());
|
||||
} else {
|
||||
TextureToBuffer(texture, frame->width(), frame->height(), frame_gen_mat_, frame->data(), frame->linesize_pixels());
|
||||
}
|
||||
|
||||
frame->set_timestamp(time);
|
||||
|
||||
emit GeneratedFrame(frame);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void VideoRenderWorker::ResizeDownloadBuffer()
|
||||
{
|
||||
download_buffer_.resize(PixelFormat::GetBufferSize(video_params_.format(), video_params_.effective_width(), video_params_.effective_height()));
|
||||
}
|
||||
|
||||
ColorProcessorCache *VideoRenderWorker::color_cache()
|
||||
{
|
||||
return &color_cache_;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,140 +0,0 @@
|
||||
/***
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef VIDEORENDERWORKER_H
|
||||
#define VIDEORENDERWORKER_H
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QMatrix4x4>
|
||||
|
||||
#include "colorprocessorcache.h"
|
||||
#include "node/dependency.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "renderworker.h"
|
||||
#include "videorenderframecache.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class VideoRenderWorker : public RenderWorker {
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* @brief VideoRenderWorker uses hashes to recognize frames that are identical to others in the render queue
|
||||
*
|
||||
* This mode can modify the behavior of the worker, either to disable hash verification or only generate hashes and
|
||||
* not render. These are only used in the context of exporting.
|
||||
*
|
||||
* These are also flags that can be or'd together, mostly for the convenience of checking which functionalities are
|
||||
* disabled and enabled.
|
||||
*/
|
||||
enum OperatingMode {
|
||||
/// Generate hashes but don't render or download anything
|
||||
kHashOnly = 0x1,
|
||||
|
||||
/// Render but don't download or hash
|
||||
kRenderOnly = 0x2,
|
||||
|
||||
/// Enable download (NEVER USE THIS ON ITS OWN, this is only here for checking flags, download-only mode makes no sense)
|
||||
kDownloadOnly = 0x4,
|
||||
|
||||
/// Use hash verification and render, but don't cache any frames to disk
|
||||
kHashAndRenderOnly = 0x3,
|
||||
|
||||
/// Render and download, but don't use hash verification
|
||||
kRenderAndCacheOnly = 0x6,
|
||||
|
||||
/// Render and use hashes to identify exact matches (default)
|
||||
kHashRenderCache = 0x7
|
||||
};
|
||||
|
||||
VideoRenderWorker(VideoRenderFrameCache* frame_cache, QObject* parent = nullptr);
|
||||
|
||||
void SetParameters(const VideoRenderingParams& video_params);
|
||||
|
||||
void SetOperatingMode(const OperatingMode& mode);
|
||||
|
||||
void SetFrameGenerationParams(int width, int height, const QMatrix4x4 &matrix);
|
||||
|
||||
signals:
|
||||
void CompletedDownload(NodeDependency path, qint64 job_time, QByteArray hash);
|
||||
|
||||
void HashAlreadyBeingCached(NodeDependency path, qint64 job_time, QByteArray hash);
|
||||
|
||||
void HashAlreadyExists(NodeDependency path, qint64 job_time, QByteArray hash);
|
||||
|
||||
void GeneratedFrame(FramePtr frame);
|
||||
|
||||
void Aborted();
|
||||
|
||||
protected:
|
||||
virtual bool InitInternal() override;
|
||||
|
||||
virtual void CloseInternal() override;
|
||||
|
||||
const VideoRenderingParams& video_params();
|
||||
|
||||
virtual void ParametersChangedEvent(){}
|
||||
|
||||
void TextureToBuffer(const QVariant& texture, void *buffer, int linesize);
|
||||
|
||||
virtual void TextureToBuffer(const QVariant& texture, int width, int height, const QMatrix4x4& matrix, void *buffer, int linesize) = 0;
|
||||
|
||||
virtual NodeValueTable RenderInternal(const NodeDependency& CurrentPath, const qint64& job_time) override;
|
||||
|
||||
virtual void FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable* table) override;
|
||||
|
||||
ColorProcessorCache* color_cache();
|
||||
|
||||
private:
|
||||
void Download(const QByteArray &hash, const rational &time, QVariant texture);
|
||||
|
||||
void ResizeDownloadBuffer();
|
||||
|
||||
VideoRenderingParams frame_gen_params_;
|
||||
|
||||
QMatrix4x4 frame_gen_mat_;
|
||||
|
||||
VideoRenderingParams video_params_;
|
||||
|
||||
VideoRenderFrameCache* frame_cache_;
|
||||
|
||||
ColorProcessorCache color_cache_;
|
||||
|
||||
QByteArray download_buffer_;
|
||||
|
||||
OperatingMode operating_mode_;
|
||||
|
||||
struct CachedStill {
|
||||
NodeValue texture;
|
||||
QString colorspace;
|
||||
bool alpha_is_associated;
|
||||
int divider;
|
||||
rational time;
|
||||
};
|
||||
|
||||
RenderCache<Stream*, CachedStill> still_image_cache_;
|
||||
|
||||
private slots:
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // VIDEORENDERWORKER_H
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
***/
|
||||
|
||||
#include "videorenderframecache.h"
|
||||
#include "framehashcache.h"
|
||||
|
||||
#include <OpenEXR/ImfFloatAttribute.h>
|
||||
#include <OpenEXR/ImfInputFile.h>
|
||||
@@ -33,80 +33,24 @@
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
void VideoRenderFrameCache::Clear()
|
||||
{
|
||||
time_hash_map_.clear();
|
||||
|
||||
{
|
||||
QMutexLocker locker(¤tly_caching_lock_);
|
||||
currently_caching_list_.clear();
|
||||
}
|
||||
|
||||
cache_id_.clear();
|
||||
}
|
||||
|
||||
bool VideoRenderFrameCache::HasHash(const QByteArray &hash, const PixelFormat::Format& format)
|
||||
{
|
||||
return QFileInfo::exists(CachePathName(hash, format)) && !IsCaching(hash);
|
||||
}
|
||||
|
||||
bool VideoRenderFrameCache::IsCaching(const QByteArray &hash)
|
||||
{
|
||||
QMutexLocker locker(¤tly_caching_lock_);
|
||||
|
||||
return currently_caching_list_.contains(hash);
|
||||
}
|
||||
|
||||
bool VideoRenderFrameCache::TryCache(const QByteArray &hash)
|
||||
{
|
||||
QMutexLocker locker(¤tly_caching_lock_);
|
||||
|
||||
if (!currently_caching_list_.contains(hash)) {
|
||||
currently_caching_list_.append(hash);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void VideoRenderFrameCache::SetCacheID(const QString &id)
|
||||
{
|
||||
Clear();
|
||||
|
||||
cache_id_ = id;
|
||||
}
|
||||
|
||||
QByteArray VideoRenderFrameCache::TimeToHash(const rational &time) const
|
||||
QByteArray FrameHashCache::GetHash(const rational &time) const
|
||||
{
|
||||
return time_hash_map_.value(time);
|
||||
}
|
||||
|
||||
void VideoRenderFrameCache::SetHash(const rational &time, const QByteArray &hash)
|
||||
void FrameHashCache::SetHash(const rational &time, const QByteArray &hash)
|
||||
{
|
||||
time_hash_map_.insert(time, hash);
|
||||
|
||||
Validate(TimeRange(time, time + timebase_));
|
||||
}
|
||||
|
||||
void VideoRenderFrameCache::Truncate(const rational &time)
|
||||
void FrameHashCache::SetTimebase(const rational &tb)
|
||||
{
|
||||
QMap<rational, QByteArray>::iterator i = time_hash_map_.begin();
|
||||
|
||||
while (i != time_hash_map_.end()) {
|
||||
if (i.key() >= time) {
|
||||
i = time_hash_map_.erase(i);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
timebase_ = tb;
|
||||
}
|
||||
|
||||
void VideoRenderFrameCache::RemoveHashFromCurrentlyCaching(const QByteArray &hash)
|
||||
{
|
||||
QMutexLocker locker(¤tly_caching_lock_);
|
||||
|
||||
currently_caching_list_.removeOne(hash);
|
||||
}
|
||||
|
||||
QList<rational> VideoRenderFrameCache::FramesWithHash(const QByteArray &hash) const
|
||||
QList<rational> FrameHashCache::GetFramesWithHash(const QByteArray &hash) const
|
||||
{
|
||||
QList<rational> times;
|
||||
|
||||
@@ -121,7 +65,7 @@ QList<rational> VideoRenderFrameCache::FramesWithHash(const QByteArray &hash) co
|
||||
return times;
|
||||
}
|
||||
|
||||
QList<rational> VideoRenderFrameCache::TakeFramesWithHash(const QByteArray &hash)
|
||||
QList<rational> FrameHashCache::TakeFramesWithHash(const QByteArray &hash)
|
||||
{
|
||||
QList<rational> times;
|
||||
|
||||
@@ -140,12 +84,12 @@ QList<rational> VideoRenderFrameCache::TakeFramesWithHash(const QByteArray &hash
|
||||
return times;
|
||||
}
|
||||
|
||||
const QMap<rational, QByteArray> &VideoRenderFrameCache::time_hash_map() const
|
||||
const QMap<rational, QByteArray> &FrameHashCache::time_hash_map() const
|
||||
{
|
||||
return time_hash_map_;
|
||||
}
|
||||
|
||||
QString VideoRenderFrameCache::GetFormatExtension(const PixelFormat::Format &f)
|
||||
QString FrameHashCache::GetFormatExtension(const PixelFormat::Format &f)
|
||||
{
|
||||
if (PixelFormat::FormatIsFloat(f)) {
|
||||
// EXR is only fast with float buffers so we only use it for those
|
||||
@@ -160,9 +104,9 @@ QString VideoRenderFrameCache::GetFormatExtension(const PixelFormat::Format &f)
|
||||
}
|
||||
}
|
||||
|
||||
void VideoRenderFrameCache::SaveCacheFrame(const QByteArray& hash,
|
||||
char* data,
|
||||
const VideoRenderingParams& vparam) const
|
||||
void FrameHashCache::SaveCacheFrame(const QByteArray& hash,
|
||||
char* data,
|
||||
const VideoRenderingParams& vparam)
|
||||
{
|
||||
QString fn = CachePathName(hash, vparam.format());
|
||||
|
||||
@@ -172,7 +116,35 @@ void VideoRenderFrameCache::SaveCacheFrame(const QByteArray& hash,
|
||||
}
|
||||
}
|
||||
|
||||
QString VideoRenderFrameCache::CachePathName(const QByteArray& hash, const PixelFormat::Format& pix_fmt) const
|
||||
void FrameHashCache::LengthChangedEvent(const rational &old, const rational &newlen)
|
||||
{
|
||||
if (newlen < old) {
|
||||
QMap<rational, QByteArray>::iterator i = time_hash_map_.begin();
|
||||
|
||||
while (i != time_hash_map_.end()) {
|
||||
if (i.key() >= newlen) {
|
||||
i = time_hash_map_.erase(i);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FrameHashCache::InvalidateEvent(const TimeRange &r)
|
||||
{
|
||||
QMap<rational, QByteArray>::iterator i = time_hash_map_.begin();
|
||||
|
||||
while (i != time_hash_map_.end()) {
|
||||
if (i.key() >= r.in() && i.key() < r.out()) {
|
||||
i = time_hash_map_.erase(i);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString FrameHashCache::CachePathName(const QByteArray& hash, const PixelFormat::Format& pix_fmt)
|
||||
{
|
||||
QString ext = GetFormatExtension(pix_fmt);
|
||||
|
||||
@@ -184,7 +156,7 @@ QString VideoRenderFrameCache::CachePathName(const QByteArray& hash, const Pixel
|
||||
return cache_dir.filePath(filename);
|
||||
}
|
||||
|
||||
bool VideoRenderFrameCache::SaveCacheFrame(const QString &filename, char *data, const VideoRenderingParams &vparam)
|
||||
bool FrameHashCache::SaveCacheFrame(const QString &filename, char *data, const VideoRenderingParams &vparam)
|
||||
{
|
||||
switch (vparam.format()) {
|
||||
case PixelFormat::PIX_FMT_RGB8:
|
||||
@@ -24,47 +24,29 @@
|
||||
#include <QMutex>
|
||||
|
||||
#include "common/rational.h"
|
||||
#include "common/timerange.h"
|
||||
#include "render/pixelformat.h"
|
||||
#include "render/playbackcache.h"
|
||||
#include "render/videoparams.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class VideoRenderFrameCache
|
||||
class FrameHashCache : public PlaybackCache
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
VideoRenderFrameCache() = default;
|
||||
FrameHashCache() = default;
|
||||
|
||||
void Clear();
|
||||
|
||||
/**
|
||||
* @brief Return whether a frame with this hash already exists
|
||||
*/
|
||||
bool HasHash(const QByteArray& hash, const PixelFormat::Format &format);
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
void SetCacheID(const QString& id);
|
||||
|
||||
QByteArray TimeToHash(const rational& time) const;
|
||||
QByteArray GetHash(const rational& time) const;
|
||||
|
||||
void SetHash(const rational& time, const QByteArray& hash);
|
||||
|
||||
void Truncate(const rational& time);
|
||||
|
||||
void RemoveHashFromCurrentlyCaching(const QByteArray& hash);
|
||||
void SetTimebase(const rational& tb);
|
||||
|
||||
/**
|
||||
* @brief Returns a list of frames that use a particular hash
|
||||
*/
|
||||
QList<rational> FramesWithHash(const QByteArray& hash) const;
|
||||
QList<rational> GetFramesWithHash(const QByteArray& hash) const;
|
||||
|
||||
/**
|
||||
* @brief Same as FramesWithHash() but also removes these frames from the map
|
||||
@@ -73,23 +55,26 @@ public:
|
||||
|
||||
const QMap<rational, QByteArray>& time_hash_map() const;
|
||||
|
||||
static QString GetFormatExtension(const PixelFormat::Format& f);
|
||||
|
||||
/**
|
||||
* @brief Return the path of the cached image at this time
|
||||
*/
|
||||
QString CachePathName(const QByteArray &hash, const PixelFormat::Format& pix_fmt) const;
|
||||
static QString CachePathName(const QByteArray &hash, const PixelFormat::Format& pix_fmt);
|
||||
|
||||
static bool SaveCacheFrame(const QString& filename, char *data, const VideoRenderingParams &vparam);
|
||||
void SaveCacheFrame(const QByteArray& hash, char *data, const VideoRenderingParams &vparam) const;
|
||||
static void SaveCacheFrame(const QByteArray& hash, char *data, const VideoRenderingParams &vparam);
|
||||
|
||||
static QString GetFormatExtension(const PixelFormat::Format& f);
|
||||
|
||||
protected:
|
||||
virtual void LengthChangedEvent(const rational& old, const rational& newlen) override;
|
||||
|
||||
virtual void InvalidateEvent(const TimeRange& range) override;
|
||||
|
||||
private:
|
||||
QMap<rational, QByteArray> time_hash_map_;
|
||||
|
||||
QMutex currently_caching_lock_;
|
||||
QVector<QByteArray> currently_caching_list_;
|
||||
rational timebase_;
|
||||
|
||||
QString cache_id_;
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -0,0 +1,84 @@
|
||||
/***
|
||||
|
||||
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 "playbackcache.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
PlaybackCache::PlaybackCache()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void PlaybackCache::Invalidate(const TimeRange &r)
|
||||
{
|
||||
invalidated_.InsertTimeRange(r);
|
||||
|
||||
InvalidateEvent(r);
|
||||
|
||||
emit Invalidated(r);
|
||||
}
|
||||
|
||||
void PlaybackCache::SetLength(const rational &r)
|
||||
{
|
||||
if (length_ == r) {
|
||||
// Same length - do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
if (r > length_) {
|
||||
// If new length is greater, simply extend the invalidated range for now
|
||||
invalidated_.InsertTimeRange(TimeRange(r, length_));
|
||||
} else {
|
||||
// If new length is smaller, removed hashes
|
||||
invalidated_.RemoveTimeRange(TimeRange(length_, r));
|
||||
}
|
||||
|
||||
LengthChangedEvent(length_, r);
|
||||
|
||||
length_ = r;
|
||||
}
|
||||
|
||||
bool PlaybackCache::IsFullyValidated() const
|
||||
{
|
||||
return invalidated_.isEmpty();
|
||||
}
|
||||
|
||||
void PlaybackCache::Validate(const TimeRange &r)
|
||||
{
|
||||
invalidated_.RemoveTimeRange(r);
|
||||
|
||||
emit Validated(r);
|
||||
}
|
||||
|
||||
void PlaybackCache::InvalidateAll()
|
||||
{
|
||||
Invalidate(TimeRange(0, length_));
|
||||
}
|
||||
|
||||
void PlaybackCache::LengthChangedEvent(const rational &, const rational &)
|
||||
{
|
||||
}
|
||||
|
||||
void PlaybackCache::InvalidateEvent(const TimeRange &)
|
||||
{
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -18,27 +18,55 @@
|
||||
|
||||
***/
|
||||
|
||||
#ifndef AUDIOWORKER_H
|
||||
#define AUDIOWORKER_H
|
||||
#ifndef PLAYBACKCACHE_H
|
||||
#define PLAYBACKCACHE_H
|
||||
|
||||
#include "../audiorenderworker.h"
|
||||
#include <QObject>
|
||||
|
||||
#include "common/timerange.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class AudioWorker : public AudioRenderWorker
|
||||
class PlaybackCache : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioWorker(QHash<Node*, Node*>* copy_map, QObject* parent = nullptr);
|
||||
PlaybackCache();
|
||||
|
||||
void Invalidate(const TimeRange& r);
|
||||
|
||||
void SetLength(const rational& r);
|
||||
|
||||
bool IsFullyValidated() const;
|
||||
|
||||
const TimeRangeList& GetInvalidatedRanges() const
|
||||
{
|
||||
return invalidated_;
|
||||
}
|
||||
|
||||
signals:
|
||||
void Invalidated(const TimeRange& r);
|
||||
|
||||
void Validated(const TimeRange& r);
|
||||
|
||||
void LengthChanged(const rational& r);
|
||||
|
||||
protected:
|
||||
virtual NodeValue FrameToValue(DecoderPtr decoder, StreamPtr stream, const TimeRange &range) override;
|
||||
void Validate(const TimeRange& r);
|
||||
|
||||
virtual void RunNodeAccelerated(const Node *node, const TimeRange& range, NodeValueDatabase& input_params, NodeValueTable& output_params) override;
|
||||
void InvalidateAll();
|
||||
|
||||
virtual void LengthChangedEvent(const rational& old, const rational& newlen);
|
||||
|
||||
virtual void InvalidateEvent(const TimeRange& range);
|
||||
|
||||
private:
|
||||
TimeRangeList invalidated_;
|
||||
|
||||
rational length_;
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // AUDIOWORKER_H
|
||||
#endif // PLAYBACKCACHE_H
|
||||
Reference in New Issue
Block a user