Merge branch 'nodeview-redux'
This commit is contained in:
@@ -46,6 +46,8 @@ set(OLIVE_SOURCES
|
||||
render/rendercache.h
|
||||
render/rendererthreadwrapper.cpp
|
||||
render/rendererthreadwrapper.h
|
||||
render/renderjobtracker.cpp
|
||||
render/renderjobtracker.h
|
||||
render/rendermanager.cpp
|
||||
render/rendermanager.h
|
||||
render/rendermodes.h
|
||||
|
||||
@@ -54,13 +54,6 @@ const QVector<uint64_t> AudioParams::kSupportedChannelLayouts = {
|
||||
|
||||
const AudioParams::Format AudioParams::kInternalFormat = AudioParams::kFormatFloat32;
|
||||
|
||||
qint64 AudioParams::time_to_bytes(const double &time) const
|
||||
{
|
||||
Q_ASSERT(is_valid());
|
||||
|
||||
return qint64(time_to_samples(time)) * channel_count() * bytes_per_sample_per_channel();
|
||||
}
|
||||
|
||||
bool AudioParams::operator==(const AudioParams &other) const
|
||||
{
|
||||
return (format() == other.format()
|
||||
@@ -94,11 +87,28 @@ QAudioFormat::SampleType AudioParams::GetQtSampleType(AudioParams::Format format
|
||||
return QAudioFormat::Unknown;
|
||||
}
|
||||
|
||||
qint64 AudioParams::time_to_bytes(const double &time) const
|
||||
{
|
||||
return time_to_bytes_per_channel(time) * channel_count();
|
||||
}
|
||||
|
||||
qint64 AudioParams::time_to_bytes(const rational &time) const
|
||||
{
|
||||
return time_to_bytes(time.toDouble());
|
||||
}
|
||||
|
||||
qint64 AudioParams::time_to_bytes_per_channel(const double &time) const
|
||||
{
|
||||
Q_ASSERT(is_valid());
|
||||
|
||||
return qint64(time_to_samples(time)) * bytes_per_sample_per_channel();
|
||||
}
|
||||
|
||||
qint64 AudioParams::time_to_bytes_per_channel(const rational &time) const
|
||||
{
|
||||
return time_to_bytes_per_channel(time.toDouble());
|
||||
}
|
||||
|
||||
qint64 AudioParams::time_to_samples(const double &time) const
|
||||
{
|
||||
Q_ASSERT(is_valid());
|
||||
@@ -139,6 +149,13 @@ rational AudioParams::bytes_to_time(const qint64 &bytes) const
|
||||
return samples_to_time(bytes_to_samples(bytes));
|
||||
}
|
||||
|
||||
rational AudioParams::bytes_per_channel_to_time(const qint64 &bytes) const
|
||||
{
|
||||
Q_ASSERT(is_valid());
|
||||
|
||||
return samples_to_time(bytes_to_samples(bytes * channel_count()));
|
||||
}
|
||||
|
||||
int AudioParams::channel_count() const
|
||||
{
|
||||
return channel_count_;
|
||||
|
||||
@@ -161,12 +161,15 @@ public:
|
||||
|
||||
qint64 time_to_bytes(const double& time) const;
|
||||
qint64 time_to_bytes(const rational& time) const;
|
||||
qint64 time_to_bytes_per_channel(const double& time) const;
|
||||
qint64 time_to_bytes_per_channel(const rational& time) const;
|
||||
qint64 time_to_samples(const double& time) const;
|
||||
qint64 time_to_samples(const rational& time) const;
|
||||
qint64 samples_to_bytes(const qint64& samples) const;
|
||||
rational samples_to_time(const qint64& samples) const;
|
||||
qint64 bytes_to_samples(const qint64 &bytes) const;
|
||||
rational bytes_to_time(const qint64 &bytes) const;
|
||||
rational bytes_per_channel_to_time(const qint64 &bytes) const;
|
||||
int channel_count() const;
|
||||
int bytes_per_sample_per_channel() const;
|
||||
int bits_per_sample() const;
|
||||
|
||||
+158
-146
@@ -22,13 +22,14 @@
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QRandomGenerator>
|
||||
#include <QUuid>
|
||||
|
||||
#include "common/filefunctions.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
const qint64 AudioPlaybackCache::kDefaultSegmentSize = 5242880;
|
||||
const qint64 AudioPlaybackCache::kDefaultSegmentSizePerChannel = 10 * 1024 * 1024;
|
||||
|
||||
AudioPlaybackCache::AudioPlaybackCache(QObject* parent) :
|
||||
PlaybackCache(parent)
|
||||
@@ -56,79 +57,81 @@ void AudioPlaybackCache::SetParameters(const AudioParams ¶ms)
|
||||
emit ParametersChanged();
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr samples, const AudioVisualWaveform *waveform, const qint64 &job_time)
|
||||
void AudioPlaybackCache::WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, SampleBufferPtr samples, const AudioVisualWaveform *waveform)
|
||||
{
|
||||
QList<TimeRange> valid_ranges = GetValidRanges(range, job_time);
|
||||
if (valid_ranges.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure if we have enough segments to write this data, creating more if not
|
||||
qint64 length_diff = params_.time_to_bytes(range.out()) - playlist_.GetLength();
|
||||
qint64 length_diff = params_.time_to_bytes_per_channel(range.out()) - playlist_.GetLength();
|
||||
while (length_diff > 0) {
|
||||
qint64 seg_sz = qMin(kDefaultSegmentSize, length_diff);
|
||||
qint64 seg_sz = qMin(kDefaultSegmentSizePerChannel, length_diff);
|
||||
playlist_.push_back(CreateSegment(seg_sz, playlist_.GetLength()));
|
||||
length_diff -= seg_sz;
|
||||
}
|
||||
|
||||
// Convert to packed data, which is what we store on disk so it can be played back easily
|
||||
QByteArray a;
|
||||
if (samples) {
|
||||
a = samples->toPackedData();
|
||||
}
|
||||
|
||||
// Keep track of validated ranges so we can signal them all at once at the end
|
||||
TimeRangeList ranges_we_validated;
|
||||
|
||||
// Calculate buffer size per channel
|
||||
qint64 buffer_size_per_channel = samples->sample_count() * params_.bytes_per_sample_per_channel();
|
||||
|
||||
// Write each valid range to the segments
|
||||
foreach (const TimeRange& r, valid_ranges) {
|
||||
rational this_segment_in = 0;
|
||||
|
||||
// Write PCM to playlist
|
||||
for (auto it=playlist_.begin(); it!=playlist_.end(); it++) {
|
||||
rational this_segment_out = this_segment_in + params_.bytes_to_time((*it).size());
|
||||
rational this_segment_out = this_segment_in + params_.bytes_per_channel_to_time((*it).size());
|
||||
|
||||
if (r.in() < this_segment_out) {
|
||||
// We'll write at least something to this segment
|
||||
QFile seg_file((*it).filename());
|
||||
bool succeeded = true;
|
||||
|
||||
if (seg_file.open(QFile::ReadWrite)) {
|
||||
// Calculate how much to write
|
||||
rational this_write_in_point = qMax(r.in(), this_segment_in);
|
||||
rational this_write_out_point = qMin(r.out(), this_segment_out);
|
||||
// Calculate how much to write
|
||||
rational this_write_in_point = qMax(r.in(), this_segment_in);
|
||||
rational this_write_out_point = qMin(r.out(), this_segment_out);
|
||||
|
||||
// Calculate what the byte offsets are going to be in this segment file
|
||||
rational in_point_relative = this_write_in_point - this_segment_in;
|
||||
qint64 dst_offset = params_.time_to_bytes(in_point_relative);
|
||||
for (int i=0; i<(*it).channels(); i++) {
|
||||
QFile seg_file((*it).filename(i));
|
||||
|
||||
// Calculate where to retrieve data from in the source buffer
|
||||
qint64 src_offset = params_.time_to_bytes(this_write_in_point - range.in());
|
||||
if (seg_file.open(QFile::ReadWrite)) {
|
||||
// Calculate what the byte offsets are going to be in this segment file
|
||||
rational in_point_relative = this_write_in_point - this_segment_in;
|
||||
qint64 dst_offset = params_.time_to_bytes_per_channel(in_point_relative);
|
||||
|
||||
// Determine how many bytes need to be written
|
||||
qint64 total_write_length = params_.time_to_bytes(this_write_out_point - this_write_in_point);
|
||||
// Calculate where to retrieve data from in the source buffer
|
||||
qint64 src_offset = params_.time_to_bytes_per_channel(this_write_in_point - range.in());
|
||||
|
||||
// Determine how many bytes we actually have in the source buffer
|
||||
qint64 possible_write_length = qMin(qMax(qint64(0), a.size() - src_offset), total_write_length);
|
||||
// Determine how many bytes need to be written
|
||||
qint64 total_write_length = params_.time_to_bytes_per_channel(this_write_out_point - this_write_in_point);
|
||||
|
||||
// Seek to our start offset
|
||||
seg_file.seek(dst_offset);
|
||||
// Retrieve data buffer
|
||||
const char *a = reinterpret_cast<const char*>(samples->data(i));
|
||||
|
||||
// If we have source bytes to write, write them here
|
||||
if (possible_write_length > 0) {
|
||||
seg_file.write(a.data() + src_offset, possible_write_length);
|
||||
// Determine how many bytes we actually have in the source buffer
|
||||
qint64 possible_write_length = qMin(qMax(qint64(0), buffer_size_per_channel - src_offset), total_write_length);
|
||||
|
||||
// Seek to our start offset
|
||||
seg_file.seek(dst_offset);
|
||||
|
||||
// If we have source bytes to write, write them here
|
||||
if (possible_write_length > 0) {
|
||||
seg_file.write(a + src_offset, possible_write_length);
|
||||
}
|
||||
|
||||
if (possible_write_length < total_write_length) {
|
||||
// Fill remaining space with silence
|
||||
QByteArray s(total_write_length - possible_write_length, 0x00);
|
||||
seg_file.write(s);
|
||||
}
|
||||
|
||||
seg_file.close();
|
||||
} else {
|
||||
qWarning() << "Failed to write PCM data to" << seg_file.fileName();
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (possible_write_length < total_write_length) {
|
||||
// Fill remaining space with silence
|
||||
QByteArray s(total_write_length - possible_write_length, 0x00);
|
||||
seg_file.write(s);
|
||||
}
|
||||
|
||||
seg_file.close();
|
||||
|
||||
if (succeeded) {
|
||||
ranges_we_validated.insert(TimeRange(this_write_in_point, this_write_out_point));
|
||||
} else {
|
||||
qWarning() << "Failed to write PCM data to" << seg_file.fileName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,23 +157,22 @@ void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr sample
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::WriteSilence(const TimeRange &range, qint64 job_time)
|
||||
void AudioPlaybackCache::WriteSilence(const TimeRange &range)
|
||||
{
|
||||
// WritePCM will automatically fill non-existent bytes with silence, so we just have to send
|
||||
// it an empty sample buffer
|
||||
WritePCM(range, nullptr, nullptr, job_time);
|
||||
WritePCM(range, {range}, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational &to_in_time)
|
||||
{
|
||||
if (from_in_time == to_in_time || from_in_time >= GetLength()) {
|
||||
// Nothing to be done
|
||||
qint64 to = params_.time_to_bytes_per_channel(to_in_time);
|
||||
qint64 from = params_.time_to_bytes_per_channel(from_in_time);
|
||||
|
||||
if (from >= playlist_.GetLength()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 to = params_.time_to_bytes(to_in_time);
|
||||
qint64 from = params_.time_to_bytes(from_in_time);
|
||||
|
||||
int to_seg_index = playlist_.GetIndexOfPosition(to);
|
||||
int from_seg_index = playlist_.GetIndexOfPosition(from);
|
||||
|
||||
@@ -202,7 +204,7 @@ void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational
|
||||
qint64 time_to_insert = to - from;
|
||||
|
||||
while (time_to_insert) {
|
||||
qint64 new_seg_sz = qMin(kDefaultSegmentSize, time_to_insert);
|
||||
qint64 new_seg_sz = qMin(kDefaultSegmentSizePerChannel, time_to_insert);
|
||||
|
||||
// Set offset to 0 for now and fill it in later
|
||||
playlist_.insert(insert_index, CreateSegment(new_seg_sz, 0));
|
||||
@@ -262,56 +264,45 @@ void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::LengthChangedEvent(const rational& old, const rational& newlen)
|
||||
{
|
||||
Q_UNUSED(old)
|
||||
|
||||
if (!params_.is_valid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 new_len_in_bytes = params_.time_to_bytes(newlen);
|
||||
|
||||
while (new_len_in_bytes < playlist_.GetLength()) {
|
||||
Segment& last_seg = playlist_.back();
|
||||
|
||||
if (playlist_.GetLength() - last_seg.size() < new_len_in_bytes) {
|
||||
// Truncate this segment rather than removing it
|
||||
qint64 diff = playlist_.GetLength() - new_len_in_bytes;
|
||||
|
||||
TrimSegmentOut(&last_seg, last_seg.size() - diff);
|
||||
} else {
|
||||
// Remove last segment
|
||||
RemoveSegmentFromArray(playlist_.size() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AudioPlaybackCache::Segment AudioPlaybackCache::CloneSegment(const AudioPlaybackCache::Segment &s) const
|
||||
{
|
||||
Segment new_seg = s;
|
||||
|
||||
// Copy data to a new file
|
||||
QString new_filename = GenerateSegmentFilename();
|
||||
QFile::copy(s.filename(), new_filename);
|
||||
new_seg.set_channels(s.channels());
|
||||
|
||||
new_seg.set_filename(new_filename);
|
||||
// Copy data to a new file
|
||||
for (int i=0; i<s.channels(); i++) {
|
||||
QString new_filename = GenerateSegmentFilename();
|
||||
QFile::copy(s.filename(i), new_filename);
|
||||
|
||||
new_seg.set_filename(i, new_filename);
|
||||
}
|
||||
|
||||
return new_seg;
|
||||
}
|
||||
|
||||
AudioPlaybackCache::Segment AudioPlaybackCache::CreateSegment(const qint64 &size, const qint64& offset) const
|
||||
{
|
||||
Segment s(size, GenerateSegmentFilename());
|
||||
Segment s(size);
|
||||
|
||||
s.set_channels(params_.channel_count());
|
||||
|
||||
for (int i=0; i<params_.channel_count(); i++) {
|
||||
// Generate random unused filename for this segment
|
||||
QString fn = GenerateSegmentFilename();
|
||||
|
||||
// Set it for this segment/channel
|
||||
s.set_filename(i, fn);
|
||||
|
||||
// Create empty file
|
||||
QFile f(fn);
|
||||
if (f.open(QFile::WriteOnly)) {
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
s.set_offset(offset);
|
||||
|
||||
// Create empty file
|
||||
QFile f(s.filename());
|
||||
if (f.open(QFile::WriteOnly)) {
|
||||
f.close();
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -320,7 +311,7 @@ QString AudioPlaybackCache::GenerateSegmentFilename() const
|
||||
QString new_seg_filename;
|
||||
|
||||
do {
|
||||
uint32_t r = std::rand();
|
||||
uint32_t r = QRandomGenerator::global()->generate();
|
||||
new_seg_filename = QDir(GetCacheDirectory()).filePath(QStringLiteral("%1.pcm").arg(r));
|
||||
} while (QFileInfo::exists(new_seg_filename));
|
||||
|
||||
@@ -330,21 +321,23 @@ QString AudioPlaybackCache::GenerateSegmentFilename() const
|
||||
void AudioPlaybackCache::TrimSegmentIn(AudioPlaybackCache::Segment *s, qint64 new_length)
|
||||
{
|
||||
// Read filename
|
||||
QFile f(s->filename());
|
||||
if (f.open(QFile::ReadWrite)) {
|
||||
// Read segment into memory, according to the size we acknowledge
|
||||
QByteArray data = f.read(s->size());
|
||||
for (int i=0; i<s->channels(); i++) {
|
||||
QFile f(s->filename(i));
|
||||
if (f.open(QFile::ReadWrite)) {
|
||||
// Read segment into memory, according to the size we acknowledge
|
||||
QByteArray data = f.read(s->size());
|
||||
|
||||
// Trim to new length
|
||||
data = data.right(new_length);
|
||||
// Trim to new length
|
||||
data = data.right(new_length);
|
||||
|
||||
// Seek to start and write
|
||||
f.seek(0);
|
||||
// Seek to start and write
|
||||
f.seek(0);
|
||||
|
||||
// Write trimmed data
|
||||
f.write(data);
|
||||
// Write trimmed data
|
||||
f.write(data);
|
||||
|
||||
f.close();
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
s->set_size(new_length);
|
||||
@@ -358,14 +351,19 @@ void AudioPlaybackCache::TrimSegmentOut(AudioPlaybackCache::Segment *s, qint64 n
|
||||
|
||||
void AudioPlaybackCache::RemoveSegmentFromArray(int index)
|
||||
{
|
||||
QFile::remove(playlist_.at(index).filename());
|
||||
const Segment &s = playlist_.at(index);
|
||||
for (int i=0; i<s.channels(); i++) {
|
||||
QFile::remove(s.filename(i));
|
||||
}
|
||||
playlist_.removeAt(index);
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::ClearPlaylist()
|
||||
{
|
||||
foreach (const Segment& s, playlist_) {
|
||||
QFile::remove(s.filename());
|
||||
for (int i=0; i<s.channels(); i++) {
|
||||
QFile::remove(s.filename(i));
|
||||
}
|
||||
}
|
||||
playlist_.clear();
|
||||
}
|
||||
@@ -391,37 +389,22 @@ void AudioPlaybackCache::UpdateOffsetsFrom(int index)
|
||||
}
|
||||
}
|
||||
|
||||
QList<TimeRange> AudioPlaybackCache::GetValidRanges(const TimeRange& range, const qint64& job_time)
|
||||
{
|
||||
QList<TimeRange> valid_ranges;
|
||||
|
||||
for (int i=jobs_.size()-1;i>=0;i--) {
|
||||
const JobIdentifier& job = jobs_.at(i);
|
||||
|
||||
if (job_time >= job.job_time && job.range.OverlapsWith(range)) {
|
||||
valid_ranges.append(job.range.Intersected(range));
|
||||
}
|
||||
}
|
||||
|
||||
return valid_ranges;
|
||||
}
|
||||
|
||||
AudioPlaybackCache::PlaybackDevice *AudioPlaybackCache::CreatePlaybackDevice(QObject* parent) const
|
||||
{
|
||||
return new PlaybackDevice(playlist_, parent);
|
||||
return new PlaybackDevice(playlist_, params_.bytes_per_sample_per_channel(), parent);
|
||||
}
|
||||
|
||||
AudioPlaybackCache::Segment::Segment(qint64 size, const QString &filename)
|
||||
AudioPlaybackCache::Segment::Segment(qint64 size)
|
||||
{
|
||||
size_ = size;
|
||||
filename_ = filename;
|
||||
}
|
||||
|
||||
AudioPlaybackCache::PlaybackDevice::PlaybackDevice(const AudioPlaybackCache::Playlist &playlist, QObject *parent) :
|
||||
AudioPlaybackCache::PlaybackDevice::PlaybackDevice(const AudioPlaybackCache::Playlist &playlist, int sample_sz, QObject *parent) :
|
||||
QIODevice(parent),
|
||||
playlist_(playlist),
|
||||
current_segment_(0),
|
||||
segment_read_index_(0)
|
||||
segment_read_index_(0),
|
||||
sample_size_(sample_sz)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -458,37 +441,66 @@ qint64 AudioPlaybackCache::PlaybackDevice::readData(char *data, qint64 maxSize)
|
||||
&& current_segment_ < playlist_.size()) {
|
||||
const Segment& cs = playlist_.at(current_segment_);
|
||||
qint64 current_segment_sz = cs.size();
|
||||
QFile segment_file(cs.filename());
|
||||
|
||||
if (segment_file.open(QFile::ReadOnly)) {
|
||||
// Seek to our stored index of this segment
|
||||
segment_file.seek(segment_read_index_);
|
||||
QVector<QFile*> segment_files(cs.channels());
|
||||
segment_files.fill(nullptr);
|
||||
|
||||
// Determine how many bytes to read
|
||||
qint64 this_read_length = qMin(current_segment_sz - segment_read_index_,
|
||||
maxSize - read_size);
|
||||
bool all_files_opened = true;
|
||||
|
||||
// Read those bytes
|
||||
segment_file.read(data + read_size, this_read_length);
|
||||
// Open all file handles
|
||||
for (int i=0; i<cs.channels(); i++) {
|
||||
QFile *f = new QFile(cs.filename(i));
|
||||
segment_files[i] = f;
|
||||
|
||||
// Close the file
|
||||
segment_file.close();
|
||||
if (f->open(QFile::ReadOnly)) {
|
||||
// Seek to our stored index of this segment
|
||||
f->seek(segment_read_index_);
|
||||
} else {
|
||||
all_files_opened = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add to the read index
|
||||
segment_read_index_ += this_read_length;
|
||||
// If all file handles opened successfully, time to interleave and send them out
|
||||
if (all_files_opened) {
|
||||
// Determine how many bytes to read
|
||||
qint64 this_read_length = qMin((current_segment_sz - segment_read_index_) * cs.channels(), maxSize - read_size);
|
||||
|
||||
// Add to the read size
|
||||
read_size += this_read_length;
|
||||
qint64 target = read_size + this_read_length;
|
||||
|
||||
// If we've reached the end of this segment, tick the counter over to the next segment
|
||||
if (segment_read_index_ == current_segment_sz) {
|
||||
// Jump to the next file
|
||||
segment_read_index_ = 0;
|
||||
current_segment_++;
|
||||
while (read_size < target) {
|
||||
for (int i=0; i<cs.channels(); i++) {
|
||||
QFile *segment_file = segment_files.at(i);
|
||||
|
||||
// Read those bytes
|
||||
segment_file->read(data + read_size, sample_size_);
|
||||
|
||||
// Add to the read size
|
||||
read_size += sample_size_;
|
||||
}
|
||||
|
||||
// Add to the read index
|
||||
segment_read_index_ += sample_size_;
|
||||
|
||||
// If we've reached the end of this segment, tick the counter over to the next segment
|
||||
if (segment_read_index_ == current_segment_sz) {
|
||||
// Jump to the next file
|
||||
segment_read_index_ = 0;
|
||||
current_segment_++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close and delete file handles
|
||||
for (int i=0; i<cs.channels(); i++) {
|
||||
QFile *f = segment_files.at(i);
|
||||
|
||||
if (f) {
|
||||
if (f->isOpen()) {
|
||||
f->close();
|
||||
}
|
||||
delete f;
|
||||
}
|
||||
} else {
|
||||
qWarning() << "Failed to read data from segment";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,17 +66,14 @@ public:
|
||||
|
||||
void SetParameters(const AudioParams& params);
|
||||
|
||||
void WritePCM(const TimeRange &range, SampleBufferPtr samples, const AudioVisualWaveform *waveform, const qint64& job_time);
|
||||
void WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, SampleBufferPtr samples, const AudioVisualWaveform *waveform);
|
||||
|
||||
void WriteSilence(const TimeRange &range, qint64 job_time);
|
||||
|
||||
QList<TimeRange> GetValidRanges(const TimeRange &range, const qint64 &job_time);
|
||||
void WriteSilence(const TimeRange &range);
|
||||
|
||||
class Segment
|
||||
{
|
||||
public:
|
||||
Segment() = default;
|
||||
Segment(qint64 size, const QString& filename);
|
||||
Segment(qint64 size = 0);
|
||||
|
||||
qint64 size() const
|
||||
{
|
||||
@@ -98,14 +95,24 @@ public:
|
||||
offset_ = o;
|
||||
}
|
||||
|
||||
const QString& filename() const
|
||||
int channels() const
|
||||
{
|
||||
return filename_;
|
||||
return filenames_.size();
|
||||
}
|
||||
|
||||
void set_filename(const QString& filename)
|
||||
void set_channels(int index)
|
||||
{
|
||||
filename_ = filename;
|
||||
filenames_.resize(index);
|
||||
}
|
||||
|
||||
const QString& filename(int index) const
|
||||
{
|
||||
return filenames_.at(index);
|
||||
}
|
||||
|
||||
void set_filename(int index, const QString& filename)
|
||||
{
|
||||
filenames_[index] = filename;
|
||||
}
|
||||
|
||||
qint64 end() const
|
||||
@@ -114,7 +121,7 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
QString filename_;
|
||||
QVector<QString> filenames_;
|
||||
|
||||
qint64 size_;
|
||||
|
||||
@@ -136,7 +143,7 @@ public:
|
||||
class PlaybackDevice : public QIODevice
|
||||
{
|
||||
public:
|
||||
PlaybackDevice(const Playlist& playlist, QObject* parent = nullptr);
|
||||
PlaybackDevice(const Playlist& playlist, int sample_sz, QObject* parent = nullptr);
|
||||
|
||||
virtual ~PlaybackDevice() override;
|
||||
|
||||
@@ -169,6 +176,8 @@ public:
|
||||
|
||||
qint64 segment_read_index_;
|
||||
|
||||
int sample_size_;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -193,10 +202,8 @@ signals:
|
||||
protected:
|
||||
virtual void ShiftEvent(const rational& from, const rational& to) override;
|
||||
|
||||
virtual void LengthChangedEvent(const rational& old, const rational& newlen) override;
|
||||
|
||||
private:
|
||||
static const qint64 kDefaultSegmentSize;
|
||||
static const qint64 kDefaultSegmentSizePerChannel;
|
||||
|
||||
Segment CloneSegment(const Segment& s) const;
|
||||
|
||||
|
||||
+56
-142
@@ -32,7 +32,6 @@
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "common/filefunctions.h"
|
||||
#include "common/timecodefunctions.h"
|
||||
#include "render/diskmanager.h"
|
||||
|
||||
namespace olive {
|
||||
@@ -52,24 +51,32 @@ FrameHashCache::FrameHashCache(QObject *parent) :
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray FrameHashCache::GetHash(const rational &time)
|
||||
QByteArray FrameHashCache::GetHash(const int64_t &time)
|
||||
{
|
||||
return time_hash_map_.value(time);
|
||||
if (time < GetMapSize()) {
|
||||
return time_hash_map_.at(time);
|
||||
} else {
|
||||
return QByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, const qint64& job_time, bool frame_exists)
|
||||
QByteArray FrameHashCache::GetHash(const rational &time)
|
||||
{
|
||||
for (int i=jobs_.size()-1; i>=0; i--) {
|
||||
const JobIdentifier& job = jobs_.at(i);
|
||||
return GetHash(ToTimestamp(time));
|
||||
}
|
||||
|
||||
if (job.range.Contains(time)
|
||||
&& job_time < job.job_time) {
|
||||
// Hash here has changed since this frame started rendering, discard it
|
||||
return;
|
||||
}
|
||||
void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, bool frame_exists)
|
||||
{
|
||||
int64_t ts = ToTimestamp(time);
|
||||
if (ts >= GetMapSize()) {
|
||||
// Disabled: bizarrely causes the whole app to hang indefinitely when used
|
||||
// Reserve an extra minute to cut down on the amount of reallocations to make
|
||||
//time_hash_map_.reserve(ts + timebase_.flipped().toDouble() * 60);
|
||||
|
||||
// Add enough entries to insert this hash
|
||||
time_hash_map_.resize(ts + 1);
|
||||
}
|
||||
|
||||
time_hash_map_.insert(time, hash);
|
||||
time_hash_map_[ts] = hash;
|
||||
|
||||
TimeRange validated_range;
|
||||
if (frame_exists) {
|
||||
@@ -85,11 +92,11 @@ void FrameHashCache::SetTimebase(const rational &tb)
|
||||
|
||||
void FrameHashCache::ValidateFramesWithHash(const QByteArray &hash)
|
||||
{
|
||||
const TimeRangeList& invalidated_ranges = GetInvalidatedRanges();
|
||||
auto invalidated_ranges = GetInvalidatedRanges(ToTime(GetMapSize()));
|
||||
|
||||
for (auto iterator=time_hash_map_.begin();iterator!=time_hash_map_.end();iterator++) {
|
||||
if (iterator.value() == hash) {
|
||||
TimeRange frame_range(iterator.key(), iterator.key() + timebase_);
|
||||
for (int64_t i=0; i<GetMapSize(); i++) {
|
||||
if (time_hash_map_[i] == hash) {
|
||||
TimeRange frame_range(ToTime(i), ToTime(i+1));
|
||||
|
||||
if (invalidated_ranges.contains(frame_range)) {
|
||||
Validate(frame_range);
|
||||
@@ -98,85 +105,6 @@ void FrameHashCache::ValidateFramesWithHash(const QByteArray &hash)
|
||||
}
|
||||
}
|
||||
|
||||
QList<rational> FrameHashCache::GetFramesWithHash(const QByteArray &hash)
|
||||
{
|
||||
QList<rational> times;
|
||||
|
||||
for (auto iterator=time_hash_map_.begin();iterator!=time_hash_map_.end();iterator++) {
|
||||
if (iterator.value() == hash) {
|
||||
times.append(iterator.key());
|
||||
}
|
||||
}
|
||||
|
||||
return times;
|
||||
}
|
||||
|
||||
QList<rational> FrameHashCache::TakeFramesWithHash(const QByteArray &hash)
|
||||
{
|
||||
TimeRangeList range_to_invalidate;
|
||||
QList<rational> times;
|
||||
|
||||
auto iterator = time_hash_map_.begin();
|
||||
|
||||
while (iterator != time_hash_map_.end()) {
|
||||
if (iterator.value() == hash) {
|
||||
times.append(iterator.key());
|
||||
range_to_invalidate.insert(TimeRange(iterator.key(), iterator.key() + timebase_));
|
||||
|
||||
iterator = time_hash_map_.erase(iterator);
|
||||
} else {
|
||||
iterator++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (const TimeRange& r, range_to_invalidate) {
|
||||
// We apply a 0 job time because the graph hasn't changed to get here, so any renderer should
|
||||
// be up to date already
|
||||
Invalidate(r, 0);
|
||||
}
|
||||
|
||||
return times;
|
||||
}
|
||||
|
||||
QMap<rational, QByteArray> FrameHashCache::time_hash_map()
|
||||
{
|
||||
return time_hash_map_;
|
||||
}
|
||||
|
||||
QVector<rational> FrameHashCache::GetFrameListFromTimeRange(TimeRangeList range_list, const rational &timebase)
|
||||
{
|
||||
// If timebase is null, this will be an infinite loop
|
||||
Q_ASSERT(!timebase.isNull());
|
||||
|
||||
QVector<rational> times;
|
||||
|
||||
foreach (const TimeRange &range, range_list) {
|
||||
rational frame = Timecode::snap_time_to_timebase(range.in(), timebase, true);
|
||||
|
||||
while (frame < range.out()) {
|
||||
times.append(frame);
|
||||
frame += timebase;
|
||||
}
|
||||
}
|
||||
|
||||
return times;
|
||||
}
|
||||
|
||||
QVector<rational> FrameHashCache::GetFrameListFromTimeRange(const TimeRangeList &range)
|
||||
{
|
||||
return GetFrameListFromTimeRange(range, timebase_);
|
||||
}
|
||||
|
||||
QVector<rational> FrameHashCache::GetInvalidatedFrames()
|
||||
{
|
||||
return GetFrameListFromTimeRange(GetInvalidatedRanges());
|
||||
}
|
||||
|
||||
QVector<rational> FrameHashCache::GetInvalidatedFrames(const TimeRange &intersecting)
|
||||
{
|
||||
return GetFrameListFromTimeRange(GetInvalidatedRanges().Intersects(intersecting));
|
||||
}
|
||||
|
||||
bool FrameHashCache::SaveCacheFrame(const QByteArray& hash,
|
||||
char* data,
|
||||
const VideoParams& vparam,
|
||||
@@ -313,21 +241,6 @@ FramePtr FrameHashCache::LoadCacheFrame(const QString &fn)
|
||||
return frame;
|
||||
}
|
||||
|
||||
void FrameHashCache::LengthChangedEvent(const rational &old, const rational &newlen)
|
||||
{
|
||||
if (newlen < old) {
|
||||
auto i = time_hash_map_.begin();
|
||||
|
||||
while (i != time_hash_map_.end()) {
|
||||
if (i.key() >= newlen) {
|
||||
i = time_hash_map_.erase(i);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HashTimePair {
|
||||
rational time;
|
||||
QByteArray hash;
|
||||
@@ -335,51 +248,52 @@ struct HashTimePair {
|
||||
|
||||
void FrameHashCache::ShiftEvent(const rational &from, const rational &to)
|
||||
{
|
||||
auto i = time_hash_map_.begin();
|
||||
|
||||
// POSITIVE if moving forward ->
|
||||
// NEGATIVE if moving backward <-
|
||||
rational diff = to - from;
|
||||
bool diff_is_negative = (diff < 0);
|
||||
|
||||
QList<HashTimePair> shifted_times;
|
||||
int64_t to_ts = ToTimestamp(to);
|
||||
int64_t from_ts = ToTimestamp(from);
|
||||
|
||||
while (i != time_hash_map_.end()) {
|
||||
if (diff_is_negative && i.key() >= to && i.key() < from) {
|
||||
|
||||
// This time will be removed in the shift so we just discard it
|
||||
i = time_hash_map_.erase(i);
|
||||
|
||||
} else if (i.key() >= from) {
|
||||
|
||||
// This time is after the from time and must be shifted
|
||||
shifted_times.append({i.key() + diff, i.value()});
|
||||
i = time_hash_map_.erase(i);
|
||||
|
||||
} else {
|
||||
|
||||
// Do nothing
|
||||
i++;
|
||||
|
||||
}
|
||||
if (from_ts >= GetMapSize()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (const HashTimePair& p, shifted_times) {
|
||||
time_hash_map_.insert(p.time, p.hash);
|
||||
if (diff_is_negative) {
|
||||
// We're moving the frames starting at `from` backwards to where `to` is
|
||||
if (to_ts < GetMapSize()) {
|
||||
time_hash_map_.erase(time_hash_map_.begin() + to_ts, time_hash_map_.begin() + from_ts);
|
||||
}
|
||||
} else {
|
||||
// We're moving the frames starting at `from` forwards to where `to` is
|
||||
if (from_ts < GetMapSize()) {
|
||||
time_hash_map_.insert(time_hash_map_.begin() + from_ts, to_ts - from_ts, QByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FrameHashCache::InvalidateEvent(const TimeRange &range)
|
||||
{
|
||||
if (!timebase_.isNull()) {
|
||||
QVector<rational> invalid_frames = GetFrameListFromTimeRange({range});
|
||||
|
||||
foreach (const rational& r, invalid_frames) {
|
||||
time_hash_map_.remove(r);
|
||||
int64_t start = ToTimestamp(range.in(), Timecode::kCeil);
|
||||
int64_t end = ToTimestamp(range.out(), Timecode::kCeil);
|
||||
for (int64_t i=start; i<GetMapSize() && i<end; i++) {
|
||||
time_hash_map_[i].clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rational FrameHashCache::ToTime(const int64_t &ts) const
|
||||
{
|
||||
return Timecode::timestamp_to_time(ts, timebase_);
|
||||
}
|
||||
|
||||
int64_t FrameHashCache::ToTimestamp(const rational &ts, Timecode::Rounding rounding) const
|
||||
{
|
||||
return Timecode::time_to_timestamp(ts, timebase_, rounding);
|
||||
}
|
||||
|
||||
void FrameHashCache::HashDeleted(const QString& s, const QByteArray &hash)
|
||||
{
|
||||
QString cache_dir = GetCacheDirectory();
|
||||
@@ -388,16 +302,16 @@ void FrameHashCache::HashDeleted(const QString& s, const QByteArray &hash)
|
||||
}
|
||||
|
||||
TimeRangeList ranges_to_invalidate;
|
||||
for (auto i=time_hash_map_.constBegin(); i!=time_hash_map_.constEnd(); i++) {
|
||||
if (i.value() == hash) {
|
||||
ranges_to_invalidate.insert(TimeRange(i.key(), i.key() + timebase_));
|
||||
for (int64_t i=0; i<GetMapSize(); i++) {
|
||||
if (time_hash_map_.at(i) == hash) {
|
||||
ranges_to_invalidate.insert(TimeRange(ToTime(i), ToTime(i+1)));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (const TimeRange& range, ranges_to_invalidate) {
|
||||
// We set job time to 0 because the nodes haven't changed and any render job should be up
|
||||
// to date
|
||||
Invalidate(range, 0);
|
||||
Invalidate(range);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-22
@@ -24,6 +24,7 @@
|
||||
#include <QMutex>
|
||||
|
||||
#include "common/rational.h"
|
||||
#include "common/timecodefunctions.h"
|
||||
#include "common/timerange.h"
|
||||
#include "codec/frame.h"
|
||||
#include "render/playbackcache.h"
|
||||
@@ -37,24 +38,18 @@ class FrameHashCache : public PlaybackCache
|
||||
public:
|
||||
FrameHashCache(QObject* parent = nullptr);
|
||||
|
||||
QByteArray GetHash(const int64_t& time);
|
||||
QByteArray GetHash(const rational& time);
|
||||
|
||||
const rational &GetTimebase() const
|
||||
{
|
||||
return timebase_;
|
||||
}
|
||||
|
||||
void SetTimebase(const rational& tb);
|
||||
|
||||
void ValidateFramesWithHash(const QByteArray& hash);
|
||||
|
||||
/**
|
||||
* @brief Returns a list of frames that use a particular hash
|
||||
*/
|
||||
QList<rational> GetFramesWithHash(const QByteArray& hash);
|
||||
|
||||
/**
|
||||
* @brief Same as FramesWithHash() but also removes these frames from the map
|
||||
*/
|
||||
QList<rational> TakeFramesWithHash(const QByteArray& hash);
|
||||
|
||||
QMap<rational, QByteArray> time_hash_map();
|
||||
|
||||
/**
|
||||
* @brief Return the path of the cached image at this time
|
||||
*/
|
||||
@@ -70,23 +65,23 @@ public:
|
||||
FramePtr LoadCacheFrame(const QByteArray& hash) const;
|
||||
static FramePtr LoadCacheFrame(const QString& fn);
|
||||
|
||||
static QVector<rational> GetFrameListFromTimeRange(TimeRangeList range_list, const rational& timebase);
|
||||
QVector<rational> GetFrameListFromTimeRange(const TimeRangeList &range);
|
||||
QVector<rational> GetInvalidatedFrames();
|
||||
QVector<rational> GetInvalidatedFrames(const TimeRange& intersecting);
|
||||
|
||||
public slots:
|
||||
void SetHash(const olive::rational& time, const QByteArray& hash, const qint64 &job_time, bool frame_exists);
|
||||
void SetHash(const olive::rational &time, const QByteArray& hash, bool frame_exists);
|
||||
|
||||
protected:
|
||||
virtual void LengthChangedEvent(const rational& old, const rational& newlen) override;
|
||||
|
||||
virtual void ShiftEvent(const rational& from, const rational& to) override;
|
||||
|
||||
virtual void InvalidateEvent(const TimeRange& range) override;
|
||||
|
||||
private:
|
||||
QMap<rational, QByteArray> time_hash_map_;
|
||||
rational ToTime(const int64_t &ts) const;
|
||||
int64_t ToTimestamp(const rational &ts, Timecode::Rounding rounding = Timecode::kRound) const;
|
||||
|
||||
int64_t GetMapSize() const
|
||||
{
|
||||
return int64_t(time_hash_map_.size());
|
||||
}
|
||||
|
||||
std::vector<QByteArray> time_hash_map_;
|
||||
|
||||
rational timebase_;
|
||||
|
||||
|
||||
@@ -27,64 +27,25 @@
|
||||
|
||||
namespace olive {
|
||||
|
||||
void PlaybackCache::Invalidate(const TimeRange &r, qint64 job_time)
|
||||
void PlaybackCache::Invalidate(const TimeRange &r, bool signal)
|
||||
{
|
||||
if (r.in() == r.out()) {
|
||||
qWarning() << "Tried to invalidate zero-length range";
|
||||
return;
|
||||
}
|
||||
|
||||
invalidated_.insert(r);
|
||||
|
||||
RemoveRangeFromJobs(r);
|
||||
jobs_.append({r, job_time});
|
||||
validated_.remove(r);
|
||||
|
||||
InvalidateEvent(r);
|
||||
|
||||
emit Invalidated(r);
|
||||
if (signal) {
|
||||
emit Invalidated(r);
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::InvalidateAll()
|
||||
{
|
||||
if (length_.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Invalidate(TimeRange(0, length_), 0);
|
||||
}
|
||||
|
||||
void PlaybackCache::SetLength(const rational &r)
|
||||
{
|
||||
if (length_ == r) {
|
||||
// Same length - do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
LengthChangedEvent(length_, r);
|
||||
|
||||
TimeRange range_diff(length_, r);
|
||||
|
||||
if (r.isNull()) {
|
||||
invalidated_.clear();
|
||||
jobs_.clear();
|
||||
} else if (r > length_) {
|
||||
// If new length is greater, simply extend the invalidated range for now
|
||||
invalidated_.insert(range_diff);
|
||||
jobs_.append({range_diff, 0});
|
||||
} else {
|
||||
// If new length is smaller, removed hashes
|
||||
invalidated_.remove(range_diff);
|
||||
RemoveRangeFromJobs(range_diff);
|
||||
}
|
||||
|
||||
rational old_length = length_;
|
||||
length_ = r;
|
||||
|
||||
if (r > old_length) {
|
||||
emit Invalidated(range_diff);
|
||||
} else {
|
||||
emit Validated(range_diff);
|
||||
}
|
||||
Invalidate(TimeRange(0, RATIONAL_MAX));
|
||||
}
|
||||
|
||||
void PlaybackCache::Shift(rational from, rational to)
|
||||
@@ -93,57 +54,33 @@ void PlaybackCache::Shift(rational from, rational to)
|
||||
return;
|
||||
}
|
||||
|
||||
if (from > length_) {
|
||||
if (to > from) {
|
||||
// No-op
|
||||
return;
|
||||
} else if (to >= length_) {
|
||||
// No-op
|
||||
return;
|
||||
} else {
|
||||
from = length_;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "FIXME: 0 job time may cause cache desyncs";
|
||||
|
||||
// An region between `from` and `to` will be inserted or spliced out
|
||||
TimeRangeList ranges_to_shift = invalidated_.Intersects(TimeRange(from, RATIONAL_MAX));
|
||||
TimeRangeList ranges_to_shift = validated_.Intersects(TimeRange(from, RATIONAL_MAX));
|
||||
|
||||
// Remove everything from the minimum point
|
||||
TimeRange remove_range = TimeRange(qMin(from, to), RATIONAL_MAX);
|
||||
RemoveRangeFromJobs(remove_range);
|
||||
Validate(remove_range);
|
||||
Invalidate(remove_range, false);
|
||||
|
||||
// Shift invalidated ranges
|
||||
// (`diff` is POSITIVE when moving forward -> and NEGATIVE when moving backward <-)
|
||||
rational diff = to - from;
|
||||
foreach (const TimeRange& r, ranges_to_shift) {
|
||||
Invalidate(r + diff, 0);
|
||||
Validate(r + diff, false);
|
||||
}
|
||||
|
||||
ShiftEvent(from, to);
|
||||
|
||||
length_ += diff;
|
||||
|
||||
if (diff > 0) {
|
||||
// If shifting forward, add this section to the invalidated region
|
||||
Invalidate(TimeRange(from, to), 0);
|
||||
}
|
||||
|
||||
// Emit signals
|
||||
emit Shifted(from, to);
|
||||
}
|
||||
|
||||
void PlaybackCache::Validate(const TimeRange &r)
|
||||
void PlaybackCache::Validate(const TimeRange &r, bool signal)
|
||||
{
|
||||
invalidated_.remove(r);
|
||||
validated_.insert(r);
|
||||
|
||||
emit Validated(r);
|
||||
}
|
||||
|
||||
void PlaybackCache::LengthChangedEvent(const rational &, const rational &)
|
||||
{
|
||||
if (signal) {
|
||||
emit Validated(r);
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::InvalidateEvent(const TimeRange &)
|
||||
@@ -165,29 +102,27 @@ Project *PlaybackCache::GetProject() const
|
||||
return viewer->project();
|
||||
}
|
||||
|
||||
void PlaybackCache::RemoveRangeFromJobs(const TimeRange &remove)
|
||||
TimeRangeList PlaybackCache::GetInvalidatedRanges(TimeRange intersecting)
|
||||
{
|
||||
// Code shamelessly copied from TimeRangeList::RemoveTimeRange
|
||||
for (int i=0;i<jobs_.size();i++) {
|
||||
JobIdentifier& job = jobs_[i];
|
||||
TimeRange& compare = job.range;
|
||||
TimeRangeList invalidated;
|
||||
|
||||
if (remove.Contains(compare)) {
|
||||
// This element is entirely encompassed in this range, remove it
|
||||
jobs_.removeAt(i);
|
||||
i--;
|
||||
} else if (compare.Contains(remove, false, false)) {
|
||||
// The remove range is within this element, only choice is to split the element into two
|
||||
jobs_.append({TimeRange(remove.out(), compare.out()), job.job_time});
|
||||
compare.set_out(remove.in());
|
||||
} else if (compare.in() < remove.in() && compare.out() > remove.in()) {
|
||||
// This element's out point overlaps the range's in, we'll trim it
|
||||
compare.set_out(remove.in());
|
||||
} else if (compare.in() < remove.out() && compare.out() > remove.out()) {
|
||||
// This element's in point overlaps the range's out, we'll trim it
|
||||
compare.set_in(remove.out());
|
||||
}
|
||||
// Prevent TimeRange from being below 0, some other behavior in Olive relies on this behavior
|
||||
// and it seemed reasonable to have safety code in here
|
||||
intersecting.set_out(qMax(rational(0), intersecting.out()));
|
||||
intersecting.set_in(qMax(rational(0), intersecting.in()));
|
||||
|
||||
invalidated.insert(intersecting);
|
||||
|
||||
foreach (const TimeRange &range, validated_) {
|
||||
invalidated.remove(range);
|
||||
}
|
||||
|
||||
return invalidated;
|
||||
}
|
||||
|
||||
bool PlaybackCache::HasInvalidatedRanges(const TimeRange &intersecting)
|
||||
{
|
||||
return !validated_.contains(intersecting);
|
||||
}
|
||||
|
||||
QString PlaybackCache::GetCacheDirectory() const
|
||||
@@ -201,4 +136,9 @@ QString PlaybackCache::GetCacheDirectory() const
|
||||
}
|
||||
}
|
||||
|
||||
ViewerOutput *PlaybackCache::viewer_parent() const
|
||||
{
|
||||
return dynamic_cast<ViewerOutput*>(parent());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+15
-37
@@ -24,51 +24,44 @@
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
|
||||
#include "common/jobtime.h"
|
||||
#include "common/timerange.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
class Project;
|
||||
class ViewerOutput;
|
||||
|
||||
class PlaybackCache : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
PlaybackCache(QObject* parent = nullptr) :
|
||||
QObject(parent),
|
||||
length_(0)
|
||||
QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
const rational& GetLength()
|
||||
TimeRangeList GetInvalidatedRanges(TimeRange intersecting);
|
||||
TimeRangeList GetInvalidatedRanges(const rational &length)
|
||||
{
|
||||
return length_;
|
||||
return GetInvalidatedRanges(TimeRange(0, length));
|
||||
}
|
||||
|
||||
bool IsFullyValidated()
|
||||
bool HasInvalidatedRanges(const TimeRange &intersecting);
|
||||
bool HasInvalidatedRanges(const rational &length)
|
||||
{
|
||||
return invalidated_.isEmpty();
|
||||
}
|
||||
|
||||
const TimeRangeList& GetInvalidatedRanges()
|
||||
{
|
||||
return invalidated_;
|
||||
}
|
||||
|
||||
bool HasInvalidatedRanges()
|
||||
{
|
||||
return !invalidated_.isEmpty();
|
||||
return HasInvalidatedRanges(TimeRange(0, length));
|
||||
}
|
||||
|
||||
QString GetCacheDirectory() const;
|
||||
|
||||
ViewerOutput *viewer_parent() const;
|
||||
|
||||
void Invalidate(const TimeRange& r, bool signal = true);
|
||||
|
||||
public slots:
|
||||
void Invalidate(const TimeRange& r, qint64 job_time);
|
||||
|
||||
void InvalidateAll();
|
||||
|
||||
void SetLength(const rational& r);
|
||||
|
||||
void Shift(rational from, rational to);
|
||||
|
||||
signals:
|
||||
@@ -78,12 +71,8 @@ signals:
|
||||
|
||||
void Shifted(const olive::rational& from, const olive::rational& to);
|
||||
|
||||
void LengthChanged(const olive::rational& r);
|
||||
|
||||
protected:
|
||||
void Validate(const TimeRange& r);
|
||||
|
||||
virtual void LengthChangedEvent(const rational& old, const rational& newlen);
|
||||
void Validate(const TimeRange& r, bool signal = true);
|
||||
|
||||
virtual void InvalidateEvent(const TimeRange& range);
|
||||
|
||||
@@ -91,19 +80,8 @@ protected:
|
||||
|
||||
Project* GetProject() const;
|
||||
|
||||
struct JobIdentifier {
|
||||
TimeRange range;
|
||||
qint64 job_time;
|
||||
};
|
||||
|
||||
QList<JobIdentifier> jobs_;
|
||||
|
||||
private:
|
||||
void RemoveRangeFromJobs(const TimeRange& remove);
|
||||
|
||||
TimeRangeList invalidated_;
|
||||
|
||||
rational length_;
|
||||
TimeRangeList validated_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
+157
-115
@@ -4,6 +4,7 @@
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#include "codec/conformmanager.h"
|
||||
#include "core.h"
|
||||
#include "node/project/project.h"
|
||||
#include "render/rendermanager.h"
|
||||
#include "render/renderprocessor.h"
|
||||
@@ -14,10 +15,7 @@ PreviewAutoCacher::PreviewAutoCacher() :
|
||||
viewer_node_(nullptr),
|
||||
has_changed_(false),
|
||||
use_custom_range_(false),
|
||||
single_frame_render_(nullptr),
|
||||
last_update_time_(0),
|
||||
ignore_next_mouse_button_(false),
|
||||
last_conform_task_(0)
|
||||
single_frame_render_(nullptr)
|
||||
{
|
||||
paused_ = !Config::Current()[QStringLiteral("AutoCacheEnabled")].toBool(),
|
||||
|
||||
@@ -63,16 +61,20 @@ void PreviewAutoCacher::SetPaused(bool paused)
|
||||
paused_ = paused;
|
||||
}
|
||||
|
||||
void GenerateHashesInternal(ViewerOutput *viewer, FrameHashCache* cache, const QVector<rational> ×, qint64 job_time)
|
||||
QVector<PreviewAutoCacher::HashData> PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, const QVector<rational> ×)
|
||||
{
|
||||
std::vector<QByteArray> existing_hashes;
|
||||
QVector<HashData> hash_data(times.size());
|
||||
|
||||
QVector<QByteArray> existing_hashes;
|
||||
|
||||
for (int i=0; i<times.size(); i++) {
|
||||
const rational &time = times.at(i);
|
||||
|
||||
foreach (const rational& time, times) {
|
||||
// See if hash already exists in disk cache
|
||||
QByteArray hash = RenderManager::Hash(viewer->GetConnectedTextureOutput(), viewer->GetVideoParams(), time);
|
||||
|
||||
// Check memory list since disk checking is slow
|
||||
bool hash_exists = (std::find(existing_hashes.begin(), existing_hashes.end(), hash) != existing_hashes.end());
|
||||
bool hash_exists = existing_hashes.contains(hash);
|
||||
|
||||
if (!hash_exists) {
|
||||
hash_exists = QFileInfo::exists(cache->CachePathName(hash));
|
||||
@@ -83,40 +85,10 @@ void GenerateHashesInternal(ViewerOutput *viewer, FrameHashCache* cache, const Q
|
||||
}
|
||||
|
||||
// Set hash in FrameHashCache's thread rather than in ours to prevent race conditions
|
||||
QMetaObject::invokeMethod(cache, "SetHash", Qt::QueuedConnection,
|
||||
OLIVE_NS_ARG(rational, time),
|
||||
Q_ARG(QByteArray, hash),
|
||||
Q_ARG(qint64, job_time),
|
||||
Q_ARG(bool, hash_exists));
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, const QVector<rational> ×, qint64 job_time)
|
||||
{
|
||||
// Ensure number of threads doesn't exceed idealThreadCount for maximum concurrency
|
||||
int hashes_per_thread = times.size() / qMax(1, QThread::idealThreadCount()-1);
|
||||
|
||||
// Somewhat arbitrary (it felt right) number used to determine when the overhead of sending this
|
||||
// to threads will exceed the benefit of multithreading
|
||||
static const int kMinimumHashesPerThread = 500;
|
||||
if (hashes_per_thread < kMinimumHashesPerThread) {
|
||||
hashes_per_thread = kMinimumHashesPerThread;
|
||||
hash_data[i] = {time, hash, hash_exists};
|
||||
}
|
||||
|
||||
// Queue threaded tasks for each
|
||||
if (hashes_per_thread >= times.size()) {
|
||||
// Don't bother queuing in other thread, just run
|
||||
GenerateHashesInternal(viewer, cache, times, job_time);
|
||||
} else {
|
||||
QVector<QFuture<void> > threads;
|
||||
for (int i=0; i<times.size(); i+=hashes_per_thread) {
|
||||
threads.append(QtConcurrent::run(GenerateHashesInternal, viewer, cache, times.mid(i, i == times.size() - 1 ? -1 : hashes_per_thread), job_time));
|
||||
}
|
||||
|
||||
for (int i=0; i<threads.size(); i++) {
|
||||
threads[i].waitForFinished();
|
||||
}
|
||||
}
|
||||
return hash_data;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::VideoInvalidated(const TimeRange &range)
|
||||
@@ -124,10 +96,9 @@ void PreviewAutoCacher::VideoInvalidated(const TimeRange &range)
|
||||
ClearVideoQueue();
|
||||
|
||||
// Hash these frames since that should be relatively quick.
|
||||
if (ignore_next_mouse_button_ || !(qApp->mouseButtons() & Qt::LeftButton)) {
|
||||
ignore_next_mouse_button_ = false;
|
||||
|
||||
if (!Core::instance()->EffectsSliderIsBeingDragged()) {
|
||||
invalidated_video_.insert(range);
|
||||
video_job_tracker_.insert(range, graph_changed_time_);
|
||||
|
||||
TryRender();
|
||||
}
|
||||
@@ -135,7 +106,9 @@ void PreviewAutoCacher::VideoInvalidated(const TimeRange &range)
|
||||
|
||||
void PreviewAutoCacher::AudioInvalidated(const TimeRange &range)
|
||||
{
|
||||
// ClearAudioQueue();
|
||||
// ClearAudioQueue();
|
||||
|
||||
audio_job_tracker_.insert(range, graph_changed_time_);
|
||||
|
||||
// Start jobs to re-render the audio at this range, split into 2 second chunks
|
||||
invalidated_audio_.insert(range);
|
||||
@@ -145,19 +118,33 @@ void PreviewAutoCacher::AudioInvalidated(const TimeRange &range)
|
||||
|
||||
void PreviewAutoCacher::HashesProcessed()
|
||||
{
|
||||
QFutureWatcher<void>* watcher = static_cast<QFutureWatcher<void>*>(sender());
|
||||
QFutureWatcher< QVector<HashData> >* watcher = static_cast<QFutureWatcher<QVector<HashData> >*>(sender());
|
||||
|
||||
if (hash_tasks_.contains(watcher)) {
|
||||
hash_tasks_.removeOne(watcher);
|
||||
|
||||
// Restart delayed requeue timer
|
||||
delayed_requeue_timer_.stop();
|
||||
delayed_requeue_timer_.start();
|
||||
// Set all hashes we received
|
||||
JobTime job_time = watcher->property("job").value<JobTime>();
|
||||
auto hashes = watcher->result();
|
||||
foreach (auto hash, hashes) {
|
||||
if (video_job_tracker_.isCurrent(hash.time, job_time)) {
|
||||
viewer_node_->video_frame_cache()->SetHash(hash.time, hash.hash, hash.exists);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hash_iterator_.HasNext()) {
|
||||
// Restart delayed requeue timer
|
||||
delayed_requeue_timer_.stop();
|
||||
delayed_requeue_timer_.start();
|
||||
}
|
||||
}
|
||||
|
||||
// The cacher might be waiting for this job to finish
|
||||
if (!graph_update_queue_.isEmpty()) {
|
||||
TryRender();
|
||||
} else if (hash_iterator_.HasNext()) {
|
||||
// Launch next hashes
|
||||
QueueNextHashTask();
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
@@ -170,20 +157,23 @@ void PreviewAutoCacher::AudioRendered()
|
||||
if (audio_tasks_.contains(watcher)) {
|
||||
if (watcher->HasResult()) {
|
||||
const TimeRange &range = audio_tasks_.value(watcher);
|
||||
JobTime watcher_job_time = watcher->property("job").value<JobTime>();
|
||||
|
||||
TimeRangeList valid_ranges = audio_job_tracker_.getCurrentSubRanges(range, watcher_job_time);
|
||||
|
||||
AudioVisualWaveform waveform = watcher->GetTicket()->property("waveform").value<AudioVisualWaveform>();
|
||||
|
||||
viewer_node_->audio_playback_cache()->WritePCM(range,
|
||||
valid_ranges,
|
||||
watcher->Get().value<SampleBufferPtr>(),
|
||||
&waveform,
|
||||
watcher->GetTicket()->GetJobTime());
|
||||
&waveform);
|
||||
|
||||
bool pcm_is_usable = true;
|
||||
|
||||
if (watcher->GetTicket()->property("incomplete").toBool()) {
|
||||
if (last_conform_task_ > watcher->GetTicket()->GetJobTime()) {
|
||||
if (last_conform_task_ > watcher_job_time) {
|
||||
// Requeue now
|
||||
viewer_node_->audio_playback_cache()->Invalidate(range, QDateTime::currentMSecsSinceEpoch());
|
||||
viewer_node_->audio_playback_cache()->Invalidate(range);
|
||||
pcm_is_usable = false;
|
||||
} else {
|
||||
// Wait for conform
|
||||
@@ -205,19 +195,15 @@ void PreviewAutoCacher::AudioRendered()
|
||||
}
|
||||
}
|
||||
|
||||
if (track) {
|
||||
QList<TimeRange> valid_ranges = viewer_node_->audio_playback_cache()->GetValidRanges(waveform_info.range,
|
||||
watcher->GetTicket()->GetJobTime());
|
||||
if (!valid_ranges.isEmpty()) {
|
||||
// Generate visual waveform in this background thread
|
||||
track->waveform().set_channel_count(viewer_node_->GetAudioParams().channel_count());
|
||||
if (track && !valid_ranges.isEmpty()) {
|
||||
// Generate visual waveform in this background thread
|
||||
track->waveform().set_channel_count(viewer_node_->GetAudioParams().channel_count());
|
||||
|
||||
foreach (const TimeRange& r, valid_ranges) {
|
||||
track->waveform().OverwriteSums(waveform_info.waveform, r.in(), r.in() - waveform_info.range.in(), r.length());
|
||||
}
|
||||
|
||||
emit track->PreviewChanged();
|
||||
foreach (const TimeRange& r, valid_ranges) {
|
||||
track->waveform().OverwriteSums(waveform_info.waveform, r.in(), r.in() - waveform_info.range.in(), r.length());
|
||||
}
|
||||
|
||||
emit track->PreviewChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,7 +213,9 @@ void PreviewAutoCacher::AudioRendered()
|
||||
}
|
||||
|
||||
// The cacher might be waiting for this job to finish
|
||||
if (!graph_update_queue_.isEmpty()) {
|
||||
if (graph_update_queue_.isEmpty()) {
|
||||
QueueNextAudioTask();
|
||||
} else {
|
||||
TryRender();
|
||||
}
|
||||
|
||||
@@ -246,6 +234,7 @@ void PreviewAutoCacher::VideoRendered()
|
||||
if (!hash.isEmpty() && VideoParams::FormatIsFloat(viewer_node_->GetVideoParams().format())) {
|
||||
FramePtr frame = watcher->Get().value<FramePtr>();
|
||||
RenderTicketWatcher* w = new RenderTicketWatcher();
|
||||
w->setProperty("job", QVariant::fromValue(last_update_time_));
|
||||
w->setProperty("frame", QVariant::fromValue(frame));
|
||||
video_download_tasks_.insert(w, hash);
|
||||
connect(w, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::VideoDownloaded);
|
||||
@@ -274,6 +263,8 @@ void PreviewAutoCacher::VideoRendered()
|
||||
TryRender();
|
||||
}
|
||||
|
||||
QueueNextFrameInRange(1);
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
|
||||
@@ -394,9 +385,14 @@ void PreviewAutoCacher::InsertIntoCopyMap(Node *node, Node *copy)
|
||||
Node::CopyInputs(node, copy, false);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::UpdateGraphChangeValue()
|
||||
{
|
||||
graph_changed_time_.Acquire();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::UpdateLastSyncedValue()
|
||||
{
|
||||
last_update_time_ = QDateTime::currentMSecsSinceEpoch();
|
||||
last_update_time_.Acquire();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::CancelQueuedSingleFrameRender()
|
||||
@@ -442,11 +438,14 @@ void PreviewAutoCacher::ClearVideoQueue(bool hard)
|
||||
|
||||
has_changed_ = true;
|
||||
use_custom_range_ = false;
|
||||
queued_frame_iterator_.reset();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::ClearAudioQueue(bool hard)
|
||||
{
|
||||
ClearQueueInternal(audio_tasks_, hard, &PreviewAutoCacher::AudioRendered);
|
||||
|
||||
audio_iterator_.clear();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::ClearVideoDownloadQueue(bool hard)
|
||||
@@ -457,26 +456,31 @@ void PreviewAutoCacher::ClearVideoDownloadQueue(bool hard)
|
||||
void PreviewAutoCacher::NodeAdded(Node *node)
|
||||
{
|
||||
graph_update_queue_.append({QueuedJob::kNodeAdded, node, NodeInput(), NodeOutput()});
|
||||
UpdateGraphChangeValue();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::NodeRemoved(Node *node)
|
||||
{
|
||||
graph_update_queue_.append({QueuedJob::kNodeRemoved, node, NodeInput(), NodeOutput()});
|
||||
UpdateGraphChangeValue();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::EdgeAdded(const NodeOutput &output, const NodeInput &input)
|
||||
{
|
||||
graph_update_queue_.append({QueuedJob::kEdgeAdded, nullptr, input, output});
|
||||
UpdateGraphChangeValue();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::EdgeRemoved(const NodeOutput &output, const NodeInput &input)
|
||||
{
|
||||
graph_update_queue_.append({QueuedJob::kEdgeRemoved, nullptr, input, output});
|
||||
UpdateGraphChangeValue();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::ValueChanged(const NodeInput &input)
|
||||
{
|
||||
graph_update_queue_.append({QueuedJob::kValueChanged, nullptr, input, NodeOutput()});
|
||||
UpdateGraphChangeValue();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::TryRender()
|
||||
@@ -493,30 +497,20 @@ void PreviewAutoCacher::TryRender()
|
||||
|
||||
// If we're here, we must be able to render
|
||||
if (!invalidated_video_.isEmpty()) {
|
||||
QVector<rational> frames = viewer_node_->video_frame_cache()->GetFrameListFromTimeRange(invalidated_video_);
|
||||
hash_iterator_ = TimeRangeListFrameIterator(invalidated_video_, viewer_node_->video_frame_cache()->GetTimebase());
|
||||
|
||||
QFutureWatcher<void>* watcher = new QFutureWatcher<void>();
|
||||
hash_tasks_.append(watcher);
|
||||
connect(watcher, &QFutureWatcher<void>::finished, this, &PreviewAutoCacher::HashesProcessed);
|
||||
watcher->setFuture(QtConcurrent::run(&PreviewAutoCacher::GenerateHashes,
|
||||
copied_viewer_node_,
|
||||
viewer_node_->video_frame_cache(),
|
||||
frames,
|
||||
last_update_time_));
|
||||
for (int i=0; i<QThread::idealThreadCount(); i++) {
|
||||
QueueNextHashTask();
|
||||
}
|
||||
|
||||
invalidated_video_.clear();
|
||||
}
|
||||
|
||||
if (!invalidated_audio_.isEmpty()) {
|
||||
foreach (const TimeRange& range, invalidated_audio_) {
|
||||
std::list<TimeRange> chunks = range.Split(30);
|
||||
audio_iterator_ = invalidated_audio_;
|
||||
|
||||
foreach (const TimeRange& r, chunks) {
|
||||
RenderTicketWatcher* watcher = new RenderTicketWatcher();
|
||||
connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered);
|
||||
audio_tasks_.insert(watcher, r);
|
||||
watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, true));
|
||||
}
|
||||
for (int i=0; i<QThread::idealThreadCount(); i++) {
|
||||
QueueNextAudioTask();
|
||||
}
|
||||
|
||||
invalidated_audio_.clear();
|
||||
@@ -547,6 +541,7 @@ RenderTicketWatcher* PreviewAutoCacher::RenderFrame(const QByteArray &hash, cons
|
||||
{
|
||||
RenderTicketWatcher* watcher = new RenderTicketWatcher();
|
||||
watcher->setProperty("hash", hash);
|
||||
watcher->setProperty("job", QVariant::fromValue(last_update_time_));
|
||||
connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::VideoRendered);
|
||||
video_tasks_.insert(watcher, hash);
|
||||
watcher->SetTicket(RenderManager::instance()->RenderFrame(copied_viewer_node_,
|
||||
@@ -564,7 +559,7 @@ void PreviewAutoCacher::RequeueFrames()
|
||||
delayed_requeue_timer_.stop();
|
||||
|
||||
if (viewer_node_
|
||||
&& viewer_node_->video_frame_cache()->HasInvalidatedRanges()
|
||||
&& viewer_node_->video_frame_cache()->HasInvalidatedRanges(viewer_node_->GetVideoLength())
|
||||
&& hash_tasks_.isEmpty()
|
||||
&& has_changed_
|
||||
&& VideoParams::FormatIsFloat(viewer_node_->GetVideoParams().format())
|
||||
@@ -578,30 +573,10 @@ void PreviewAutoCacher::RequeueFrames()
|
||||
using_range = cache_range_;
|
||||
}
|
||||
|
||||
QVector<rational> invalidated_ranges = viewer_node_->video_frame_cache()->GetInvalidatedFrames(using_range);
|
||||
TimeRangeList invalidated = viewer_node_->video_frame_cache()->GetInvalidatedRanges(using_range);
|
||||
queued_frame_iterator_ = TimeRangeListFrameIterator(invalidated, viewer_node_->video_frame_cache()->GetTimebase());
|
||||
|
||||
foreach (const rational& t, invalidated_ranges) {
|
||||
const QByteArray& hash = viewer_node_->video_frame_cache()->GetHash(t);
|
||||
|
||||
RenderTicketWatcher* render_task = video_tasks_.key(hash);
|
||||
|
||||
if (t >= using_range.in()
|
||||
&& t < using_range.out()) {
|
||||
// We want this hash, if we're not already rendering, start render now
|
||||
if (!render_task && !video_download_tasks_.key(hash)) {
|
||||
// Don't render any hash more than once
|
||||
RenderFrame(hash, t, false, false);
|
||||
}
|
||||
} else if (render_task) {
|
||||
// Cancel this frame unless it's already started
|
||||
QMutexLocker locker(render_task->GetTicket()->lock());
|
||||
|
||||
if (!render_task->GetTicket()->IsRunning(false)) {
|
||||
video_tasks_.remove(render_task);
|
||||
delete render_task;
|
||||
}
|
||||
}
|
||||
}
|
||||
QueueNextFrameInRange(RenderManager::GetNumberOfIdealConcurrentJobs());
|
||||
|
||||
has_changed_ = false;
|
||||
}
|
||||
@@ -609,21 +584,16 @@ void PreviewAutoCacher::RequeueFrames()
|
||||
|
||||
void PreviewAutoCacher::ConformFinished()
|
||||
{
|
||||
last_conform_task_ = QDateTime::currentMSecsSinceEpoch();
|
||||
last_conform_task_.Acquire();
|
||||
|
||||
if (viewer_node_) {
|
||||
foreach (const TimeRange &range, audio_needing_conform_) {
|
||||
viewer_node_->audio_playback_cache()->Invalidate(range, QDateTime::currentMSecsSinceEpoch());
|
||||
viewer_node_->audio_playback_cache()->Invalidate(range);
|
||||
}
|
||||
audio_needing_conform_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::IgnoreNextMouseButton()
|
||||
{
|
||||
ignore_next_mouse_button_ = true;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::ForceCacheRange(const TimeRange &range)
|
||||
{
|
||||
has_changed_ = true;
|
||||
@@ -671,6 +641,8 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node)
|
||||
copy_map_.clear();
|
||||
copied_viewer_node_ = nullptr;
|
||||
graph_update_queue_.clear();
|
||||
video_job_tracker_.clear();
|
||||
audio_job_tracker_.clear();
|
||||
|
||||
// Disconnect signals for future node additions/deletions
|
||||
NodeGraph* graph = viewer_node_->parent();
|
||||
@@ -709,6 +681,8 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node)
|
||||
|
||||
// Find copied viewer node
|
||||
copied_viewer_node_ = static_cast<ViewerOutput*>(copy_map_.value(viewer_node_));
|
||||
copied_viewer_node_->SetViewerVideoCacheEnabled(false);
|
||||
copied_viewer_node_->SetViewerAudioCacheEnabled(false);
|
||||
copied_color_manager_ = static_cast<ColorManager*>(copy_map_.value(viewer_node_->project()->color_manager()));
|
||||
|
||||
// Add all connections
|
||||
@@ -718,6 +692,8 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure graph change value is just before the sync value
|
||||
UpdateGraphChangeValue();
|
||||
UpdateLastSyncedValue();
|
||||
|
||||
// Connect signals for future node additions/deletions
|
||||
@@ -728,8 +704,10 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node)
|
||||
connect(graph, &NodeGraph::ValueChanged, this, &PreviewAutoCacher::ValueChanged);
|
||||
|
||||
// Copy invalidated ranges - used to determine which frames need hashing
|
||||
invalidated_video_ = viewer_node_->video_frame_cache()->GetInvalidatedRanges();
|
||||
invalidated_audio_ = viewer_node_->audio_playback_cache()->GetInvalidatedRanges();
|
||||
invalidated_video_ = viewer_node_->video_frame_cache()->GetInvalidatedRanges(viewer_node_->GetVideoLength());
|
||||
video_job_tracker_.insert(invalidated_video_, graph_changed_time_);
|
||||
invalidated_audio_ = viewer_node_->audio_playback_cache()->GetInvalidatedRanges(viewer_node_->GetAudioLength());
|
||||
audio_job_tracker_.insert(invalidated_audio_, graph_changed_time_);
|
||||
|
||||
connect(viewer_node_->video_frame_cache(),
|
||||
&PlaybackCache::Invalidated,
|
||||
@@ -775,6 +753,70 @@ void PreviewAutoCacher::ClearQueueRemoveEventInternal(QVector<RenderTicketWatche
|
||||
Q_UNUSED(it)
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::QueueNextFrameInRange(int max)
|
||||
{
|
||||
rational t;
|
||||
while (max && queued_frame_iterator_.GetNext(&t)) {
|
||||
const QByteArray& hash = viewer_node_->video_frame_cache()->GetHash(t);
|
||||
|
||||
RenderTicketWatcher* render_task = video_tasks_.key(hash);
|
||||
|
||||
// We want this hash, if we're not already rendering, start render now
|
||||
if (!render_task && !video_download_tasks_.key(hash)) {
|
||||
// Don't render any hash more than once
|
||||
RenderFrame(hash, t, false, false);
|
||||
|
||||
max--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::QueueNextHashTask()
|
||||
{
|
||||
// Magic number: dunno what the best number for this is yet
|
||||
static const int kMaxFrames = 1000;
|
||||
|
||||
QVector<rational> times(kMaxFrames);
|
||||
for (int i=0; i<kMaxFrames; i++) {
|
||||
rational r;
|
||||
if (hash_iterator_.GetNext(&r)) {
|
||||
times[i] = r;
|
||||
} else {
|
||||
times.resize(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QFutureWatcher< QVector<HashData> >* watcher = new QFutureWatcher< QVector<HashData> >();
|
||||
watcher->setProperty("job", QVariant::fromValue(last_update_time_));
|
||||
hash_tasks_.append(watcher);
|
||||
connect(watcher, &QFutureWatcher< QVector<HashData> >::finished, this, &PreviewAutoCacher::HashesProcessed);
|
||||
watcher->setFuture(QtConcurrent::run(PreviewAutoCacher::GenerateHashes,
|
||||
copied_viewer_node_,
|
||||
viewer_node_->video_frame_cache(),
|
||||
times));
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::QueueNextAudioTask()
|
||||
{
|
||||
if (!audio_iterator_.isEmpty()) {
|
||||
// Copy first range in list
|
||||
TimeRange r = audio_iterator_.first();
|
||||
|
||||
// Limit to 30 seconds (FIXME: Hardcoded)
|
||||
r.set_out(qMin(r.out(), r.in() + 30));
|
||||
|
||||
// Start job
|
||||
RenderTicketWatcher* watcher = new RenderTicketWatcher();
|
||||
watcher->setProperty("job", QVariant::fromValue(last_update_time_));
|
||||
connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered);
|
||||
audio_tasks_.insert(watcher, r);
|
||||
watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, true));
|
||||
|
||||
audio_iterator_.remove(r);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T, typename Func>
|
||||
void PreviewAutoCacher::ClearQueueInternal(T& list, bool hard, Func member)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "node/node.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "node/project/project.h"
|
||||
#include "render/renderjobtracker.h"
|
||||
#include "threading/threadticketwatcher.h"
|
||||
|
||||
namespace olive {
|
||||
@@ -33,16 +34,6 @@ public:
|
||||
*/
|
||||
void SetViewerNode(ViewerOutput *viewer_node);
|
||||
|
||||
/**
|
||||
* @brief If the mouse is held during the next cache invalidation, cache anyway
|
||||
*
|
||||
* By default, PreviewAutoCacher ignores invalidations that occur while the mouse is held down,
|
||||
* assuming that if the mouse is held, the user is dragging something. If you know the mouse will
|
||||
* be held during a certain action and want PreviewAutoCacher to cache anyway, call this before
|
||||
* the cache invalidates.
|
||||
*/
|
||||
void IgnoreNextMouseButton();
|
||||
|
||||
/**
|
||||
* @brief Returns whether the auto-cache is currently paused or not
|
||||
*/
|
||||
@@ -80,8 +71,6 @@ public:
|
||||
void ClearVideoDownloadQueue(bool wait = false);
|
||||
|
||||
private:
|
||||
static void GenerateHashes(ViewerOutput *viewer, FrameHashCache *cache, const QVector<rational>& times, qint64 job_time);
|
||||
|
||||
void TryRender();
|
||||
|
||||
RenderTicketWatcher *RenderFrame(const QByteArray& hash, const rational &time, bool prioritize, bool texture_only);
|
||||
@@ -104,6 +93,7 @@ private:
|
||||
|
||||
void InsertIntoCopyMap(Node* node, Node* copy);
|
||||
|
||||
void UpdateGraphChangeValue();
|
||||
void UpdateLastSyncedValue();
|
||||
|
||||
void CancelQueuedSingleFrameRender();
|
||||
@@ -115,6 +105,18 @@ private:
|
||||
void ClearQueueRemoveEventInternal(QMap<RenderTicketWatcher*, TimeRange>::iterator it);
|
||||
void ClearQueueRemoveEventInternal(QVector<RenderTicketWatcher*>::iterator it);
|
||||
|
||||
void QueueNextFrameInRange(int max);
|
||||
void QueueNextHashTask();
|
||||
void QueueNextAudioTask();
|
||||
|
||||
struct HashData {
|
||||
rational time;
|
||||
QByteArray hash;
|
||||
bool exists;
|
||||
};
|
||||
|
||||
static QVector<HashData> GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, const QVector<rational> ×);
|
||||
|
||||
class QueuedJob {
|
||||
public:
|
||||
enum Type {
|
||||
@@ -155,21 +157,27 @@ private:
|
||||
|
||||
RenderTicketPtr single_frame_render_;
|
||||
|
||||
QList<QFutureWatcher<void>*> hash_tasks_;
|
||||
QList<QFutureWatcher< QVector<HashData> >*> hash_tasks_;
|
||||
QMap<RenderTicketWatcher*, TimeRange> audio_tasks_;
|
||||
QMap<RenderTicketWatcher*, QByteArray> video_tasks_;
|
||||
QMap<RenderTicketWatcher*, QByteArray> video_download_tasks_;
|
||||
QMap<RenderTicketWatcher*, QVector<RenderTicketPtr> > video_immediate_passthroughs_;
|
||||
|
||||
qint64 last_update_time_;
|
||||
|
||||
bool ignore_next_mouse_button_;
|
||||
JobTime graph_changed_time_;
|
||||
JobTime last_update_time_;
|
||||
|
||||
QTimer delayed_requeue_timer_;
|
||||
|
||||
TimeRangeList audio_needing_conform_;
|
||||
|
||||
qint64 last_conform_task_;
|
||||
JobTime last_conform_task_;
|
||||
|
||||
RenderJobTracker video_job_tracker_;
|
||||
RenderJobTracker audio_job_tracker_;
|
||||
|
||||
TimeRangeListFrameIterator queued_frame_iterator_;
|
||||
TimeRangeListFrameIterator hash_iterator_;
|
||||
TimeRangeList audio_iterator_;
|
||||
|
||||
private slots:
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 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 "renderjobtracker.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
void RenderJobTracker::insert(const TimeRange &range, JobTime job_time)
|
||||
{
|
||||
// First remove any ranges with this (code copied
|
||||
TimeRangeList::util_remove(&jobs_, range);
|
||||
|
||||
// Now append the job
|
||||
TimeRangeWithJob job(range, job_time);
|
||||
jobs_.append(job);
|
||||
}
|
||||
|
||||
void RenderJobTracker::insert(const TimeRangeList &ranges, JobTime job_time)
|
||||
{
|
||||
foreach (const TimeRange &r, ranges) {
|
||||
insert(r, job_time);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderJobTracker::clear()
|
||||
{
|
||||
jobs_.clear();
|
||||
}
|
||||
|
||||
bool RenderJobTracker::isCurrent(const rational &time, JobTime job_time) const
|
||||
{
|
||||
for (auto it=jobs_.crbegin(); it!=jobs_.crend(); it++) {
|
||||
if (it->Contains(time)) {
|
||||
return job_time >= it->GetJobTime();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
TimeRangeList RenderJobTracker::getCurrentSubRanges(const TimeRange &range, const JobTime &job_time) const
|
||||
{
|
||||
TimeRangeList current_ranges;
|
||||
|
||||
for (auto it=jobs_.crbegin(); it!=jobs_.crend(); it++) {
|
||||
if (job_time >= it->GetJobTime() && it->OverlapsWith(range)) {
|
||||
current_ranges.insert(it->Intersected(range));
|
||||
}
|
||||
}
|
||||
|
||||
return current_ranges;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 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 RENDERJOBTRACKER_H
|
||||
#define RENDERJOBTRACKER_H
|
||||
|
||||
#include "common/jobtime.h"
|
||||
#include "common/timerange.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
class RenderJobTracker
|
||||
{
|
||||
public:
|
||||
RenderJobTracker() = default;
|
||||
|
||||
void insert(const TimeRange &range, JobTime job_time);
|
||||
void insert(const TimeRangeList &ranges, JobTime job_time);
|
||||
|
||||
void clear();
|
||||
|
||||
bool isCurrent(const rational &time, JobTime job_time) const;
|
||||
|
||||
TimeRangeList getCurrentSubRanges(const TimeRange &range, const JobTime &job_time) const;
|
||||
|
||||
private:
|
||||
class TimeRangeWithJob : public TimeRange
|
||||
{
|
||||
public:
|
||||
TimeRangeWithJob() = default;
|
||||
TimeRangeWithJob(const TimeRange &range, const JobTime &job_time)
|
||||
{
|
||||
set_range(range.in(), range.out());
|
||||
job_time_ = job_time;
|
||||
}
|
||||
|
||||
JobTime GetJobTime() const {return job_time_;}
|
||||
void SetJobTime(JobTime jt) {job_time_ = jt;}
|
||||
|
||||
private:
|
||||
JobTime job_time_;
|
||||
|
||||
};
|
||||
|
||||
QVector<TimeRangeWithJob> jobs_;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // RENDERJOBTRACKER_H
|
||||
@@ -123,6 +123,11 @@ public:
|
||||
return backend_;
|
||||
}
|
||||
|
||||
static int GetNumberOfIdealConcurrentJobs()
|
||||
{
|
||||
return QThread::idealThreadCount();
|
||||
}
|
||||
|
||||
signals:
|
||||
|
||||
private:
|
||||
|
||||
Reference in New Issue
Block a user