build: split the engine into liboakengine.so; worker drops the UI entirely
Physical split: app/{audio,cli,codec,common,config,node,pluginSupport,
render,task,timeline,undo,tool,shaders} plus coreengine, version and
ui/icons+colorcoding move to a new top-level engine/ tree, built as
liboakengine.so (shared). The render backends (oakgl/oakvulkan) move
with it and link the engine library instead of embedding a static
render-core subset (libolive-rendercore is gone).
- oak-render-worker now links liboakengine instead of the whole
libolive-editor object set: 336MB -> 2.9MB, no Qt Widgets UI
- the editor links liboakengine for the engine and keeps only UI
objects in libolive-editor
- install/packaging: GNUInstallDirs libdir on Linux, bundle copy on
macOS, oakengine.dll staged for NSIS, AppImage validation entry
- fix backend lookup for the new layout: DynamicRenderer searched
../app but backends now live in engine/; a stale pre-split liboakgl
in the build tree got dlopened instead, re-initialized and later
destroyed the interposed engine statics (full-suite segfault at
DialogSequenceParameterTab, found via gdb watchpoint)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# Olive - Non-Linear Video Editor
|
||||
# Copyright (C) 2022 Olive Team
|
||||
# Modifications Copyright (C) 2025 mikesolar
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
add_subdirectory(job)
|
||||
add_subdirectory(ipc)
|
||||
add_subdirectory(ocioconf)
|
||||
add_subdirectory(opengl)
|
||||
add_subdirectory(plugin)
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
render/audioplaybackcache.cpp
|
||||
render/audioplaybackcache.h
|
||||
render/audiowaveformcache.cpp
|
||||
render/audiowaveformcache.h
|
||||
render/backend/dynamicrenderer.cpp
|
||||
render/backend/dynamicrenderer.h
|
||||
render/backend/renderbackend_c.h
|
||||
render/cancelatom.h
|
||||
render/colormanagement.cpp
|
||||
render/colorprocessor.cpp
|
||||
render/colorprocessor.h
|
||||
render/colorprocessorcache.h
|
||||
render/diskmanager.cpp
|
||||
render/diskmanager.h
|
||||
render/framehashcache.cpp
|
||||
render/framehashcache.h
|
||||
render/framemanager.cpp
|
||||
render/framemanager.h
|
||||
render/lutlibrary.cpp
|
||||
render/lutlibrary.h
|
||||
render/interlacetexture.cpp
|
||||
render/loopmode.h
|
||||
render/managedcolor.cpp
|
||||
render/managedcolor.h
|
||||
render/playbackcache.cpp
|
||||
render/playbackcache.h
|
||||
render/previewaudiodevice.cpp
|
||||
render/previewaudiodevice.h
|
||||
render/previewautocacher.cpp
|
||||
render/previewautocacher.h
|
||||
render/projectcopier.cpp
|
||||
render/projectcopier.h
|
||||
render/renderer.cpp
|
||||
render/renderer.h
|
||||
render/rendercache.h
|
||||
render/renderjobtracker.cpp
|
||||
render/renderjobtracker.h
|
||||
render/rendermanager.cpp
|
||||
render/rendermanager.h
|
||||
render/renderworkerpool.cpp
|
||||
render/renderworkerpool.h
|
||||
render/rendermodes.h
|
||||
render/renderprocessor.cpp
|
||||
render/renderprocessor.h
|
||||
render/renderticket.cpp
|
||||
render/renderticket.h
|
||||
render/shadercode.h
|
||||
render/subtitleparams.cpp
|
||||
render/subtitleparams.h
|
||||
render/texture.cpp
|
||||
render/texture.h
|
||||
render/videoparams.cpp
|
||||
render/videoparams.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
set(OLIVE_RESOURCES
|
||||
${OLIVE_RESOURCES}
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_ALPHAASSOC_H
|
||||
#define OAK_ALPHAASSOC_H
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
enum AlphaAssociated { k_alpha_none, k_alpha_unassociated, k_alpha_associated };
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_ALPHAASSOC_H
|
||||
@@ -0,0 +1,157 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 <QFile>
|
||||
#include <QRandomGenerator>
|
||||
#include <QUuid>
|
||||
|
||||
#include "common/filefunctions.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
const qint64 AudioPlaybackCache::k_default_segment_size_per_channel =
|
||||
10 * 1024 * 1024;
|
||||
|
||||
AudioPlaybackCache::AudioPlaybackCache(QObject *parent)
|
||||
: PlaybackCache(parent)
|
||||
{
|
||||
}
|
||||
|
||||
AudioPlaybackCache::~AudioPlaybackCache()
|
||||
{
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::set_parameters(const AudioParams ¶ms)
|
||||
{
|
||||
if (params_ == params) {
|
||||
return;
|
||||
}
|
||||
|
||||
params_ = params;
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::write_pcm(const TimeRange &range,
|
||||
const TimeRangeList &valid_ranges,
|
||||
const SampleBuffer &samples)
|
||||
{
|
||||
for (const TimeRange &r : valid_ranges) {
|
||||
if (write_part_of_sample_buffer(samples, r.in(), r.in() - range.in(),
|
||||
r.length())) {
|
||||
validate(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::write_silence(const TimeRange &range)
|
||||
{
|
||||
// WritePCM will automatically fill non-existent bytes with silence, so we just have to send
|
||||
// it an empty sample buffer
|
||||
write_pcm(range, { range }, SampleBuffer());
|
||||
}
|
||||
|
||||
bool AudioPlaybackCache::write_part_of_sample_buffer(const SampleBuffer &samples,
|
||||
const Rational &write_start,
|
||||
const Rational &buffer_start,
|
||||
const Rational &length)
|
||||
{
|
||||
int64_t length_in_bytes = params_.time_to_bytes_per_channel(length);
|
||||
|
||||
int64_t start_cache_offset = params_.time_to_bytes_per_channel(write_start);
|
||||
int64_t end_cache_offset = start_cache_offset + length_in_bytes;
|
||||
|
||||
int64_t start_buffer_offset =
|
||||
params_.time_to_bytes_per_channel(buffer_start);
|
||||
int64_t end_buffer_offset =
|
||||
std::min(start_buffer_offset + length_in_bytes,
|
||||
params_.samples_to_bytes_per_channel(samples.sample_count()));
|
||||
|
||||
int64_t current_cache_offset = start_cache_offset;
|
||||
int64_t current_buffer_offset = start_buffer_offset;
|
||||
|
||||
bool success = true;
|
||||
|
||||
while (current_cache_offset != end_cache_offset) {
|
||||
int64_t segment = current_cache_offset / k_default_segment_size_per_channel;
|
||||
int64_t segment_start = segment * k_default_segment_size_per_channel;
|
||||
int64_t segment_end = segment_start + k_default_segment_size_per_channel;
|
||||
|
||||
int64_t offset_in_segment = current_cache_offset - segment_start;
|
||||
// Never write past the end of the requested range
|
||||
int64_t write_len = std::min(segment_end - current_cache_offset,
|
||||
end_cache_offset - current_cache_offset);
|
||||
int64_t max_buffer_len = end_buffer_offset - current_buffer_offset;
|
||||
int64_t zero_len = 0;
|
||||
|
||||
if (write_len > max_buffer_len) {
|
||||
zero_len = write_len - max_buffer_len;
|
||||
write_len = max_buffer_len;
|
||||
}
|
||||
|
||||
for (int channel = 0; channel < params_.channel_count(); channel++) {
|
||||
QString filename = get_segment_filename(segment, channel);
|
||||
|
||||
if (!FileFunctions::directory_is_valid(QFileInfo(filename).dir())) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
QFile f(filename);
|
||||
if (f.open(QFile::ReadWrite)) {
|
||||
f.seek(offset_in_segment);
|
||||
if (write_len > 0) {
|
||||
f.write(reinterpret_cast<const char *>(samples.data(channel)) +
|
||||
current_buffer_offset,
|
||||
write_len);
|
||||
}
|
||||
|
||||
if (zero_len > 0) {
|
||||
// NOTE: the length must be passed explicitly; write(const
|
||||
// char*) would treat the zeros as an empty C string
|
||||
QByteArray b(zero_len, 0);
|
||||
f.write(b.constData(), b.size());
|
||||
}
|
||||
|
||||
f.close();
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
current_cache_offset += write_len + zero_len;
|
||||
current_buffer_offset += write_len;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
QString AudioPlaybackCache::get_segment_filename(qint64 segment_index,
|
||||
int channel)
|
||||
{
|
||||
return get_this_cache_directory().filePath(QStringLiteral("%1.%2").arg(
|
||||
QString::number(segment_index), QString::number(channel)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_AUDIOPLAYBACKCACHE_H
|
||||
#define OAK_AUDIOPLAYBACKCACHE_H
|
||||
|
||||
#include "audio/audiovisualwaveform.h"
|
||||
#include "render/playbackcache.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief A fully integrated system of storing and playing cached audio
|
||||
*
|
||||
* All audio in Olive is processed and rendered in advance. This makes playback extremely smooth
|
||||
* and reliable, but provides some challenges as far as storing and manipulating this audio while
|
||||
* minimizing the amount of re-renders necessary.
|
||||
*
|
||||
* Olive's PlaybackCaches support "shifting"; moving cached data to a different spot on the
|
||||
* timeline without requiring a costly re-render. While video is naturally stored on disk as
|
||||
* separate frames that are easy to swap out, audio works a little differently. It would be
|
||||
* extremely inefficient to store each sample as a separate file on the disk, but storing in
|
||||
* one single contiguous file would be detrimental to shifting, particularly for longer timelines
|
||||
* since the data will actually have to be shifted on disk.
|
||||
*
|
||||
* As such, AudioPlaybackCache compromises by storing audio in several "segments". This makes
|
||||
* operations like shifting much easier since segments can simply be removed from the playlist
|
||||
* rather than having to shift or re-render potentially hours of audio in every operation.
|
||||
*
|
||||
* Naturally, storing in segments means you can't simply play the PCM data like a file, so
|
||||
* AudioPlaybackCache also provides a playback device (accessible from CreatePlaybackDevice()) that
|
||||
* acts identically to a file-based IO device, transparently joining segments together and acting
|
||||
* like one contiguous file.
|
||||
*/
|
||||
class AudioPlaybackCache : public PlaybackCache {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioPlaybackCache(QObject *parent = nullptr);
|
||||
|
||||
virtual ~AudioPlaybackCache() override;
|
||||
|
||||
AudioParams get_parameters()
|
||||
{
|
||||
return params_;
|
||||
}
|
||||
|
||||
void set_parameters(const AudioParams ¶ms);
|
||||
|
||||
void write_pcm(const TimeRange &range, const TimeRangeList &valid_ranges,
|
||||
const SampleBuffer &samples);
|
||||
|
||||
void write_silence(const TimeRange &range);
|
||||
|
||||
private:
|
||||
bool write_part_of_sample_buffer(const SampleBuffer &samples,
|
||||
const Rational &write_start,
|
||||
const Rational &buffer_start,
|
||||
const Rational &length);
|
||||
|
||||
QString get_segment_filename(qint64 segment_index, int channel);
|
||||
|
||||
static const qint64 k_default_segment_size_per_channel;
|
||||
|
||||
AudioParams params_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIOPLAYBACKCACHE_H
|
||||
@@ -0,0 +1,130 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "audiowaveformcache.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
#define super PlaybackCache
|
||||
|
||||
AudioWaveformCache::AudioWaveformCache(QObject *parent)
|
||||
: super{ parent }
|
||||
{
|
||||
waveforms_ = std::make_shared<AudioVisualWaveform>();
|
||||
}
|
||||
|
||||
void AudioWaveformCache::write_waveform(const TimeRange &range,
|
||||
const TimeRangeList &valid_ranges,
|
||||
const AudioVisualWaveform *waveform)
|
||||
{
|
||||
// Write each valid range to the segments
|
||||
foreach (const TimeRange &r, valid_ranges) {
|
||||
if (waveform) {
|
||||
waveforms_->overwrite_sums(*waveform, r.in(), r.in() - range.in(),
|
||||
r.length());
|
||||
}
|
||||
|
||||
validate(r);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_sub_rect(QPainter *painter, const QRect &rect, const double &scale,
|
||||
const TimeRange &wave_range,
|
||||
const AudioVisualWaveform &waveform, const TimeRange &subrange)
|
||||
{
|
||||
// Find start time of passthrough
|
||||
TimeRange intersect = wave_range.intersected(subrange);
|
||||
|
||||
// Create new rect that starts at the offset of pass_start from start_time
|
||||
// Set rect width to either length of passthrough or until the end
|
||||
QRect pass_rect(
|
||||
rect.x() + (intersect.in() - wave_range.in()).to_double() * scale,
|
||||
rect.y(), intersect.length().to_double() * scale, rect.height());
|
||||
|
||||
// Draw waveform with this info
|
||||
AudioVisualWaveform::draw_waveform(painter, pass_rect, scale, waveform,
|
||||
intersect.in());
|
||||
}
|
||||
|
||||
void AudioWaveformCache::Draw(QPainter *painter, const QRect &rect,
|
||||
const double &scale,
|
||||
const Rational &start_time) const
|
||||
{
|
||||
if (!passthroughs_.empty()) {
|
||||
TimeRange wave_range(start_time,
|
||||
start_time +
|
||||
Rational::from_double(rect.width() / scale));
|
||||
TimeRangeList draw_range = { wave_range };
|
||||
for (const WaveformPassthrough &p : passthroughs_) {
|
||||
if (draw_range.overlaps_with(p, true, false)) {
|
||||
draw_sub_rect(painter, rect, scale, wave_range, *p.waveform, p);
|
||||
|
||||
// Remove this range
|
||||
draw_range.remove(p);
|
||||
}
|
||||
}
|
||||
|
||||
for (const TimeRange &r : draw_range) {
|
||||
draw_sub_rect(painter, rect, scale, wave_range, *waveforms_, r);
|
||||
}
|
||||
} else {
|
||||
AudioVisualWaveform::draw_waveform(painter, rect, scale, *waveforms_,
|
||||
start_time);
|
||||
}
|
||||
}
|
||||
|
||||
AudioVisualWaveform::Sample
|
||||
AudioWaveformCache::get_summary_from_time(const Rational &start,
|
||||
const Rational &length) const
|
||||
{
|
||||
return waveforms_->get_summary_from_time(start, length);
|
||||
}
|
||||
|
||||
Rational AudioWaveformCache::length() const
|
||||
{
|
||||
return waveforms_->length();
|
||||
}
|
||||
|
||||
void AudioWaveformCache::set_passthrough(PlaybackCache *cache)
|
||||
{
|
||||
AudioWaveformCache *c = static_cast<AudioWaveformCache *>(cache);
|
||||
|
||||
for (const TimeRange &r : c->get_validated_ranges()) {
|
||||
WaveformPassthrough t = r;
|
||||
t.waveform = c->waveforms_;
|
||||
passthroughs_.push_back(t);
|
||||
}
|
||||
passthroughs_.insert(passthroughs_.end(), c->passthroughs_.begin(),
|
||||
c->passthroughs_.end());
|
||||
|
||||
set_parameters(c->get_parameters());
|
||||
set_saving_enabled(c->is_saving_enabled());
|
||||
}
|
||||
|
||||
void AudioWaveformCache::InvalidateEvent(const TimeRange &range)
|
||||
{
|
||||
TimeRangeList::util_remove(&passthroughs_, range);
|
||||
|
||||
super::InvalidateEvent(range);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_AUDIOWAVEFORMCACHE_H
|
||||
#define OAK_AUDIOWAVEFORMCACHE_H
|
||||
|
||||
#include "audio/audiovisualwaveform.h"
|
||||
#include "playbackcache.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class AudioWaveformCache : public PlaybackCache {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioWaveformCache(QObject *parent = nullptr);
|
||||
|
||||
void write_waveform(const TimeRange &range,
|
||||
const TimeRangeList &valid_ranges,
|
||||
const AudioVisualWaveform *waveform);
|
||||
|
||||
const AudioParams &get_parameters() const
|
||||
{
|
||||
return params_;
|
||||
}
|
||||
void set_parameters(const AudioParams &p)
|
||||
{
|
||||
params_ = p;
|
||||
waveforms_->set_channel_count(p.channel_count());
|
||||
}
|
||||
|
||||
void Draw(QPainter *painter, const QRect &rect, const double &scale,
|
||||
const Rational &start_time) const;
|
||||
|
||||
AudioVisualWaveform::Sample
|
||||
get_summary_from_time(const Rational &start, const Rational &length) const;
|
||||
|
||||
Rational length() const;
|
||||
|
||||
virtual void set_passthrough(PlaybackCache *cache) override;
|
||||
|
||||
protected:
|
||||
virtual void InvalidateEvent(const TimeRange &range) override;
|
||||
|
||||
private:
|
||||
using WaveformPtr = std::shared_ptr<AudioVisualWaveform>;
|
||||
|
||||
WaveformPtr waveforms_;
|
||||
|
||||
AudioParams params_;
|
||||
|
||||
class WaveformPassthrough : public TimeRange {
|
||||
public:
|
||||
WaveformPassthrough(const TimeRange &r)
|
||||
: TimeRange(r)
|
||||
{
|
||||
}
|
||||
|
||||
WaveformPtr waveform;
|
||||
};
|
||||
|
||||
std::vector<WaveformPassthrough> passthroughs_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIOWAVEFORMCACHE_H
|
||||
@@ -0,0 +1,397 @@
|
||||
#include "dynamicrenderer.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QOpenGLContext>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
// Stores the requested backend name; the actual backend may later become
|
||||
// OpenGL if loading or availability checks require a Vulkan fallback.
|
||||
DynamicRenderer::DynamicRenderer(const QString &backend, QObject *parent)
|
||||
: Renderer(parent)
|
||||
, backend_(backend.toLower())
|
||||
{
|
||||
}
|
||||
|
||||
// Tears down the backend in the reverse order used by Load(): release renderer
|
||||
// resources, destroy the opaque backend object, then unload the shared library.
|
||||
DynamicRenderer::~DynamicRenderer()
|
||||
{
|
||||
destroy();
|
||||
post_destroy();
|
||||
if (handle_ && destroy_) {
|
||||
destroy_(handle_);
|
||||
handle_ = nullptr;
|
||||
}
|
||||
if (library_.isLoaded()) {
|
||||
library_.unload();
|
||||
}
|
||||
}
|
||||
|
||||
// Builds the private backend library path for the current platform.
|
||||
// The search is intentionally restricted to Oak-controlled directories so a
|
||||
// system libGL/libvulkan loader is never mistaken for an Oak render backend.
|
||||
QString DynamicRenderer::library_filename() const
|
||||
{
|
||||
QString base;
|
||||
if (backend_ == QStringLiteral("opengl")) {
|
||||
base = QStringLiteral("oakgl");
|
||||
} else if (backend_ == QStringLiteral("vulkan")) {
|
||||
base = QStringLiteral("oakvulkan");
|
||||
} else {
|
||||
// Unknown backend: use the name verbatim so the load fails and the
|
||||
// caller's OpenGL fallback engages
|
||||
base = backend_;
|
||||
}
|
||||
#if defined(Q_OS_WIN)
|
||||
const QString filename = base + QStringLiteral(".dll");
|
||||
#elif defined(Q_OS_MAC)
|
||||
const QString filename =
|
||||
QStringLiteral("lib") + base + QStringLiteral(".dylib");
|
||||
#else
|
||||
const QString filename =
|
||||
QStringLiteral("lib") + base + QStringLiteral(".so");
|
||||
#endif
|
||||
|
||||
const QDir app_dir(QCoreApplication::applicationDirPath());
|
||||
const QStringList candidates = {
|
||||
app_dir.filePath(filename),
|
||||
app_dir.filePath(
|
||||
QDir(QStringLiteral("render_backends")).filePath(filename)),
|
||||
app_dir.filePath(QDir(QStringLiteral("../lib")).filePath(filename)),
|
||||
app_dir.filePath(QDir(QStringLiteral("../../lib")).filePath(filename)),
|
||||
app_dir.filePath(QDir(QStringLiteral("../engine")).filePath(filename)),
|
||||
app_dir.filePath(
|
||||
QDir(QStringLiteral("../../engine")).filePath(filename))
|
||||
};
|
||||
for (const QString &candidate : candidates) {
|
||||
if (QFileInfo::exists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return candidates.first();
|
||||
}
|
||||
|
||||
// Loads the selected backend, resolves its C ABI table, creates the opaque
|
||||
// backend object, and optionally falls back from Vulkan to OpenGL when runtime
|
||||
// availability checks fail.
|
||||
bool DynamicRenderer::load()
|
||||
{
|
||||
if (handle_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
library_.setFileName(library_filename());
|
||||
if (!library_.load()) {
|
||||
if (backend_ == QStringLiteral("vulkan")) {
|
||||
qWarning()
|
||||
<< "Failed to load Vulkan render backend" << library_.fileName()
|
||||
<< library_.errorString() << "falling back to OpenGL backend";
|
||||
backend_ = QStringLiteral("opengl");
|
||||
library_.setFileName(library_filename());
|
||||
}
|
||||
|
||||
if (!library_.load()) {
|
||||
qWarning() << "Failed to load render backend" << backend_
|
||||
<< library_.fileName() << library_.errorString();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolve_functions()) {
|
||||
qWarning() << "Render backend is missing required symbols" << backend_;
|
||||
library_.unload();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pass this (rather than this->parent()) so the backend renderer becomes a
|
||||
// child QObject of the adapter. That ensures it follows DynamicRenderer when
|
||||
// the latter is moved to the render thread; otherwise it stays in the thread
|
||||
// where Load() was called and every GL operation is rejected as "wrong
|
||||
// thread", producing a black screen.
|
||||
handle_ = create_(this);
|
||||
if (!handle_) {
|
||||
library_.unload();
|
||||
return false;
|
||||
}
|
||||
if (is_available_ && !is_available_(handle_)) {
|
||||
qWarning() << "Render backend is not available" << backend_
|
||||
<< library_.fileName();
|
||||
if (backend_ == QStringLiteral("vulkan")) {
|
||||
return fallback_to_open_gl();
|
||||
}
|
||||
destroy_(handle_);
|
||||
handle_ = nullptr;
|
||||
library_.unload();
|
||||
return false;
|
||||
}
|
||||
return handle_ != nullptr;
|
||||
}
|
||||
|
||||
// Resolves the mandatory C ABI entry points from the loaded shared library.
|
||||
// Optional information probes are resolved after the required render interface.
|
||||
bool DynamicRenderer::resolve_functions()
|
||||
{
|
||||
reset_functions();
|
||||
#define RESOLVE(member, type, symbol) \
|
||||
member = reinterpret_cast<type>(library_.resolve(symbol)); \
|
||||
if (!member) \
|
||||
return false
|
||||
|
||||
RESOLVE(create_, OakBackendCreateFn, "oak_renderer_create");
|
||||
RESOLVE(destroy_, OakBackendDestroyFn, "oak_renderer_destroy");
|
||||
RESOLVE(init_, OakBackendInitFn, "oak_renderer_init");
|
||||
RESOLVE(init_with_context_, OakBackendInitWithContextFn,
|
||||
"oak_renderer_init_with_context");
|
||||
RESOLVE(post_init_, OakBackendPostInitFn, "oak_renderer_post_init");
|
||||
RESOLVE(post_destroy_, OakBackendPostDestroyFn,
|
||||
"oak_renderer_post_destroy");
|
||||
RESOLVE(destroy_internal_, OakBackendDestroyInternalFn,
|
||||
"oak_renderer_destroy_internal");
|
||||
RESOLVE(clear_destination_, OakBackendClearDestinationFn,
|
||||
"oak_renderer_clear_destination");
|
||||
RESOLVE(create_native_texture_, OakBackendCreateNativeTextureFn,
|
||||
"oak_renderer_create_native_texture");
|
||||
RESOLVE(destroy_native_texture_, OakBackendDestroyNativeTextureFn,
|
||||
"oak_renderer_destroy_native_texture");
|
||||
RESOLVE(create_native_shader_, OakBackendCreateNativeShaderFn,
|
||||
"oak_renderer_create_native_shader");
|
||||
RESOLVE(destroy_native_shader_, OakBackendDestroyNativeShaderFn,
|
||||
"oak_renderer_destroy_native_shader");
|
||||
RESOLVE(upload_to_texture_, OakBackendUploadToTextureFn,
|
||||
"oak_renderer_upload_to_texture");
|
||||
RESOLVE(download_from_texture_, OakBackendDownloadFromTextureFn,
|
||||
"oak_renderer_download_from_texture");
|
||||
RESOLVE(flush_, OakBackendFlushFn, "oak_renderer_flush");
|
||||
RESOLVE(get_pixel_from_texture_, OakBackendGetPixelFromTextureFn,
|
||||
"oak_renderer_get_pixel_from_texture");
|
||||
RESOLVE(blit_, OakBackendBlitFn, "oak_renderer_blit");
|
||||
RESOLVE(attach_output_texture_, OakBackendAttachOutputTextureFn,
|
||||
"oak_renderer_attach_output_texture");
|
||||
RESOLVE(detach_output_texture_, OakBackendDetachOutputTextureFn,
|
||||
"oak_renderer_detach_output_texture");
|
||||
RESOLVE(opengl_context_, OakBackendOpenGLContextFn,
|
||||
"oak_renderer_opengl_context");
|
||||
#undef RESOLVE
|
||||
get_info_ = reinterpret_cast<OakBackendGetInfoFn>(
|
||||
library_.resolve("oak_renderer_get_info"));
|
||||
is_available_ = reinterpret_cast<OakBackendIsAvailableFn>(
|
||||
library_.resolve("oak_renderer_is_available"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Discards a partially-created backend and restarts loading with the OpenGL
|
||||
// backend. This keeps RenderManager's fallback path inside the adapter.
|
||||
bool DynamicRenderer::fallback_to_open_gl()
|
||||
{
|
||||
if (handle_ && destroy_) {
|
||||
destroy_(handle_);
|
||||
handle_ = nullptr;
|
||||
}
|
||||
if (library_.isLoaded()) {
|
||||
library_.unload();
|
||||
}
|
||||
reset_functions();
|
||||
backend_ = QStringLiteral("opengl");
|
||||
return load();
|
||||
}
|
||||
|
||||
// Clears all cached C function pointers so a failed backend cannot leave stale
|
||||
// call targets behind for a later fallback load.
|
||||
void DynamicRenderer::reset_functions()
|
||||
{
|
||||
create_ = nullptr;
|
||||
destroy_ = nullptr;
|
||||
get_info_ = nullptr;
|
||||
is_available_ = nullptr;
|
||||
init_ = nullptr;
|
||||
init_with_context_ = nullptr;
|
||||
post_init_ = nullptr;
|
||||
post_destroy_ = nullptr;
|
||||
destroy_internal_ = nullptr;
|
||||
clear_destination_ = nullptr;
|
||||
create_native_texture_ = nullptr;
|
||||
destroy_native_texture_ = nullptr;
|
||||
create_native_shader_ = nullptr;
|
||||
destroy_native_shader_ = nullptr;
|
||||
upload_to_texture_ = nullptr;
|
||||
download_from_texture_ = nullptr;
|
||||
flush_ = nullptr;
|
||||
get_pixel_from_texture_ = nullptr;
|
||||
blit_ = nullptr;
|
||||
attach_output_texture_ = nullptr;
|
||||
detach_output_texture_ = nullptr;
|
||||
opengl_context_ = nullptr;
|
||||
}
|
||||
|
||||
// Returns backend metadata exposed by the dynamic library when available.
|
||||
bool DynamicRenderer::get_backend_info(OakRenderBackendInfo *out_info) const
|
||||
{
|
||||
return handle_ && get_info_ && out_info && get_info_(handle_, out_info);
|
||||
}
|
||||
|
||||
// Initializes the loaded backend using its own context/device creation path.
|
||||
bool DynamicRenderer::init()
|
||||
{
|
||||
return load() && init_(handle_);
|
||||
}
|
||||
|
||||
// Initializes an OpenGL backend against an existing widget context; non-OpenGL
|
||||
// backends may ignore the context on the library side.
|
||||
bool DynamicRenderer::init_with_open_gl_context(QOpenGLContext *context)
|
||||
{
|
||||
if (!load()) {
|
||||
return false;
|
||||
}
|
||||
init_with_context_(handle_, context);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Forwards post-destroy cleanup to the backend while the library is still
|
||||
// loaded and its symbols are still valid.
|
||||
void DynamicRenderer::post_destroy()
|
||||
{
|
||||
if (handle_ && post_destroy_) {
|
||||
post_destroy_(handle_);
|
||||
}
|
||||
}
|
||||
|
||||
// Runs backend post-initialization after Init/InitWithOpenGLContext has
|
||||
// established the device or GL context.
|
||||
void DynamicRenderer::post_init()
|
||||
{
|
||||
if (handle_) {
|
||||
post_init_(handle_);
|
||||
}
|
||||
}
|
||||
|
||||
// Forwards render target clearing through the C ABI.
|
||||
void DynamicRenderer::clear_destination(Texture *texture, double r, double g,
|
||||
double b, double a)
|
||||
{
|
||||
clear_destination_(handle_, texture, r, g, b, a);
|
||||
}
|
||||
|
||||
// Creates a backend-native shader and receives the result as an opaque QVariant
|
||||
// because this first-generation ABI still shares C++/Qt types between modules.
|
||||
QVariant DynamicRenderer::create_native_shader(ShaderCode code)
|
||||
{
|
||||
QVariant out;
|
||||
create_native_shader_(handle_, &code, &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Releases a backend-native shader handle.
|
||||
void DynamicRenderer::destroy_native_shader(QVariant shader)
|
||||
{
|
||||
destroy_native_shader_(handle_, &shader);
|
||||
}
|
||||
|
||||
// Uploads CPU pixel data into a backend texture through the dynamic ABI.
|
||||
void DynamicRenderer::upload_to_texture(const QVariant &handle,
|
||||
const VideoParams ¶ms,
|
||||
const void *data, int linesize)
|
||||
{
|
||||
upload_to_texture_(handle_, &handle, ¶ms, data, linesize);
|
||||
}
|
||||
|
||||
// Downloads backend texture data into a caller-provided CPU buffer.
|
||||
void DynamicRenderer::download_from_texture(const QVariant &handle,
|
||||
const VideoParams ¶ms, void *data,
|
||||
int linesize)
|
||||
{
|
||||
download_from_texture_(handle_, &handle, ¶ms, data, linesize);
|
||||
}
|
||||
|
||||
// Waits for backend work to become visible to subsequent CPU or GPU consumers.
|
||||
void DynamicRenderer::flush()
|
||||
{
|
||||
flush_(handle_);
|
||||
}
|
||||
|
||||
// Reads a single pixel through the backend-provided readback hook.
|
||||
Color DynamicRenderer::get_pixel_from_texture(Texture *texture, const QPointF &pt)
|
||||
{
|
||||
Color out;
|
||||
get_pixel_from_texture_(handle_, texture, &pt, &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Exposes the wrapped OpenGL context when the backend is OpenGL; Vulkan returns
|
||||
// null so callers can avoid GL-only paths.
|
||||
QOpenGLContext *DynamicRenderer::open_gl_context() const
|
||||
{
|
||||
return opengl_context_ && handle_ ?
|
||||
static_cast<QOpenGLContext *>(opengl_context_(handle_)) :
|
||||
nullptr;
|
||||
}
|
||||
|
||||
// Reports the effective backend after any load-time fallback has completed.
|
||||
bool DynamicRenderer::is_open_gl() const
|
||||
{
|
||||
return backend_ == QStringLiteral("opengl");
|
||||
}
|
||||
|
||||
bool DynamicRenderer::is_vulkan() const
|
||||
{
|
||||
return backend_ == QStringLiteral("vulkan");
|
||||
}
|
||||
|
||||
// Dispatches a shader blit to the loaded backend.
|
||||
void DynamicRenderer::blit(QVariant shader, AcceleratedJob &job,
|
||||
Texture *destination, VideoParams destination_params,
|
||||
bool clear_destination)
|
||||
{
|
||||
blit_(handle_, &shader, &job, destination, &destination_params,
|
||||
clear_destination);
|
||||
}
|
||||
|
||||
// Allocates a backend-native texture and wraps its opaque handle in QVariant.
|
||||
QVariant DynamicRenderer::create_native_texture(int width, int height, int depth,
|
||||
PixelFormat format,
|
||||
int channel_count,
|
||||
const void *data, int linesize)
|
||||
{
|
||||
QVariant out;
|
||||
create_native_texture_(handle_, width, height, depth, format, channel_count,
|
||||
data, linesize, &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Releases a backend-native texture handle.
|
||||
void DynamicRenderer::destroy_native_texture(QVariant texture)
|
||||
{
|
||||
destroy_native_texture_(handle_, &texture);
|
||||
}
|
||||
|
||||
// Releases renderer-owned backend resources before the backend object itself is
|
||||
// destroyed.
|
||||
void DynamicRenderer::destroy_internal()
|
||||
{
|
||||
if (handle_) {
|
||||
destroy_internal_(handle_);
|
||||
}
|
||||
}
|
||||
|
||||
// Exposes OFX OpenGL output binding through the dynamic backend when supported.
|
||||
void DynamicRenderer::attach_output_texture(Texture *texture)
|
||||
{
|
||||
if (attach_output_texture_ && texture) {
|
||||
QVariant id = texture->id();
|
||||
attach_output_texture_(handle_, &id);
|
||||
}
|
||||
}
|
||||
|
||||
// Clears any OFX output texture binding owned by the backend.
|
||||
void DynamicRenderer::detach_output_texture()
|
||||
{
|
||||
if (detach_output_texture_) {
|
||||
detach_output_texture_(handle_);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
#ifndef OAK_DYNAMICRENDERER_H
|
||||
#define OAK_DYNAMICRENDERER_H
|
||||
|
||||
#include <QLibrary>
|
||||
#include <QString>
|
||||
|
||||
#include "render/backend/renderbackend_c.h"
|
||||
#include "render/opengl/openglcontextprovider.h"
|
||||
#include "render/renderer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
// C++ Renderer adapter that loads an Oak render backend shared library and
|
||||
// forwards Renderer calls through the backend's C ABI.
|
||||
class DynamicRenderer : public Renderer, public OpenGLContextProvider {
|
||||
Q_OBJECT
|
||||
public:
|
||||
// Stores the requested backend name; Load() may change it after fallback.
|
||||
explicit DynamicRenderer(const QString &backend, QObject *parent = nullptr);
|
||||
// Destroys backend resources and unloads the dynamic library.
|
||||
virtual ~DynamicRenderer() override;
|
||||
|
||||
using Renderer::blit;
|
||||
|
||||
// Loads the backend library, resolves C ABI symbols, and creates the handle.
|
||||
bool load();
|
||||
// Initializes an OpenGL backend with a caller-owned viewer context.
|
||||
bool init_with_open_gl_context(QOpenGLContext *context);
|
||||
// Retrieves backend metadata through the optional info entry point.
|
||||
bool get_backend_info(OakRenderBackendInfo *out_info) const;
|
||||
// Returns the effective backend after any load-time fallback.
|
||||
QString backend_name() const
|
||||
{
|
||||
return backend_;
|
||||
}
|
||||
|
||||
// Initializes the backend using its default device/context path.
|
||||
virtual bool init() override;
|
||||
// Runs backend post-destroy cleanup.
|
||||
virtual void post_destroy() override;
|
||||
// Runs backend post-init setup.
|
||||
virtual void post_init() override;
|
||||
// Clears either a native texture destination or the backend output target.
|
||||
virtual void clear_destination(Texture *texture = nullptr, double r = 0.0,
|
||||
double g = 0.0, double b = 0.0,
|
||||
double a = 0.0) override;
|
||||
// Creates a native shader through the dynamic backend.
|
||||
virtual QVariant create_native_shader(ShaderCode code) override;
|
||||
// Destroys a native shader through the dynamic backend.
|
||||
virtual void destroy_native_shader(QVariant shader) override;
|
||||
// Uploads CPU pixels to a backend texture.
|
||||
virtual void upload_to_texture(const QVariant &handle,
|
||||
const VideoParams ¶ms, const void *data,
|
||||
int linesize) override;
|
||||
// Downloads backend texture pixels to CPU memory.
|
||||
virtual void download_from_texture(const QVariant &handle,
|
||||
const VideoParams ¶ms, void *data,
|
||||
int linesize) override;
|
||||
// Waits for backend work to complete.
|
||||
virtual void flush() override;
|
||||
// Reads one pixel from a backend texture.
|
||||
virtual Color get_pixel_from_texture(Texture *texture,
|
||||
const QPointF &pt) override;
|
||||
// Returns the wrapped OpenGL context for OpenGL backends.
|
||||
virtual QOpenGLContext *open_gl_context() const override;
|
||||
|
||||
// Reports whether the effective backend is OpenGL.
|
||||
virtual bool is_open_gl() const override;
|
||||
// Reports whether the effective backend is Vulkan.
|
||||
virtual bool is_vulkan() const override;
|
||||
|
||||
// Attaches a texture for OFX OpenGL output when supported.
|
||||
virtual void attach_output_texture(Texture *texture) override;
|
||||
|
||||
// Detaches any OFX output texture binding when supported.
|
||||
virtual void detach_output_texture() override;
|
||||
|
||||
protected:
|
||||
// Dispatches a shader blit through the dynamic backend.
|
||||
virtual void blit(QVariant shader, AcceleratedJob &job,
|
||||
Texture *destination, VideoParams destination_params,
|
||||
bool clear_destination) override;
|
||||
// Allocates a native texture through the dynamic backend.
|
||||
virtual QVariant create_native_texture(int width, int height, int depth,
|
||||
PixelFormat format, int channel_count,
|
||||
const void *data = nullptr,
|
||||
int linesize = 0) override;
|
||||
// Releases a native texture through the dynamic backend.
|
||||
virtual void destroy_native_texture(QVariant texture) override;
|
||||
// Releases backend-owned renderer resources.
|
||||
virtual void destroy_internal() override;
|
||||
|
||||
private:
|
||||
// Resolves required backend C ABI symbols.
|
||||
bool resolve_functions();
|
||||
// Replaces a failed Vulkan backend with OpenGL.
|
||||
bool fallback_to_open_gl();
|
||||
// Clears all cached function pointers.
|
||||
void reset_functions();
|
||||
// Resolves the private backend library path.
|
||||
QString library_filename() const;
|
||||
|
||||
QString backend_;
|
||||
QLibrary library_;
|
||||
OakRenderBackendHandle handle_ = nullptr;
|
||||
|
||||
OakBackendCreateFn create_ = nullptr;
|
||||
OakBackendDestroyFn destroy_ = nullptr;
|
||||
OakBackendGetInfoFn get_info_ = nullptr;
|
||||
OakBackendIsAvailableFn is_available_ = nullptr;
|
||||
OakBackendInitFn init_ = nullptr;
|
||||
OakBackendInitWithContextFn init_with_context_ = nullptr;
|
||||
OakBackendPostInitFn post_init_ = nullptr;
|
||||
OakBackendPostDestroyFn post_destroy_ = nullptr;
|
||||
OakBackendDestroyInternalFn destroy_internal_ = nullptr;
|
||||
OakBackendClearDestinationFn clear_destination_ = nullptr;
|
||||
OakBackendCreateNativeTextureFn create_native_texture_ = nullptr;
|
||||
OakBackendDestroyNativeTextureFn destroy_native_texture_ = nullptr;
|
||||
OakBackendCreateNativeShaderFn create_native_shader_ = nullptr;
|
||||
OakBackendDestroyNativeShaderFn destroy_native_shader_ = nullptr;
|
||||
OakBackendUploadToTextureFn upload_to_texture_ = nullptr;
|
||||
OakBackendDownloadFromTextureFn download_from_texture_ = nullptr;
|
||||
OakBackendFlushFn flush_ = nullptr;
|
||||
OakBackendGetPixelFromTextureFn get_pixel_from_texture_ = nullptr;
|
||||
OakBackendBlitFn blit_ = nullptr;
|
||||
OakBackendAttachOutputTextureFn attach_output_texture_ = nullptr;
|
||||
OakBackendDetachOutputTextureFn detach_output_texture_ = nullptr;
|
||||
OakBackendOpenGLContextFn opengl_context_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_DYNAMICRENDERER_H
|
||||
@@ -0,0 +1,121 @@
|
||||
#ifndef OAK_RENDERBACKEND_C_H
|
||||
#define OAK_RENDERBACKEND_C_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define OAK_RENDER_BACKEND_EXPORT extern "C" __declspec(dllexport)
|
||||
#else
|
||||
#define OAK_RENDER_BACKEND_EXPORT \
|
||||
extern "C" __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Opaque pointer to the backend-owned C++ renderer object. */
|
||||
typedef void *OakRenderBackendHandle;
|
||||
|
||||
/* Identifies the concrete backend behind a dynamically loaded library. */
|
||||
enum OakRenderBackendKind {
|
||||
oak_render_backend_unknown = 0,
|
||||
oak_render_backend_opengl = 1,
|
||||
oak_render_backend_vulkan = 2
|
||||
};
|
||||
|
||||
/* Capability bits advertised by a backend through oak_renderer_get_info(). */
|
||||
enum OakRenderBackendCapability {
|
||||
oak_render_backend_cap_textures = 1ULL << 0,
|
||||
oak_render_backend_cap_shaders = 1ULL << 1,
|
||||
oak_render_backend_cap_blit = 1ULL << 2,
|
||||
oak_render_backend_cap_readback = 1ULL << 3,
|
||||
oak_render_backend_cap_viewer_context = 1ULL << 4,
|
||||
oak_render_backend_cap_instance = 1ULL << 5,
|
||||
oak_render_backend_cap_device = 1ULL << 6
|
||||
};
|
||||
|
||||
/* Static and runtime metadata returned by the backend. */
|
||||
struct OakRenderBackendInfo {
|
||||
uint32_t abi_version;
|
||||
uint32_t kind;
|
||||
uint64_t capabilities;
|
||||
const char *name;
|
||||
const char *status;
|
||||
};
|
||||
|
||||
/* Creates a backend renderer object. */
|
||||
typedef OakRenderBackendHandle (*OakBackendCreateFn)(void *parent);
|
||||
/* Destroys a backend renderer object created by OakBackendCreateFn. */
|
||||
typedef void (*OakBackendDestroyFn)(OakRenderBackendHandle handle);
|
||||
/* Queries backend metadata and capability bits. */
|
||||
typedef bool (*OakBackendGetInfoFn)(OakRenderBackendHandle handle,
|
||||
struct OakRenderBackendInfo *out_info);
|
||||
/* Checks whether the backend can run on the current machine. */
|
||||
typedef bool (*OakBackendIsAvailableFn)(OakRenderBackendHandle handle);
|
||||
/* Initializes backend-owned device/context resources. */
|
||||
typedef bool (*OakBackendInitFn)(OakRenderBackendHandle handle);
|
||||
/* Initializes the backend against a caller-supplied GL context when applicable. */
|
||||
typedef void (*OakBackendInitWithContextFn)(OakRenderBackendHandle handle,
|
||||
void *context);
|
||||
/* Runs backend post-initialization after the device/context exists. */
|
||||
typedef void (*OakBackendPostInitFn)(OakRenderBackendHandle handle);
|
||||
/* Runs backend post-destroy cleanup before the library unloads. */
|
||||
typedef void (*OakBackendPostDestroyFn)(OakRenderBackendHandle handle);
|
||||
/* Destroys renderer-owned native resources. */
|
||||
typedef void (*OakBackendDestroyInternalFn)(OakRenderBackendHandle handle);
|
||||
/* Clears a texture destination or implicit output target. */
|
||||
typedef void (*OakBackendClearDestinationFn)(OakRenderBackendHandle handle,
|
||||
void *texture, double r, double g,
|
||||
double b, double a);
|
||||
/* Creates a native texture and writes a QVariant-compatible handle. */
|
||||
typedef void (*OakBackendCreateNativeTextureFn)(
|
||||
OakRenderBackendHandle handle, int width, int height, int depth, int format,
|
||||
int channel_count, const void *data, int linesize, void *out_variant);
|
||||
/* Destroys a native texture represented by a QVariant-compatible handle. */
|
||||
typedef void (*OakBackendDestroyNativeTextureFn)(OakRenderBackendHandle handle,
|
||||
const void *variant);
|
||||
/* Creates a native shader and writes a QVariant-compatible handle. */
|
||||
typedef void (*OakBackendCreateNativeShaderFn)(OakRenderBackendHandle handle,
|
||||
const void *shader_code,
|
||||
void *out_variant);
|
||||
/* Destroys a native shader represented by a QVariant-compatible handle. */
|
||||
typedef void (*OakBackendDestroyNativeShaderFn)(OakRenderBackendHandle handle,
|
||||
const void *variant);
|
||||
/* Uploads CPU pixel data to a native texture. */
|
||||
typedef void (*OakBackendUploadToTextureFn)(OakRenderBackendHandle handle,
|
||||
const void *variant,
|
||||
const void *video_params,
|
||||
const void *data, int linesize);
|
||||
/* Downloads native texture pixels into caller-owned CPU memory. */
|
||||
typedef void (*OakBackendDownloadFromTextureFn)(OakRenderBackendHandle handle,
|
||||
const void *variant,
|
||||
const void *video_params,
|
||||
void *data, int linesize);
|
||||
/* Waits for backend work that must be visible to later operations. */
|
||||
typedef void (*OakBackendFlushFn)(OakRenderBackendHandle handle);
|
||||
/* Reads one pixel from a texture. */
|
||||
typedef void (*OakBackendGetPixelFromTextureFn)(OakRenderBackendHandle handle,
|
||||
void *texture,
|
||||
const void *point,
|
||||
void *out_color);
|
||||
/* Executes a shader blit job. */
|
||||
typedef void (*OakBackendBlitFn)(OakRenderBackendHandle handle,
|
||||
const void *shader, void *job,
|
||||
void *destination,
|
||||
const void *destination_params,
|
||||
bool clear_destination);
|
||||
/* Attaches an output texture for OFX OpenGL rendering when supported. */
|
||||
typedef void (*OakBackendAttachOutputTextureFn)(OakRenderBackendHandle handle,
|
||||
const void *texture_id);
|
||||
/* Detaches an OFX output texture when supported. */
|
||||
typedef void (*OakBackendDetachOutputTextureFn)(OakRenderBackendHandle handle);
|
||||
/* Returns the backend OpenGL context, or null for non-OpenGL backends. */
|
||||
typedef void *(*OakBackendOpenGLContextFn)(OakRenderBackendHandle handle);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // OAK_RENDERBACKEND_C_H
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Oak Video Editor - Non-Linear Video Editor
|
||||
* Copyright (C) 2025 Olive CE 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 OAK_CANCELATOM_H
|
||||
#define OAK_CANCELATOM_H
|
||||
|
||||
#include <QMutex>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class CancelAtom {
|
||||
public:
|
||||
CancelAtom()
|
||||
: cancelled_(false)
|
||||
, heard_(false)
|
||||
{
|
||||
}
|
||||
|
||||
bool is_cancelled()
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
if (cancelled_) {
|
||||
heard_ = true;
|
||||
}
|
||||
return cancelled_;
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
cancelled_ = true;
|
||||
}
|
||||
|
||||
bool heard_cancel()
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
return heard_;
|
||||
}
|
||||
|
||||
private:
|
||||
QMutex mutex_;
|
||||
|
||||
bool cancelled_;
|
||||
|
||||
bool heard_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_CANCELATOM_H
|
||||
@@ -0,0 +1,230 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "renderer.h"
|
||||
|
||||
#include "common/filefunctions.h"
|
||||
#include "node/node.h"
|
||||
#include "render/colorprocessor.h"
|
||||
#include "render/job/colortransformjob.h"
|
||||
#include "render/job/shaderjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
bool Renderer::get_color_context(const ColorTransformJob &color_job,
|
||||
Renderer::ColorContext *ctx)
|
||||
{
|
||||
QMutexLocker locker(&color_cache_mutex_);
|
||||
|
||||
ColorContext &color_ctx = *ctx;
|
||||
|
||||
QString proc_id = color_job.id();
|
||||
|
||||
if (color_cache_.contains(proc_id)) {
|
||||
color_ctx = color_cache_.value(proc_id);
|
||||
return true;
|
||||
} else {
|
||||
locker.unlock();
|
||||
|
||||
// Create shader description
|
||||
QString ocio_func_name;
|
||||
if (color_job.get_function_name().isEmpty()) {
|
||||
ocio_func_name = "OCIODisplay";
|
||||
} else {
|
||||
ocio_func_name = color_job.get_function_name();
|
||||
}
|
||||
auto shader_desc = ocio::GpuShaderDesc::CreateShaderDesc();
|
||||
shader_desc->setLanguage(ocio::GPU_LANGUAGE_GLSL_ES_3_0);
|
||||
shader_desc->setFunctionName(ocio_func_name.toUtf8());
|
||||
shader_desc->setResourcePrefix("ocio_");
|
||||
|
||||
// Generate shader
|
||||
color_job.get_color_processor()
|
||||
->get_processor()
|
||||
->getDefaultGPUProcessor()
|
||||
->extractGpuShaderInfo(shader_desc);
|
||||
|
||||
ShaderCode code;
|
||||
if (const Node *shader_src = color_job.custom_shader_source()) {
|
||||
// Use shader code from associated node
|
||||
code = shader_src->get_shader_code(
|
||||
{ color_job.custom_shader_id(), shader_desc->getShaderText() });
|
||||
} else {
|
||||
// Generate shader code using OCIO stub and our auto-generated name
|
||||
code = FileFunctions::read_file_as_string(
|
||||
QStringLiteral(":/shaders/colormanage.frag"));
|
||||
code.set_frag_code(
|
||||
code.frag_code().arg(shader_desc->getShaderText()));
|
||||
}
|
||||
|
||||
// Try to compile shader
|
||||
color_ctx.compiled_shader = create_native_shader(code);
|
||||
|
||||
if (color_ctx.compiled_shader.isNull()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
color_ctx.lut3d_textures.resize(shader_desc->getNum3DTextures());
|
||||
for (unsigned int i = 0; i < shader_desc->getNum3DTextures(); i++) {
|
||||
const char *tex_name = nullptr;
|
||||
const char *sampler_name = nullptr;
|
||||
unsigned int edge_len = 0;
|
||||
ocio::Interpolation interpolation = ocio::INTERP_LINEAR;
|
||||
|
||||
shader_desc->get3DTexture(i, tex_name, sampler_name, edge_len,
|
||||
interpolation);
|
||||
|
||||
if (!tex_name || !*tex_name || !sampler_name || !*sampler_name ||
|
||||
!edge_len) {
|
||||
qCritical() << "3D LUT texture data is corrupted";
|
||||
return false;
|
||||
}
|
||||
|
||||
const float *values = nullptr;
|
||||
shader_desc->get3DTextureValues(i, values);
|
||||
if (!values) {
|
||||
qCritical() << "3D LUT texture values are missing";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate 3D LUT
|
||||
color_ctx.lut3d_textures[i].texture = create_texture(
|
||||
VideoParams(edge_len, edge_len, edge_len, PixelFormat::f32,
|
||||
VideoParams::k_rgb_channel_count),
|
||||
values);
|
||||
color_ctx.lut3d_textures[i].name = sampler_name;
|
||||
color_ctx.lut3d_textures[i].interpolation =
|
||||
(interpolation == ocio::INTERP_NEAREST) ? Texture::k_nearest :
|
||||
Texture::k_linear;
|
||||
}
|
||||
|
||||
color_ctx.lut1d_textures.resize(shader_desc->getNumTextures());
|
||||
for (unsigned int i = 0; i < shader_desc->getNumTextures(); i++) {
|
||||
const char *tex_name = nullptr;
|
||||
const char *sampler_name = nullptr;
|
||||
unsigned int width = 0, height = 0;
|
||||
ocio::GpuShaderDesc::TextureType channel =
|
||||
ocio::GpuShaderDesc::TEXTURE_RGB_CHANNEL;
|
||||
ocio::Interpolation interpolation = ocio::INTERP_LINEAR;
|
||||
#if OCIO_VERSION_MAJOR > 2 || \
|
||||
(OCIO_VERSION_MAJOR == 2 && OCIO_VERSION_MINOR >= 3)
|
||||
ocio::GpuShaderDesc::TextureDimensions dimensions =
|
||||
ocio::GpuShaderDesc::TEXTURE_2D;
|
||||
shader_desc->getTexture(i, tex_name, sampler_name, width, height,
|
||||
channel, dimensions, interpolation);
|
||||
#else
|
||||
shader_desc->getTexture(i, tex_name, sampler_name, width, height,
|
||||
channel, interpolation);
|
||||
#endif
|
||||
|
||||
if (!tex_name || !*tex_name || !sampler_name || !*sampler_name ||
|
||||
!width) {
|
||||
qCritical() << "1D LUT texture data is corrupted";
|
||||
return false;
|
||||
}
|
||||
|
||||
const float *values = nullptr;
|
||||
shader_desc->getTextureValues(i, values);
|
||||
if (!values) {
|
||||
qCritical() << "1D LUT texture values are missing";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate 1D LUT
|
||||
int lut_channels =
|
||||
(channel == ocio::GpuShaderDesc::TEXTURE_RED_CHANNEL) ?
|
||||
1 :
|
||||
VideoParams::k_rgb_channel_count;
|
||||
VideoParams lut_params(width, height, PixelFormat::f32,
|
||||
lut_channels);
|
||||
color_ctx.lut1d_textures[i].texture =
|
||||
create_texture(lut_params, values);
|
||||
color_ctx.lut1d_textures[i].name = sampler_name;
|
||||
color_ctx.lut1d_textures[i].interpolation =
|
||||
(interpolation == ocio::INTERP_NEAREST) ? Texture::k_nearest :
|
||||
Texture::k_linear;
|
||||
}
|
||||
|
||||
locker.relock();
|
||||
color_cache_.insert(proc_id, color_ctx);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::blit_color_managed(const ColorTransformJob &color_job,
|
||||
Texture *destination, const VideoParams ¶ms)
|
||||
{
|
||||
ColorContext color_ctx;
|
||||
if (!get_color_context(color_job, &color_ctx)) {
|
||||
ShaderJob fallback_job;
|
||||
fallback_job.insert(QStringLiteral("ove_maintex"),
|
||||
color_job.get_input_texture());
|
||||
fallback_job.insert(QStringLiteral("ove_mvpmat"),
|
||||
NodeValue(NodeValue::k_matrix,
|
||||
color_job.get_transform_matrix()));
|
||||
|
||||
if (destination) {
|
||||
blit_to_texture(get_default_shader(), fallback_job, destination,
|
||||
color_job.is_clear_destination_enabled());
|
||||
} else {
|
||||
blit(get_default_shader(), fallback_job, params,
|
||||
color_job.is_clear_destination_enabled());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ShaderJob job;
|
||||
job.insert(QStringLiteral("ove_maintex"), color_job.get_input_texture());
|
||||
job.insert(QStringLiteral("ove_mvpmat"),
|
||||
NodeValue(NodeValue::k_matrix, color_job.get_transform_matrix()));
|
||||
job.insert(QStringLiteral("ove_cropmatrix"),
|
||||
NodeValue(NodeValue::k_matrix,
|
||||
color_job.get_crop_matrix().inverted()));
|
||||
job.insert(QStringLiteral("ove_maintex_alpha"),
|
||||
NodeValue(NodeValue::k_int,
|
||||
int(color_job.get_input_alpha_association())));
|
||||
job.insert(QStringLiteral("ove_force_opaque"),
|
||||
NodeValue(NodeValue::k_boolean, color_job.get_force_opaque()));
|
||||
job.insert(color_job.get_values());
|
||||
|
||||
foreach (const ColorContext::LUT &l, color_ctx.lut3d_textures) {
|
||||
job.insert(l.name, NodeValue(NodeValue::k_texture,
|
||||
QVariant::fromValue(l.texture)));
|
||||
job.set_interpolation(l.name, l.interpolation);
|
||||
}
|
||||
foreach (const ColorContext::LUT &l, color_ctx.lut1d_textures) {
|
||||
job.insert(l.name, NodeValue(NodeValue::k_texture,
|
||||
QVariant::fromValue(l.texture)));
|
||||
job.set_interpolation(l.name, l.interpolation);
|
||||
}
|
||||
|
||||
if (destination) {
|
||||
blit_to_texture(color_ctx.compiled_shader, job, destination,
|
||||
color_job.is_clear_destination_enabled());
|
||||
} else {
|
||||
blit(color_ctx.compiled_shader, job, params,
|
||||
color_job.is_clear_destination_enabled());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "colorprocessor.h"
|
||||
|
||||
#include "common/define.h"
|
||||
#include "common/ocioutils.h"
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
ColorProcessor::ColorProcessor(ColorManager *config, const QString &input,
|
||||
const ColorTransform &transform,
|
||||
Direction direction)
|
||||
{
|
||||
processor_ = nullptr;
|
||||
cpu_processor_ = nullptr;
|
||||
|
||||
try {
|
||||
// Resolve role names (e.g. "scene_linear") to canonical colorspace names
|
||||
// so they can be passed to getProcessor()/DisplayViewTransform.
|
||||
QString resolved_input = input;
|
||||
ocio::ConstConfigRcPtr ocio_config = config->get_config();
|
||||
if (ocio_config && ocio_config->hasRole(input.toUtf8())) {
|
||||
resolved_input = ocio_config->getCanonicalName(input.toUtf8());
|
||||
}
|
||||
|
||||
const QString &output = (transform.output().isEmpty()) ?
|
||||
config->get_default_display() :
|
||||
transform.output();
|
||||
|
||||
if (transform.is_display()) {
|
||||
const QString &view = (transform.view().isEmpty()) ?
|
||||
config->get_default_view(output) :
|
||||
transform.view();
|
||||
|
||||
auto display_transform = ocio::DisplayViewTransform::Create();
|
||||
|
||||
display_transform->setSrc(resolved_input.toUtf8());
|
||||
display_transform->setDisplay(output.toUtf8());
|
||||
display_transform->setView(view.toUtf8());
|
||||
display_transform->setDirection(direction == k_normal ?
|
||||
ocio::TRANSFORM_DIR_FORWARD :
|
||||
ocio::TRANSFORM_DIR_INVERSE);
|
||||
|
||||
if (transform.look().isEmpty()) {
|
||||
processor_ = ocio_config->getProcessor(display_transform);
|
||||
} else {
|
||||
auto group = ocio::GroupTransform::Create();
|
||||
|
||||
const char *out_cs =
|
||||
ocio::LookTransform::GetLooksResultColorSpace(
|
||||
ocio_config, ocio_config->getCurrentContext(),
|
||||
transform.look().toUtf8());
|
||||
|
||||
auto lt = ocio::LookTransform::Create();
|
||||
lt->setSrc(resolved_input.toUtf8());
|
||||
lt->setDst(out_cs);
|
||||
lt->setLooks(transform.look().toUtf8());
|
||||
lt->setSkipColorSpaceConversion(false);
|
||||
group->appendTransform(lt);
|
||||
|
||||
display_transform->setSrc(out_cs);
|
||||
group->appendTransform(display_transform);
|
||||
|
||||
processor_ = ocio_config->getProcessor(group);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (direction == k_normal) {
|
||||
processor_ = ocio_config->getProcessor(resolved_input.toUtf8(),
|
||||
output.toUtf8());
|
||||
} else {
|
||||
processor_ = ocio_config->getProcessor(output.toUtf8(),
|
||||
resolved_input.toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
if (processor_) {
|
||||
cpu_processor_ = processor_->getDefaultCPUProcessor();
|
||||
}
|
||||
} catch (ocio::Exception &e) {
|
||||
qWarning() << "ColorProcessor exception:" << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
ColorProcessor::ColorProcessor(ocio::ConstProcessorRcPtr processor)
|
||||
{
|
||||
processor_ = processor;
|
||||
cpu_processor_ = processor_ ? processor_->getDefaultCPUProcessor() :
|
||||
nullptr;
|
||||
}
|
||||
|
||||
void ColorProcessor::convert_frame(Frame *f)
|
||||
{
|
||||
if (!cpu_processor_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ocio::BitDepth ocio_bit_depth =
|
||||
OCIOUtils::get_ocio_bit_depth_from_pixel_format(f->format());
|
||||
|
||||
if (ocio_bit_depth == ocio::BIT_DEPTH_UNKNOWN) {
|
||||
qCritical() << "Tried to color convert frame with no format";
|
||||
return;
|
||||
}
|
||||
|
||||
ocio::PackedImageDesc img(f->data(), f->width(), f->height(),
|
||||
f->channel_count(), ocio_bit_depth,
|
||||
ocio::AutoStride, ocio::AutoStride,
|
||||
f->linesize_bytes());
|
||||
|
||||
cpu_processor_->apply(img);
|
||||
}
|
||||
|
||||
Color ColorProcessor::convert_color(const Color &in)
|
||||
{
|
||||
if (!cpu_processor_) {
|
||||
return in;
|
||||
}
|
||||
|
||||
// I've been bamboozled
|
||||
float c[4] = { float(in.red()), float(in.green()), float(in.blue()),
|
||||
float(in.alpha()) };
|
||||
|
||||
cpu_processor_->applyRGBA(c);
|
||||
|
||||
return Color(c[0], c[1], c[2], c[3]);
|
||||
}
|
||||
|
||||
ColorProcessorPtr ColorProcessor::create(ColorManager *config,
|
||||
const QString &input,
|
||||
const ColorTransform &transform,
|
||||
Direction direction)
|
||||
{
|
||||
return std::make_shared<ColorProcessor>(config, input, transform,
|
||||
direction);
|
||||
}
|
||||
|
||||
ColorProcessorPtr ColorProcessor::create(ocio::ConstProcessorRcPtr processor)
|
||||
{
|
||||
return std::make_shared<ColorProcessor>(processor);
|
||||
}
|
||||
|
||||
ocio::ConstProcessorRcPtr ColorProcessor::get_processor()
|
||||
{
|
||||
return processor_;
|
||||
}
|
||||
|
||||
void ColorProcessor::convert_frame(FramePtr f)
|
||||
{
|
||||
convert_frame(f.get());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_COLORPROCESSOR_H
|
||||
#define OAK_COLORPROCESSOR_H
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "common/ocioutils.h"
|
||||
#include "render/colortransform.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class ColorManager;
|
||||
|
||||
class ColorProcessor;
|
||||
using ColorProcessorPtr = std::shared_ptr<ColorProcessor>;
|
||||
|
||||
class ColorProcessor {
|
||||
public:
|
||||
enum Direction { k_normal, k_inverse };
|
||||
|
||||
ColorProcessor(ColorManager *config, const QString &input,
|
||||
const ColorTransform &dest_space,
|
||||
Direction direction = k_normal);
|
||||
ColorProcessor(ocio::ConstProcessorRcPtr processor);
|
||||
|
||||
DISABLE_COPY_MOVE(ColorProcessor)
|
||||
|
||||
static ColorProcessorPtr create(ColorManager *config, const QString &input,
|
||||
const ColorTransform &dest_space,
|
||||
Direction direction = k_normal);
|
||||
static ColorProcessorPtr create(ocio::ConstProcessorRcPtr processor);
|
||||
|
||||
ocio::ConstProcessorRcPtr get_processor();
|
||||
|
||||
void convert_frame(FramePtr f);
|
||||
void convert_frame(Frame *f);
|
||||
|
||||
Color convert_color(const Color &in);
|
||||
|
||||
const char *id() const
|
||||
{
|
||||
return processor_->getCacheID();
|
||||
}
|
||||
|
||||
private:
|
||||
ocio::ConstProcessorRcPtr processor_;
|
||||
|
||||
ocio::ConstCPUProcessorRcPtr cpu_processor_;
|
||||
};
|
||||
|
||||
using ColorProcessorChain = QVector<ColorProcessorPtr>;
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(olive::ColorProcessorPtr)
|
||||
|
||||
#endif // OAK_COLORPROCESSOR_H
|
||||
@@ -0,0 +1,34 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_COLORPROCESSORCACHE_H
|
||||
#define OAK_COLORPROCESSORCACHE_H
|
||||
|
||||
#include "render/colorprocessor.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
using ColorProcessorCache = QHash<QString, ColorProcessorPtr>;
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_COLORPROCESSORCACHE_H
|
||||
@@ -0,0 +1,92 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_COLORTRANSFORM_H
|
||||
#define OAK_COLORTRANSFORM_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "common/define.h"
|
||||
#include "common/ocioutils.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class ColorTransform {
|
||||
public:
|
||||
ColorTransform()
|
||||
{
|
||||
is_display_ = false;
|
||||
}
|
||||
|
||||
ColorTransform(const QString &output)
|
||||
{
|
||||
is_display_ = false;
|
||||
output_ = output;
|
||||
}
|
||||
|
||||
ColorTransform(const QString &display, const QString &view,
|
||||
const QString &look)
|
||||
{
|
||||
is_display_ = true;
|
||||
output_ = display;
|
||||
view_ = view;
|
||||
look_ = look;
|
||||
}
|
||||
|
||||
bool is_display() const
|
||||
{
|
||||
return is_display_;
|
||||
}
|
||||
|
||||
const QString &display() const
|
||||
{
|
||||
return output_;
|
||||
}
|
||||
|
||||
const QString &output() const
|
||||
{
|
||||
return output_;
|
||||
}
|
||||
|
||||
const QString &view() const
|
||||
{
|
||||
return view_;
|
||||
}
|
||||
|
||||
const QString &look() const
|
||||
{
|
||||
return look_;
|
||||
}
|
||||
|
||||
private:
|
||||
QString output_;
|
||||
|
||||
bool is_display_;
|
||||
QString view_;
|
||||
QString look_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(olive::ColorTransform)
|
||||
|
||||
#endif // OAK_COLORTRANSFORM_H
|
||||
@@ -0,0 +1,434 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "diskmanager.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include "common/filefunctions.h"
|
||||
#include "config/config.h"
|
||||
#include "coreengine.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
DiskManager *DiskManager::instance_ = nullptr;
|
||||
|
||||
DiskManager::ShowDiskCacheSettingsHandler
|
||||
DiskManager::show_disk_cache_settings_handler_;
|
||||
|
||||
DiskManager::DiskManager()
|
||||
{
|
||||
// Add default cache location
|
||||
QFile default_disk_cache_file(get_default_disk_cache_config_file());
|
||||
if (default_disk_cache_file.open(QFile::ReadOnly)) {
|
||||
QString default_dir = default_disk_cache_file.readAll();
|
||||
|
||||
if (!default_dir.isEmpty()) {
|
||||
if (FileFunctions::directory_is_valid(default_dir)) {
|
||||
get_open_folder(default_dir);
|
||||
} else {
|
||||
QMessageBox::warning(
|
||||
nullptr, tr("Disk Cache Error"),
|
||||
tr("Unable to set custom application disk cache. Using default instead."));
|
||||
}
|
||||
}
|
||||
|
||||
default_disk_cache_file.close();
|
||||
}
|
||||
|
||||
// If no custom default was loaded, load default
|
||||
if (open_folders_.isEmpty()) {
|
||||
get_open_folder(get_default_disk_cache_path());
|
||||
}
|
||||
|
||||
QFile disk_cache_index(QDir(FileFunctions::get_configuration_location())
|
||||
.filePath(QStringLiteral("diskcache2")));
|
||||
|
||||
if (disk_cache_index.open(QFile::ReadOnly)) {
|
||||
QTextStream stream(&disk_cache_index);
|
||||
|
||||
QString line;
|
||||
while (stream.readLineInto(&line)) {
|
||||
get_open_folder(line);
|
||||
}
|
||||
|
||||
disk_cache_index.close();
|
||||
}
|
||||
}
|
||||
|
||||
DiskManager::~DiskManager()
|
||||
{
|
||||
QFile default_disk_cache_file(get_default_disk_cache_config_file());
|
||||
if (default_disk_cache_file.open(QFile::WriteOnly)) {
|
||||
if (get_default_disk_cache_path() != get_default_cache_path()) {
|
||||
default_disk_cache_file.write(get_default_cache_path().toUtf8());
|
||||
}
|
||||
|
||||
default_disk_cache_file.close();
|
||||
}
|
||||
}
|
||||
|
||||
void DiskManager::create_instance()
|
||||
{
|
||||
instance_ = new DiskManager();
|
||||
}
|
||||
|
||||
void DiskManager::destroy_instance()
|
||||
{
|
||||
delete instance_;
|
||||
instance_ = nullptr;
|
||||
}
|
||||
|
||||
DiskManager *DiskManager::instance()
|
||||
{
|
||||
return instance_;
|
||||
}
|
||||
|
||||
void DiskManager::accessed(const QString &cache_folder, const QString &filename)
|
||||
{
|
||||
DiskCacheFolder *f = get_open_folder(cache_folder);
|
||||
|
||||
f->accessed(filename);
|
||||
}
|
||||
|
||||
void DiskManager::created_file(const QString &cache_folder,
|
||||
const QString &filename)
|
||||
{
|
||||
DiskCacheFolder *f = get_open_folder(cache_folder);
|
||||
|
||||
f->created_file(filename);
|
||||
}
|
||||
|
||||
void DiskManager::delete_specific_file(const QString &filename)
|
||||
{
|
||||
foreach (DiskCacheFolder *f, open_folders_) {
|
||||
f->delete_specific_file(filename);
|
||||
}
|
||||
}
|
||||
|
||||
bool DiskManager::clear_disk_cache(const QString &cache_folder)
|
||||
{
|
||||
DiskCacheFolder *f = get_open_folder(cache_folder);
|
||||
|
||||
return f->clear_cache();
|
||||
}
|
||||
|
||||
DiskCacheFolder *DiskManager::get_open_folder(const QString &path)
|
||||
{
|
||||
// If path is empty, this must mean default
|
||||
if (path.isEmpty()) {
|
||||
return get_default_cache_folder();
|
||||
}
|
||||
|
||||
// See if we have an existing path with this name
|
||||
foreach (DiskCacheFolder *f, open_folders_) {
|
||||
if (f->get_path() == path) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
||||
// We must have to open this folder
|
||||
DiskCacheFolder *f = new DiskCacheFolder(path, this);
|
||||
connect(f, &DiskCacheFolder::deleted_frame, this,
|
||||
&DiskManager::deleted_frame);
|
||||
open_folders_.append(f);
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
bool DiskManager::show_disk_cache_change_confirmation_dialog(QWidget *parent)
|
||||
{
|
||||
return (
|
||||
QMessageBox::question(
|
||||
parent, tr("Disk Cache"),
|
||||
tr("You've chosen to change the default disk cache location. This "
|
||||
"will invalidate your current cache. Would you like to continue?"),
|
||||
QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok);
|
||||
}
|
||||
|
||||
QString DiskManager::get_default_disk_cache_config_file()
|
||||
{
|
||||
return QDir(FileFunctions::get_configuration_location())
|
||||
.filePath(QStringLiteral("defaultdiskcache"));
|
||||
}
|
||||
|
||||
QString DiskManager::get_default_disk_cache_path()
|
||||
{
|
||||
return QDir(QStandardPaths::writableLocation(
|
||||
QStandardPaths::AppLocalDataLocation))
|
||||
.filePath("mediacache");
|
||||
}
|
||||
|
||||
void DiskManager::set_show_disk_cache_settings_handler(
|
||||
ShowDiskCacheSettingsHandler handler)
|
||||
{
|
||||
show_disk_cache_settings_handler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void DiskManager::show_disk_cache_settings_dialog(DiskCacheFolder *folder,
|
||||
QWidget *parent)
|
||||
{
|
||||
if (show_disk_cache_settings_handler_) {
|
||||
show_disk_cache_settings_handler_(folder, parent);
|
||||
return;
|
||||
}
|
||||
|
||||
qWarning() << "No disk cache settings dialog handler registered, skipping";
|
||||
}
|
||||
|
||||
void DiskManager::show_disk_cache_settings_dialog(const QString &path,
|
||||
QWidget *parent)
|
||||
{
|
||||
if (!FileFunctions::directory_is_valid(path)) {
|
||||
QMessageBox::critical(
|
||||
parent, tr("Disk Cache Error"),
|
||||
tr("Failed to open disk cache at \"%1\". Try a different folder.")
|
||||
.arg(path));
|
||||
return;
|
||||
}
|
||||
|
||||
DiskCacheFolder *folder = get_open_folder(path);
|
||||
|
||||
show_disk_cache_settings_dialog(folder, parent);
|
||||
}
|
||||
|
||||
DiskCacheFolder::DiskCacheFolder(const QString &path, QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
set_path(path);
|
||||
|
||||
save_timer_.setInterval(OAK_CONFIG("DiskCacheSaveInterval").toInt());
|
||||
connect(&save_timer_, &QTimer::timeout, this,
|
||||
&DiskCacheFolder::save_disk_cache_index);
|
||||
save_timer_.start();
|
||||
}
|
||||
|
||||
DiskCacheFolder::~DiskCacheFolder()
|
||||
{
|
||||
close_cache_folder();
|
||||
}
|
||||
|
||||
bool DiskCacheFolder::clear_cache()
|
||||
{
|
||||
bool deleted_files = true;
|
||||
|
||||
auto i = disk_data_.begin();
|
||||
|
||||
while (i != disk_data_.end()) {
|
||||
// We return a false result if any of the files fail to delete, but still try to delete as many as we can
|
||||
QString filename = i.key();
|
||||
|
||||
if (QFile::remove(filename) || !QFileInfo::exists(filename)) {
|
||||
emit deleted_frame(path_, filename);
|
||||
i = disk_data_.erase(i);
|
||||
} else {
|
||||
qWarning() << "Failed to delete" << filename;
|
||||
deleted_files = false;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return deleted_files;
|
||||
}
|
||||
|
||||
void DiskCacheFolder::accessed(const QString &filename)
|
||||
{
|
||||
if (!disk_data_.contains(filename)) {
|
||||
return;
|
||||
}
|
||||
|
||||
disk_data_[filename].access_time = QDateTime::currentMSecsSinceEpoch();
|
||||
}
|
||||
|
||||
void DiskCacheFolder::created_file(const QString &filename)
|
||||
{
|
||||
qint64 file_size = QFile(filename).size();
|
||||
|
||||
disk_data_.insert(filename,
|
||||
{ file_size, QDateTime::currentMSecsSinceEpoch() });
|
||||
|
||||
consumption_ += file_size;
|
||||
|
||||
while (consumption_ > limit_) {
|
||||
delete_least_recent();
|
||||
}
|
||||
}
|
||||
|
||||
void DiskCacheFolder::set_path(const QString &path)
|
||||
{
|
||||
// If this is currently set to a folder, close it out now
|
||||
close_cache_folder();
|
||||
|
||||
// Signal that disk cache is gone
|
||||
if (!disk_data_.empty()) {
|
||||
for (auto it = disk_data_.cbegin(); it != disk_data_.cend(); it++) {
|
||||
emit deleted_frame(path_, it.key());
|
||||
}
|
||||
disk_data_.clear();
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
clear_on_close_ = false;
|
||||
consumption_ = 0;
|
||||
limit_ = 21474836480; // Default to 20 GB
|
||||
|
||||
// Set path
|
||||
path_ = path;
|
||||
|
||||
// Attempt to load existing index file from path
|
||||
QDir path_dir(path_);
|
||||
FileFunctions::directory_is_valid(path_dir);
|
||||
|
||||
index_path_ = path_dir.filePath(QStringLiteral("index"));
|
||||
|
||||
// Try to load any current cache index from file
|
||||
QFile cache_index_file(index_path_);
|
||||
|
||||
if (cache_index_file.open(QFile::ReadOnly)) {
|
||||
QDataStream ds(&cache_index_file);
|
||||
|
||||
ds >> limit_;
|
||||
ds >> clear_on_close_;
|
||||
|
||||
while (!cache_index_file.atEnd()) {
|
||||
QString filename;
|
||||
HashTime h;
|
||||
|
||||
ds >> filename;
|
||||
ds >> h.file_size;
|
||||
ds >> h.access_time;
|
||||
|
||||
if (QFileInfo::exists(filename)) {
|
||||
consumption_ += h.file_size;
|
||||
disk_data_.insert(filename, h);
|
||||
}
|
||||
}
|
||||
|
||||
cache_index_file.close();
|
||||
}
|
||||
}
|
||||
|
||||
bool DiskCacheFolder::delete_file_internal(
|
||||
QMap<QString, HashTime>::iterator hash_to_delete)
|
||||
{
|
||||
// Cache HashTime object
|
||||
QString filename = hash_to_delete.key();
|
||||
HashTime ht = hash_to_delete.value();
|
||||
|
||||
// Remove from disk
|
||||
QFile f(filename);
|
||||
|
||||
if (!f.exists() || f.remove()) {
|
||||
// Remove from internal map
|
||||
disk_data_.erase(hash_to_delete);
|
||||
|
||||
// Reduce consumption
|
||||
consumption_ -= ht.file_size;
|
||||
|
||||
emit deleted_frame(path_, filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DiskCacheFolder::delete_specific_file(const QString &f)
|
||||
{
|
||||
for (auto it = disk_data_.begin(); it != disk_data_.end(); it++) {
|
||||
if (it.key() == f) {
|
||||
// Break out of this loop, assuming we'll only have one instance_ of each filename
|
||||
return delete_file_internal(it);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DiskCacheFolder::delete_least_recent()
|
||||
{
|
||||
auto hash_to_delete = disk_data_.begin();
|
||||
|
||||
if (disk_data_.begin() != disk_data_.end()) {
|
||||
for (auto it = disk_data_.begin() + 1; it != disk_data_.end(); it++) {
|
||||
if (it->access_time < hash_to_delete->access_time) {
|
||||
hash_to_delete = it;
|
||||
}
|
||||
}
|
||||
|
||||
bool e = delete_file_internal(hash_to_delete);
|
||||
|
||||
if (e) {
|
||||
EngineCore::instance()->warn_cache_full();
|
||||
}
|
||||
|
||||
return e;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void DiskCacheFolder::close_cache_folder()
|
||||
{
|
||||
if (path_.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clear_on_close_) {
|
||||
// If we're not moving to new and we're set to clear on close, clear now or else it'll never
|
||||
// get cleared later
|
||||
clear_cache();
|
||||
}
|
||||
|
||||
// Save current cache index
|
||||
save_disk_cache_index();
|
||||
}
|
||||
|
||||
void DiskCacheFolder::save_disk_cache_index()
|
||||
{
|
||||
QFile cache_index_file(index_path_);
|
||||
|
||||
if (cache_index_file.open(QFile::WriteOnly)) {
|
||||
QDataStream ds(&cache_index_file);
|
||||
|
||||
ds << limit_;
|
||||
ds << clear_on_close_;
|
||||
|
||||
for (auto it = disk_data_.cbegin(); it != disk_data_.cend(); it++) {
|
||||
const HashTime &ht = it.value();
|
||||
|
||||
ds << it.key();
|
||||
ds << ht.file_size;
|
||||
ds << ht.access_time;
|
||||
}
|
||||
|
||||
cache_index_file.close();
|
||||
} else {
|
||||
qWarning() << "Failed to write cache index:" << index_path_;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_DISKMANAGER_H
|
||||
#define OAK_DISKMANAGER_H
|
||||
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "common/define.h"
|
||||
#include "node/project.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class DiskCacheFolder : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
DiskCacheFolder(const QString &path, QObject *parent = nullptr);
|
||||
|
||||
virtual ~DiskCacheFolder() override;
|
||||
|
||||
bool clear_cache();
|
||||
|
||||
void accessed(const QString &filename);
|
||||
|
||||
void created_file(const QString &filename);
|
||||
|
||||
const QString &get_path() const
|
||||
{
|
||||
return path_;
|
||||
}
|
||||
|
||||
void set_path(const QString &path);
|
||||
|
||||
qint64 get_limit() const
|
||||
{
|
||||
return limit_;
|
||||
}
|
||||
|
||||
bool get_clear_on_close() const
|
||||
{
|
||||
return clear_on_close_;
|
||||
}
|
||||
|
||||
void set_limit(qint64 l)
|
||||
{
|
||||
limit_ = l;
|
||||
}
|
||||
|
||||
void set_clear_on_close(bool e)
|
||||
{
|
||||
clear_on_close_ = e;
|
||||
}
|
||||
|
||||
bool delete_specific_file(const QString &f);
|
||||
|
||||
signals:
|
||||
void deleted_frame(const QString &path, const QString &filename);
|
||||
|
||||
private:
|
||||
struct HashTime {
|
||||
qint64 file_size;
|
||||
qint64 access_time;
|
||||
};
|
||||
|
||||
bool delete_file_internal(QMap<QString, HashTime>::iterator hash_to_delete);
|
||||
|
||||
bool delete_least_recent();
|
||||
|
||||
void close_cache_folder();
|
||||
|
||||
QString path_;
|
||||
|
||||
QString index_path_;
|
||||
|
||||
QMap<QString, HashTime> disk_data_;
|
||||
|
||||
qint64 consumption_;
|
||||
|
||||
qint64 limit_;
|
||||
|
||||
bool clear_on_close_;
|
||||
|
||||
QTimer save_timer_;
|
||||
|
||||
private slots:
|
||||
void save_disk_cache_index();
|
||||
};
|
||||
|
||||
class DiskManager : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static void create_instance();
|
||||
|
||||
static void destroy_instance();
|
||||
|
||||
static DiskManager *instance();
|
||||
|
||||
bool clear_disk_cache(const QString &cache_folder);
|
||||
|
||||
DiskCacheFolder *get_default_cache_folder() const
|
||||
{
|
||||
// The first folder will always be the default
|
||||
return open_folders_.first();
|
||||
}
|
||||
|
||||
const QString &get_default_cache_path() const
|
||||
{
|
||||
return get_default_cache_folder()->get_path();
|
||||
}
|
||||
|
||||
DiskCacheFolder *get_open_folder(const QString &path);
|
||||
|
||||
const QVector<DiskCacheFolder *> &get_open_folders() const
|
||||
{
|
||||
return open_folders_;
|
||||
}
|
||||
|
||||
static bool show_disk_cache_change_confirmation_dialog(QWidget *parent);
|
||||
|
||||
static QString get_default_disk_cache_config_file();
|
||||
|
||||
static QString get_default_disk_cache_path();
|
||||
|
||||
/**
|
||||
* @brief Handler showing the disk cache settings dialog for a folder
|
||||
*
|
||||
* Registered by the UI layer (e.g. a DiskCacheDialog-based
|
||||
* implementation), since the engine cannot show dialogs itself. Without
|
||||
* a handler, the request is logged and skipped.
|
||||
*/
|
||||
using ShowDiskCacheSettingsHandler =
|
||||
std::function<void(DiskCacheFolder *folder, QWidget *parent)>;
|
||||
|
||||
static void set_show_disk_cache_settings_handler(
|
||||
ShowDiskCacheSettingsHandler handler);
|
||||
|
||||
void show_disk_cache_settings_dialog(DiskCacheFolder *folder, QWidget *parent);
|
||||
void show_disk_cache_settings_dialog(const QString &path, QWidget *parent);
|
||||
|
||||
public slots:
|
||||
void accessed(const QString &cache_folder, const QString &filename);
|
||||
|
||||
void created_file(const QString &cache_folder, const QString &filename);
|
||||
|
||||
void delete_specific_file(const QString &filename);
|
||||
|
||||
signals:
|
||||
void deleted_frame(const QString &path, const QString &filename);
|
||||
|
||||
void invalidate_project(Project *p);
|
||||
|
||||
private:
|
||||
DiskManager();
|
||||
|
||||
virtual ~DiskManager() override;
|
||||
|
||||
static DiskManager *instance_;
|
||||
|
||||
static ShowDiskCacheSettingsHandler show_disk_cache_settings_handler_;
|
||||
|
||||
QVector<DiskCacheFolder *> open_folders_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_DISKMANAGER_H
|
||||
@@ -0,0 +1,462 @@
|
||||
/*** Olive - Non-Linear Video Editor Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "framehashcache.h"
|
||||
|
||||
#include <OpenEXR/ImfFloatAttribute.h>
|
||||
#include <OpenEXR/ImfFrameBuffer.h>
|
||||
#include <OpenEXR/ImfHeader.h>
|
||||
#include <OpenEXR/ImfInputFile.h>
|
||||
#include <OpenEXR/ImfIntAttribute.h>
|
||||
#include <OpenEXR/ImfOutputFile.h>
|
||||
#include <OpenEXR/ImfChannelList.h>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "common/filefunctions.h"
|
||||
#include "common/oiioutils.h"
|
||||
#include "render/diskmanager.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
#define super PlaybackCache
|
||||
|
||||
FrameHashCache::FrameHashCache(QObject *parent)
|
||||
: super(parent)
|
||||
{
|
||||
if (DiskManager::instance()) {
|
||||
connect(DiskManager::instance(), &DiskManager::deleted_frame, this,
|
||||
&FrameHashCache::hash_deleted);
|
||||
connect(DiskManager::instance(), &DiskManager::invalidate_project, this,
|
||||
&FrameHashCache::project_invalidated);
|
||||
}
|
||||
}
|
||||
|
||||
void FrameHashCache::set_timebase(const Rational &tb)
|
||||
{
|
||||
timebase_ = tb;
|
||||
}
|
||||
|
||||
void FrameHashCache::validate_timestamp(const int64_t &ts)
|
||||
{
|
||||
TimeRange frame_range(to_time(ts), to_time(ts + 1));
|
||||
validate(frame_range);
|
||||
}
|
||||
|
||||
void FrameHashCache::validate_time(const Rational &time)
|
||||
{
|
||||
validate(TimeRange(time, time + timebase_));
|
||||
}
|
||||
|
||||
QString FrameHashCache::get_valid_cache_filename(const Rational &time) const
|
||||
{
|
||||
if (is_frame_cached(time)) {
|
||||
return cache_path_name(time);
|
||||
} else if (!get_passthroughs().empty()) {
|
||||
for (const Passthrough &p : get_passthroughs()) {
|
||||
if (p.contains(time)) {
|
||||
return cache_path_name(get_cache_directory(), p.cache, time,
|
||||
timebase_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool FrameHashCache::save_cache_frame(const int64_t &time, FramePtr frame) const
|
||||
{
|
||||
return save_cache_frame(get_cache_directory(), get_uuid(), time, frame);
|
||||
}
|
||||
|
||||
bool FrameHashCache::save_cache_frame(const QString &cache_path,
|
||||
const QUuid &uuid, const int64_t &time,
|
||||
FramePtr frame)
|
||||
{
|
||||
if (cache_path.isEmpty()) {
|
||||
qWarning() << "Failed to save cache frame with empty path";
|
||||
return false;
|
||||
}
|
||||
|
||||
QString fn = cache_path_name(cache_path, uuid, time);
|
||||
|
||||
bool ret = save_cache_frame(fn, frame);
|
||||
|
||||
// Register frame with the disk manager
|
||||
if (ret) {
|
||||
QMetaObject::invokeMethod(DiskManager::instance(), "created_file",
|
||||
Q_ARG(QString, cache_path),
|
||||
Q_ARG(QString, fn));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool FrameHashCache::save_cache_frame(const QString &cache_path,
|
||||
const QUuid &uuid, const Rational &time,
|
||||
const Rational &tb, FramePtr frame)
|
||||
{
|
||||
if (cache_path.isEmpty()) {
|
||||
qWarning() << "Failed to save cache frame with empty path";
|
||||
return false;
|
||||
}
|
||||
|
||||
QString fn = cache_path_name(cache_path, uuid, time, tb);
|
||||
|
||||
bool ret = save_cache_frame(fn, frame);
|
||||
|
||||
// Register frame with the disk manager
|
||||
if (ret) {
|
||||
QMetaObject::invokeMethod(DiskManager::instance(), "created_file",
|
||||
Q_ARG(QString, cache_path),
|
||||
Q_ARG(QString, fn));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
FramePtr FrameHashCache::load_cache_frame(const QString &cache_path,
|
||||
const QUuid &uuid, const int64_t &time)
|
||||
{
|
||||
// Minor optimization, we store frames currently being saved just in case something tries to load
|
||||
// while we're saving. This should *occasionally* optimize and also prevent scenarios where
|
||||
// we try to load a frame that's half way through being saved.
|
||||
QString filename = cache_path_name(cache_path, uuid, time);
|
||||
|
||||
if (cache_path.isEmpty()) {
|
||||
qWarning() << "Failed to load cache frame with empty path";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return load_cache_frame(filename);
|
||||
}
|
||||
|
||||
FramePtr FrameHashCache::load_cache_frame(const int64_t &hash) const
|
||||
{
|
||||
return load_cache_frame(get_cache_directory(), get_uuid(), hash);
|
||||
}
|
||||
|
||||
FramePtr FrameHashCache::load_cache_frame(const QString &fn)
|
||||
{
|
||||
FramePtr frame = nullptr;
|
||||
|
||||
if (!fn.isEmpty() && QFileInfo::exists(fn)) {
|
||||
try {
|
||||
Imf::InputFile file(fn.toUtf8(), 0);
|
||||
|
||||
Imath::Box2i dw = file.header().dataWindow();
|
||||
Imf::PixelType pix_type =
|
||||
file.header().channels().begin().channel().type;
|
||||
int width = dw.max.x - dw.min.x + 1;
|
||||
int height = dw.max.y - dw.min.y + 1;
|
||||
bool has_alpha = file.header().channels().findChannel("A");
|
||||
|
||||
int div = qMax(1, static_cast<const Imf::IntAttribute &>(
|
||||
file.header()["oliveDivider"])
|
||||
.value());
|
||||
|
||||
PixelFormat image_format;
|
||||
if (pix_type == Imf::HALF) {
|
||||
image_format = PixelFormat::f16;
|
||||
} else {
|
||||
image_format = PixelFormat::f32;
|
||||
}
|
||||
|
||||
int channel_count = has_alpha ? VideoParams::k_rgba_channel_count :
|
||||
VideoParams::k_rgb_channel_count;
|
||||
|
||||
frame = Frame::create();
|
||||
frame->set_video_params(VideoParams(
|
||||
width * div, height * div, image_format, channel_count,
|
||||
Rational::from_double(file.header().pixelAspectRatio()),
|
||||
VideoParams::k_interlace_none, div));
|
||||
|
||||
frame->allocate();
|
||||
|
||||
int bpc = VideoParams::get_bytes_per_channel(image_format);
|
||||
|
||||
size_t xs = channel_count * bpc;
|
||||
size_t ys = frame->linesize_bytes();
|
||||
|
||||
Imf::FrameBuffer framebuffer;
|
||||
framebuffer.insert("R",
|
||||
Imf::Slice(pix_type, frame->data(), xs, ys));
|
||||
framebuffer.insert("G", Imf::Slice(pix_type, frame->data() + bpc,
|
||||
xs, ys));
|
||||
framebuffer.insert(
|
||||
"B", Imf::Slice(pix_type, frame->data() + 2 * bpc, xs, ys));
|
||||
if (has_alpha) {
|
||||
framebuffer.insert(
|
||||
"A", Imf::Slice(pix_type, frame->data() + 3 * bpc, xs, ys));
|
||||
}
|
||||
|
||||
file.setFrameBuffer(framebuffer);
|
||||
|
||||
file.readPixels(dw.min.y, dw.max.y);
|
||||
} catch (const std::exception &e) {
|
||||
// Not an EXR, maybe it's a JPEG?
|
||||
QImage img;
|
||||
|
||||
if (img.load(fn, "jpg")) {
|
||||
// FIXME: Hardcoded
|
||||
const int div = 1;
|
||||
const PixelFormat image_format = PixelFormat::u8;
|
||||
const int channel_count = 4;
|
||||
const Rational par(1, 1);
|
||||
|
||||
// Convert to frame (FIXME: might be slow? may be a better way to do this on the GPU)
|
||||
img.convertTo(QImage::Format_RGBA8888_Premultiplied);
|
||||
|
||||
frame = Frame::create();
|
||||
frame->set_video_params(VideoParams(
|
||||
img.width() * div, img.height() * div, image_format,
|
||||
channel_count, par, VideoParams::k_interlace_none, div));
|
||||
|
||||
frame->allocate();
|
||||
|
||||
for (int i = 0; i < img.height(); i++) {
|
||||
memcpy(frame->data() + frame->linesize_bytes() * i,
|
||||
img.bits() + img.bytesPerLine() * i,
|
||||
frame->width() *
|
||||
frame->video_params().get_bytes_per_pixel());
|
||||
}
|
||||
|
||||
} else {
|
||||
qCritical() << "Failed to read cache frame:" << e.what();
|
||||
|
||||
// Clear frame to signal that nothing was loaded
|
||||
frame = nullptr;
|
||||
|
||||
// Assume this frame is corrupt in some way and delete it
|
||||
QMetaObject::invokeMethod(DiskManager::instance(),
|
||||
"delete_specific_file",
|
||||
Q_ARG(QString, fn));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
void FrameHashCache::set_passthrough(PlaybackCache *cache)
|
||||
{
|
||||
super::set_passthrough(cache);
|
||||
set_timebase(static_cast<FrameHashCache *>(cache)->get_timebase());
|
||||
}
|
||||
|
||||
void FrameHashCache::LoadStateEvent(QDataStream &stream)
|
||||
{
|
||||
uint32_t version;
|
||||
int num, den;
|
||||
|
||||
stream >> version;
|
||||
|
||||
switch (version) {
|
||||
case 1:
|
||||
stream >> num;
|
||||
stream >> den;
|
||||
timebase_ = Rational(num, den);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void FrameHashCache::SaveStateEvent(QDataStream &stream)
|
||||
{
|
||||
uint32_t version = 1;
|
||||
|
||||
stream << version;
|
||||
|
||||
stream << timebase_.numerator();
|
||||
stream << timebase_.denominator();
|
||||
}
|
||||
|
||||
Rational FrameHashCache::to_time(const int64_t &ts) const
|
||||
{
|
||||
return Timecode::timestamp_to_time(ts, timebase_);
|
||||
}
|
||||
|
||||
int64_t FrameHashCache::to_timestamp(const Rational &ts,
|
||||
Timecode::Rounding rounding) const
|
||||
{
|
||||
return Timecode::time_to_timestamp(ts, timebase_, rounding);
|
||||
}
|
||||
|
||||
void FrameHashCache::hash_deleted(const QString &path, const QString &filename)
|
||||
{
|
||||
QString cache_dir = get_cache_directory();
|
||||
if (cache_dir.isEmpty() || path != cache_dir) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFileInfo info(filename);
|
||||
if (get_uuid().toString() != info.dir().dirName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t timestamp = info.fileName().toLongLong();
|
||||
invalidate(TimeRange(to_time(timestamp), to_time(timestamp + 1)));
|
||||
}
|
||||
|
||||
void FrameHashCache::project_invalidated(Project *p)
|
||||
{
|
||||
if (get_project() == p) {
|
||||
invalidate_all();
|
||||
}
|
||||
}
|
||||
|
||||
QString FrameHashCache::cache_path_name(const int64_t &time) const
|
||||
{
|
||||
return cache_path_name(get_cache_directory(), get_uuid(), time);
|
||||
}
|
||||
|
||||
QString FrameHashCache::cache_path_name(const Rational &time) const
|
||||
{
|
||||
return cache_path_name(get_cache_directory(), get_uuid(), time, timebase_);
|
||||
}
|
||||
|
||||
QString FrameHashCache::cache_path_name(const QString &cache_path,
|
||||
const QUuid &cache_id,
|
||||
const int64_t &time)
|
||||
{
|
||||
QString filename = get_this_cache_directory(cache_path, cache_id)
|
||||
.filePath(QString::number(time));
|
||||
|
||||
// Register that in some way this hash has been accessed
|
||||
if (DiskManager::instance()) {
|
||||
QMetaObject::invokeMethod(DiskManager::instance(), "accessed",
|
||||
Q_ARG(QString, cache_path),
|
||||
Q_ARG(QString, filename));
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
QString FrameHashCache::cache_path_name(const QString &cache_path,
|
||||
const QUuid &cache_id,
|
||||
const Rational &time, const Rational &tb)
|
||||
{
|
||||
return cache_path_name(cache_path, cache_id,
|
||||
Timecode::time_to_timestamp(time, tb,
|
||||
Timecode::k_round));
|
||||
}
|
||||
|
||||
bool FrameHashCache::save_cache_frame(const QString &filename,
|
||||
const FramePtr frame)
|
||||
{
|
||||
// Ensure directory is created
|
||||
QDir cache_dir = QFileInfo(filename).dir();
|
||||
if (!FileFunctions::directory_is_valid(cache_dir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (VideoParams::format_is_float(frame->format())) {
|
||||
// Floating point types are stored in EXR
|
||||
Imf::PixelType pix_type;
|
||||
|
||||
if (frame->format() == PixelFormat::f16) {
|
||||
pix_type = Imf::HALF;
|
||||
} else {
|
||||
pix_type = Imf::FLOAT;
|
||||
}
|
||||
|
||||
Imf::Header header(frame->width(), frame->height());
|
||||
header.channels().insert("R", Imf::Channel(pix_type));
|
||||
header.channels().insert("G", Imf::Channel(pix_type));
|
||||
header.channels().insert("B", Imf::Channel(pix_type));
|
||||
if (frame->channel_count() == VideoParams::k_rgba_channel_count) {
|
||||
header.channels().insert("A", Imf::Channel(pix_type));
|
||||
}
|
||||
|
||||
header.compression() = Imf::DWAA_COMPRESSION;
|
||||
header.insert("dwaCompressionLevel", Imf::FloatAttribute(200.0f));
|
||||
header.pixelAspectRatio() =
|
||||
frame->video_params().pixel_aspect_ratio().to_double();
|
||||
|
||||
header.insert("oliveDivider",
|
||||
Imf::IntAttribute(frame->video_params().divider()));
|
||||
|
||||
try {
|
||||
Imf::OutputFile out(filename.toUtf8(), header, 0);
|
||||
|
||||
int bpc = VideoParams::get_bytes_per_channel(frame->format());
|
||||
|
||||
size_t xs = frame->channel_count() * bpc;
|
||||
size_t ys = frame->linesize_bytes();
|
||||
|
||||
Imf::FrameBuffer framebuffer;
|
||||
framebuffer.insert("R",
|
||||
Imf::Slice(pix_type, frame->data(), xs, ys));
|
||||
framebuffer.insert("G", Imf::Slice(pix_type, frame->data() + bpc,
|
||||
xs, ys));
|
||||
framebuffer.insert(
|
||||
"B", Imf::Slice(pix_type, frame->data() + 2 * bpc, xs, ys));
|
||||
if (frame->channel_count() == VideoParams::k_rgba_channel_count) {
|
||||
framebuffer.insert(
|
||||
"A", Imf::Slice(pix_type, frame->data() + 3 * bpc, xs, ys));
|
||||
}
|
||||
out.setFrameBuffer(framebuffer);
|
||||
|
||||
out.writePixels(frame->height());
|
||||
|
||||
return true;
|
||||
} catch (const std::exception &e) {
|
||||
qCritical() << "Failed to write cache frame:" << e.what();
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
QImage::Format fmt = QImage::Format_Invalid;
|
||||
|
||||
switch (frame->format()) {
|
||||
case PixelFormat::u8:
|
||||
if (frame->channel_count() == VideoParams::k_rgba_channel_count) {
|
||||
fmt = QImage::Format_RGBA8888_Premultiplied;
|
||||
} else if (frame->channel_count() ==
|
||||
VideoParams::k_rgb_channel_count) {
|
||||
fmt = QImage::Format_RGB888;
|
||||
}
|
||||
break;
|
||||
case PixelFormat::u10:
|
||||
break;
|
||||
case PixelFormat::u16:
|
||||
if (frame->channel_count() == VideoParams::k_rgba_channel_count) {
|
||||
fmt = QImage::Format_RGBA64_Premultiplied;
|
||||
}
|
||||
break;
|
||||
case PixelFormat::f16:
|
||||
case PixelFormat::f32:
|
||||
case PixelFormat::count:
|
||||
case PixelFormat::invalid:
|
||||
break;
|
||||
}
|
||||
|
||||
if (fmt == QImage::Format_Invalid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QImage img(reinterpret_cast<const uchar *>(frame->data()),
|
||||
frame->width(), frame->height(), frame->linesize_bytes(),
|
||||
fmt);
|
||||
|
||||
return img.save(filename, "jpg");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_VIDEORENDERFRAMECACHE_H
|
||||
#define OAK_VIDEORENDERFRAMECACHE_H
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "render/playbackcache.h"
|
||||
#include "render/videoparams.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class FrameHashCache : public PlaybackCache {
|
||||
Q_OBJECT
|
||||
public:
|
||||
FrameHashCache(QObject *parent = nullptr);
|
||||
|
||||
const Rational &get_timebase() const
|
||||
{
|
||||
return timebase_;
|
||||
}
|
||||
|
||||
void set_timebase(const Rational &tb);
|
||||
|
||||
void validate_timestamp(const int64_t &ts);
|
||||
void validate_time(const Rational &time);
|
||||
|
||||
bool is_frame_cached(const Rational &time) const
|
||||
{
|
||||
return get_validated_ranges().contains(time);
|
||||
}
|
||||
|
||||
QString get_valid_cache_filename(const Rational &time) const;
|
||||
|
||||
static bool save_cache_frame(const QString &filename, FramePtr frame);
|
||||
bool save_cache_frame(const int64_t &time, FramePtr frame) const;
|
||||
static bool save_cache_frame(const QString &cache_path, const QUuid &uuid,
|
||||
const int64_t &time, FramePtr frame);
|
||||
static bool save_cache_frame(const QString &cache_path, const QUuid &uuid,
|
||||
const Rational &time, const Rational &tb,
|
||||
FramePtr frame);
|
||||
static FramePtr load_cache_frame(const QString &cache_path, const QUuid &uuid,
|
||||
const int64_t &time);
|
||||
FramePtr load_cache_frame(const int64_t &time) const;
|
||||
static FramePtr load_cache_frame(const QString &fn);
|
||||
|
||||
virtual void set_passthrough(PlaybackCache *cache) override;
|
||||
|
||||
protected:
|
||||
virtual void LoadStateEvent(QDataStream &stream) override;
|
||||
virtual void SaveStateEvent(QDataStream &stream) override;
|
||||
|
||||
private:
|
||||
Rational to_time(const int64_t &ts) const;
|
||||
int64_t to_timestamp(const Rational &ts,
|
||||
Timecode::Rounding rounding = Timecode::k_round) const;
|
||||
|
||||
/**
|
||||
* @brief Return the path of the cached image at this time
|
||||
*/
|
||||
QString cache_path_name(const int64_t &time) const;
|
||||
QString cache_path_name(const Rational &time) const;
|
||||
|
||||
static QString cache_path_name(const QString &cache_path,
|
||||
const QUuid &cache_id, const int64_t &time);
|
||||
static QString cache_path_name(const QString &cache_path,
|
||||
const QUuid &cache_id, const Rational &time,
|
||||
const Rational &tb);
|
||||
|
||||
Rational timebase_;
|
||||
|
||||
private slots:
|
||||
void hash_deleted(const QString &path, const QString &filename);
|
||||
|
||||
void project_invalidated(Project *p);
|
||||
};
|
||||
|
||||
class ThumbnailCache : public FrameHashCache {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ThumbnailCache(QObject *parent = nullptr)
|
||||
: FrameHashCache(parent)
|
||||
{
|
||||
set_timebase(Rational(1, 10));
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_VIDEORENDERFRAMECACHE_H
|
||||
@@ -0,0 +1,132 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "framemanager.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
FrameManager *FrameManager::instance_ = nullptr;
|
||||
const int FrameManager::k_frame_lifetime = 5000;
|
||||
|
||||
void FrameManager::create_instance()
|
||||
{
|
||||
instance_ = new FrameManager();
|
||||
}
|
||||
|
||||
void FrameManager::destroy_instance()
|
||||
{
|
||||
delete instance_;
|
||||
instance_ = nullptr;
|
||||
}
|
||||
|
||||
FrameManager *FrameManager::instance()
|
||||
{
|
||||
return instance_;
|
||||
}
|
||||
|
||||
char *FrameManager::allocate(int size)
|
||||
{
|
||||
if (instance()) {
|
||||
return instance()->allocate_from_pool(size);
|
||||
} else {
|
||||
return new char[size];
|
||||
}
|
||||
}
|
||||
|
||||
void FrameManager::deallocate(int size, char *buffer)
|
||||
{
|
||||
if (instance()) {
|
||||
instance()->deallocate_to_pool(size, buffer);
|
||||
} else {
|
||||
delete[] buffer;
|
||||
}
|
||||
}
|
||||
|
||||
FrameManager::FrameManager()
|
||||
{
|
||||
clear_timer_.setInterval(k_frame_lifetime);
|
||||
connect(&clear_timer_, &QTimer::timeout, this,
|
||||
&FrameManager::garbage_collection);
|
||||
clear_timer_.start();
|
||||
}
|
||||
|
||||
char *FrameManager::allocate_from_pool(int size)
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
|
||||
std::list<Buffer> &buffer_list = pool_[size];
|
||||
char *buf = nullptr;
|
||||
|
||||
if (buffer_list.empty()) {
|
||||
buf = new char[size];
|
||||
} else {
|
||||
// Take this buffer from the list
|
||||
buf = buffer_list.front().data;
|
||||
buffer_list.pop_front();
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
void FrameManager::deallocate_to_pool(int size, char *buffer)
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
|
||||
std::list<Buffer> &buffer_list = pool_[size];
|
||||
|
||||
buffer_list.push_back({ QDateTime::currentMSecsSinceEpoch(), buffer });
|
||||
}
|
||||
|
||||
void FrameManager::garbage_collection()
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
|
||||
qint64 min_life = QDateTime::currentMSecsSinceEpoch() - k_frame_lifetime;
|
||||
|
||||
for (auto it = pool_.begin(); it != pool_.end(); it++) {
|
||||
std::list<Buffer> &list = it->second;
|
||||
|
||||
while (list.size() > 0 && list.front().time < min_life) {
|
||||
delete[] list.front().data;
|
||||
list.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FrameManager::~FrameManager()
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
|
||||
for (auto it = pool_.begin(); it != pool_.end(); it++) {
|
||||
std::list<Buffer> &list = it->second;
|
||||
for (auto jt = list.begin(); jt != list.end(); jt++) {
|
||||
delete[] (*jt).data;
|
||||
}
|
||||
}
|
||||
|
||||
pool_.clear();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_FRAMEMANAGER_H
|
||||
#define OAK_FRAMEMANAGER_H
|
||||
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class FrameManager : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static void create_instance();
|
||||
|
||||
static void destroy_instance();
|
||||
|
||||
static FrameManager *instance();
|
||||
|
||||
static char *allocate(int size);
|
||||
|
||||
static void deallocate(int size, char *buffer);
|
||||
|
||||
private:
|
||||
FrameManager();
|
||||
|
||||
virtual ~FrameManager() override;
|
||||
|
||||
/**
|
||||
* @brief Allocate buffer
|
||||
*
|
||||
* Caller takes ownership of buffer and can delete it if they want. It can also be returned to
|
||||
* the manager with Deallocate and potentially be re-used later.
|
||||
*
|
||||
* Thread-safe.
|
||||
*/
|
||||
char *allocate_from_pool(int size);
|
||||
|
||||
/**
|
||||
* @brief Deallocate buffer
|
||||
*
|
||||
* Manager will take ownership and buffer will stay allocated for some time in case it can be
|
||||
* re-used.
|
||||
*
|
||||
* Thread-safe.
|
||||
*/
|
||||
void deallocate_to_pool(int size, char *buffer);
|
||||
|
||||
static FrameManager *instance_;
|
||||
|
||||
static const int k_frame_lifetime;
|
||||
|
||||
struct Buffer {
|
||||
qint64 time;
|
||||
char *data;
|
||||
};
|
||||
|
||||
std::map<int, std::list<Buffer>> pool_;
|
||||
|
||||
QMutex mutex_;
|
||||
|
||||
QTimer clear_timer_;
|
||||
|
||||
private slots:
|
||||
void garbage_collection();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_FRAMEMANAGER_H
|
||||
@@ -0,0 +1,59 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "renderer.h"
|
||||
|
||||
#include "common/filefunctions.h"
|
||||
#include "node/value.h"
|
||||
#include "render/job/shaderjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
TexturePtr Renderer::interlace_texture(TexturePtr top, TexturePtr bottom,
|
||||
const VideoParams ¶ms)
|
||||
{
|
||||
color_cache_mutex_.lock();
|
||||
if (interlace_texture_.isNull()) {
|
||||
interlace_texture_ =
|
||||
create_native_shader(ShaderCode(FileFunctions::read_file_as_string(
|
||||
QStringLiteral(":/shaders/interlace.frag"))));
|
||||
}
|
||||
color_cache_mutex_.unlock();
|
||||
|
||||
ShaderJob job;
|
||||
job.insert(QStringLiteral("top_tex_in"),
|
||||
NodeValue(NodeValue::k_texture, QVariant::fromValue(top)));
|
||||
job.insert(QStringLiteral("bottom_tex_in"),
|
||||
NodeValue(NodeValue::k_texture, QVariant::fromValue(bottom)));
|
||||
job.insert(QStringLiteral("resolution_in"),
|
||||
NodeValue(NodeValue::k_vec2,
|
||||
QVector2D(params.effective_width(),
|
||||
params.effective_height())));
|
||||
|
||||
TexturePtr output = create_texture(params);
|
||||
|
||||
blit_to_texture(interlace_texture_, job, output.get());
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Oak - Non-Linear Video Editor
|
||||
# Copyright (C) 2026 Oak 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/ipc/frameslotpool.cpp
|
||||
render/ipc/frameslotpool.h
|
||||
render/ipc/ipcmessage.cpp
|
||||
render/ipc/ipcmessage.h
|
||||
render/ipc/sharedmemoryregion.cpp
|
||||
render/ipc/sharedmemoryregion.h
|
||||
render/ipc/spscringbuffer.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1,179 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 "frameslotpool.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Round `value` up to the next multiple of `align` (align must be a power of two).
|
||||
size_t align_up(size_t value, size_t align)
|
||||
{
|
||||
return (value + (align - 1)) & ~(align - 1);
|
||||
}
|
||||
|
||||
constexpr size_t k_align = 64; // Cache-line alignment for each sub-region.
|
||||
|
||||
} // namespace
|
||||
|
||||
size_t FrameSlotPool::bytes_needed(uint32_t slot_count, size_t slot_data_bytes)
|
||||
{
|
||||
const uint32_t ring_cap = ring_capacity(slot_count);
|
||||
size_t total = align_up(sizeof(Header), k_align);
|
||||
total +=
|
||||
align_up(SpscRingBuffer::bytes_needed(ring_cap), k_align); // free ring
|
||||
total +=
|
||||
align_up(SpscRingBuffer::bytes_needed(ring_cap), k_align); // ready ring
|
||||
total +=
|
||||
align_up(sizeof(FrameSlotMeta) * slot_count, k_align); // metadata array
|
||||
total += align_up(slot_data_bytes, k_align) * slot_count; // pixel data blocks
|
||||
return total;
|
||||
}
|
||||
|
||||
FrameSlotPool FrameSlotPool::create(void *mem, uint32_t slot_count,
|
||||
size_t slot_data_bytes)
|
||||
{
|
||||
FrameSlotPool pool;
|
||||
pool.base_ = reinterpret_cast<uint8_t *>(mem);
|
||||
|
||||
const uint32_t ring_cap = ring_capacity(slot_count);
|
||||
|
||||
size_t offset = 0;
|
||||
const size_t header_off = offset;
|
||||
offset += align_up(sizeof(Header), k_align);
|
||||
|
||||
const size_t free_off = offset;
|
||||
offset += align_up(SpscRingBuffer::bytes_needed(ring_cap), k_align);
|
||||
|
||||
const size_t ready_off = offset;
|
||||
offset += align_up(SpscRingBuffer::bytes_needed(ring_cap), k_align);
|
||||
|
||||
const size_t meta_off = offset;
|
||||
offset += align_up(sizeof(FrameSlotMeta) * slot_count, k_align);
|
||||
|
||||
const size_t data_off = offset;
|
||||
|
||||
pool.header_ = reinterpret_cast<Header *>(pool.base_ + header_off);
|
||||
pool.header_->magic = k_magic;
|
||||
pool.header_->slot_count = slot_count;
|
||||
pool.header_->slot_data_bytes = slot_data_bytes;
|
||||
pool.header_->free_ring_offset = free_off;
|
||||
pool.header_->ready_ring_offset = ready_off;
|
||||
pool.header_->meta_offset = meta_off;
|
||||
pool.header_->data_offset = data_off;
|
||||
|
||||
pool.free_ring_ = SpscRingBuffer::create(pool.base_ + free_off, ring_cap);
|
||||
pool.ready_ring_ = SpscRingBuffer::create(pool.base_ + ready_off, ring_cap);
|
||||
pool.meta_ = reinterpret_cast<FrameSlotMeta *>(pool.base_ + meta_off);
|
||||
pool.data_ = pool.base_ + data_off;
|
||||
|
||||
memset(pool.meta_, 0, sizeof(FrameSlotMeta) * slot_count);
|
||||
|
||||
// Seed the free ring with every slot index so the filler can Acquire() immediately.
|
||||
for (uint32_t i = 0; i < slot_count; i++) {
|
||||
pool.free_ring_->push(i);
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
FrameSlotPool FrameSlotPool::attach(void *mem)
|
||||
{
|
||||
FrameSlotPool pool;
|
||||
pool.base_ = reinterpret_cast<uint8_t *>(mem);
|
||||
pool.header_ = reinterpret_cast<Header *>(pool.base_);
|
||||
|
||||
if (pool.header_->magic != k_magic) {
|
||||
// Caller will see IsValid() == false via a null header reset.
|
||||
pool.header_ = nullptr;
|
||||
pool.base_ = nullptr;
|
||||
return pool;
|
||||
}
|
||||
|
||||
pool.free_ring_ =
|
||||
SpscRingBuffer::attach(pool.base_ + pool.header_->free_ring_offset);
|
||||
pool.ready_ring_ =
|
||||
SpscRingBuffer::attach(pool.base_ + pool.header_->ready_ring_offset);
|
||||
pool.meta_ = reinterpret_cast<FrameSlotMeta *>(pool.base_ +
|
||||
pool.header_->meta_offset);
|
||||
pool.data_ = pool.base_ + pool.header_->data_offset;
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
uint32_t FrameSlotPool::slot_count() const
|
||||
{
|
||||
return header_ ? header_->slot_count : 0;
|
||||
}
|
||||
|
||||
size_t FrameSlotPool::slot_data_bytes() const
|
||||
{
|
||||
return header_ ? size_t(header_->slot_data_bytes) : 0;
|
||||
}
|
||||
|
||||
bool FrameSlotPool::acquire(uint32_t *index)
|
||||
{
|
||||
return free_ring_->pop(index);
|
||||
}
|
||||
|
||||
void *FrameSlotPool::slot_data(uint32_t index)
|
||||
{
|
||||
return data_ + size_t(index) * align_up(slot_data_bytes(), k_align);
|
||||
}
|
||||
|
||||
const void *FrameSlotPool::slot_data(uint32_t index) const
|
||||
{
|
||||
return data_ + size_t(index) * align_up(slot_data_bytes(), k_align);
|
||||
}
|
||||
|
||||
FrameSlotMeta *FrameSlotPool::meta(uint32_t index)
|
||||
{
|
||||
return &meta_[index];
|
||||
}
|
||||
|
||||
const FrameSlotMeta *FrameSlotPool::meta(uint32_t index) const
|
||||
{
|
||||
return &meta_[index];
|
||||
}
|
||||
|
||||
bool FrameSlotPool::publish(uint32_t index)
|
||||
{
|
||||
return ready_ring_->push(index);
|
||||
}
|
||||
|
||||
bool FrameSlotPool::consume(uint32_t *index)
|
||||
{
|
||||
return ready_ring_->pop(index);
|
||||
}
|
||||
|
||||
bool FrameSlotPool::release(uint32_t index)
|
||||
{
|
||||
return free_ring_->push(index);
|
||||
}
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
@@ -0,0 +1,183 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_IPC_FRAMESLOTPOOL_H
|
||||
#define OAK_IPC_FRAMESLOTPOOL_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "spscringbuffer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Per-slot metadata describing the frame currently occupying a slot.
|
||||
*
|
||||
* Trivially-copyable POD that lives in shared memory alongside the pixel data. Carries everything
|
||||
* the consumer needs to reconstruct an olive::Frame without any out-of-band information. We store
|
||||
* the Rational timestamp as an explicit numerator/denominator pair to stay POD (olive::Rational is
|
||||
* not guaranteed shared-memory-safe).
|
||||
*/
|
||||
struct FrameSlotMeta {
|
||||
int64_t id; ///< Caller-defined tag (e.g. ticket id, or footage stream hash).
|
||||
int64_t time_num; ///< Frame timestamp numerator.
|
||||
int64_t time_den; ///< Frame timestamp denominator.
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
int32_t format; ///< olive::PixelFormat::Format value.
|
||||
int32_t channel_count;
|
||||
int32_t linesize; ///< Bytes per scanline (stride).
|
||||
int32_t data_size; ///< Valid bytes written into the slot's data block.
|
||||
char colorspace[128]; ///< Input colorspace name for color-managed footage.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A fixed-size pool of equal-sized frame slots in shared memory, with lock-free hand-off.
|
||||
*
|
||||
* One pool models a single direction of frame flow (e.g. worker -> main for rendered output, or
|
||||
* main -> worker for decoded input). Ownership of a slot is transferred via two SPSC ring buffers
|
||||
* of slot indices, so no mutex is ever taken:
|
||||
*
|
||||
* - free_ring: indices of slots available to the FILLER. The drainer returns slots here.
|
||||
* - ready_ring: indices of slots holding a published frame, produced by the FILLER for the
|
||||
* DRAINER to consume.
|
||||
*
|
||||
* Lifecycle (filler = producer of frames, drainer = consumer of frames):
|
||||
* filler: Acquire() -> pop a free index -> write meta + pixels -> Publish() -> push to ready
|
||||
* drainer: Consume() -> pop a ready index -> read meta + pixels -> Release() -> push to free
|
||||
*
|
||||
* Because each ring has exactly one producer and one consumer (the filler owns free.Pop +
|
||||
* ready.Push, the drainer owns ready.Pop + free.Push), the SPSC invariant holds and the whole
|
||||
* exchange is lock-free.
|
||||
*
|
||||
* All slots are sized to `slot_data_bytes`, computed for the maximum supported frame (e.g. 8K RGBA
|
||||
* half-float). Frames smaller than that simply use a prefix of the slot.
|
||||
*
|
||||
* The pool does NOT own the memory; it is constructed over a SharedMemoryRegion mapping. Use
|
||||
* BytesNeeded() to size that region.
|
||||
*/
|
||||
class FrameSlotPool {
|
||||
public:
|
||||
/**
|
||||
* @brief Total bytes a region must provide to back a pool of `slot_count` x `slot_data_bytes`.
|
||||
*/
|
||||
static size_t bytes_needed(uint32_t slot_count, size_t slot_data_bytes);
|
||||
|
||||
/**
|
||||
* @brief Lay out and initialize a brand-new pool over `mem` (owner side, once).
|
||||
*
|
||||
* Initializes both rings, seeds the free ring with every slot index, and zeroes metadata.
|
||||
* `mem` must provide at least BytesNeeded(slot_count, slot_data_bytes) bytes.
|
||||
*/
|
||||
static FrameSlotPool create(void *mem, uint32_t slot_count,
|
||||
size_t slot_data_bytes);
|
||||
|
||||
/**
|
||||
* @brief Map an existing, already-initialized pool (peer side).
|
||||
*
|
||||
* Reads slot_count/slot_data_bytes from the in-memory header written by Create().
|
||||
*/
|
||||
static FrameSlotPool attach(void *mem);
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return header_ != nullptr;
|
||||
}
|
||||
|
||||
uint32_t slot_count() const;
|
||||
size_t slot_data_bytes() const;
|
||||
|
||||
// ---- Filler side ----
|
||||
|
||||
/**
|
||||
* @brief Take ownership of a free slot. Returns false (and leaves *index untouched) if none free.
|
||||
*/
|
||||
bool acquire(uint32_t *index);
|
||||
|
||||
/**
|
||||
* @brief Pointer to a slot's pixel data block (slot_data_bytes available).
|
||||
*/
|
||||
void *slot_data(uint32_t index);
|
||||
|
||||
/**
|
||||
* @brief Mutable metadata for a slot. Filler writes this before Publish().
|
||||
*/
|
||||
FrameSlotMeta *meta(uint32_t index);
|
||||
|
||||
/**
|
||||
* @brief Publish a filled slot to the drainer. Must follow a successful Acquire() of `index`.
|
||||
*/
|
||||
bool publish(uint32_t index);
|
||||
|
||||
// ---- Drainer side ----
|
||||
|
||||
/**
|
||||
* @brief Take the next published slot. Returns false if nothing is ready.
|
||||
*/
|
||||
bool consume(uint32_t *index);
|
||||
|
||||
/**
|
||||
* @brief Return a consumed slot to the free pool for reuse. Must follow Consume() of `index`.
|
||||
*/
|
||||
bool release(uint32_t index);
|
||||
|
||||
const FrameSlotMeta *meta(uint32_t index) const;
|
||||
const void *slot_data(uint32_t index) const;
|
||||
|
||||
public:
|
||||
FrameSlotPool() = default;
|
||||
|
||||
private:
|
||||
struct Header {
|
||||
uint32_t magic;
|
||||
uint32_t slot_count;
|
||||
uint64_t slot_data_bytes;
|
||||
// Byte offsets from the start of the segment to each sub-region.
|
||||
uint64_t free_ring_offset;
|
||||
uint64_t ready_ring_offset;
|
||||
uint64_t meta_offset;
|
||||
uint64_t data_offset;
|
||||
};
|
||||
|
||||
static constexpr uint32_t k_magic = 0x4F4B5350; // 'OKSP'
|
||||
|
||||
// Ring capacity must exceed slot_count by one because a ring can hold at most capacity-1 entries
|
||||
// and we need to be able to enqueue every slot at once.
|
||||
static uint32_t ring_capacity(uint32_t slot_count)
|
||||
{
|
||||
return slot_count + 1;
|
||||
}
|
||||
|
||||
uint8_t *base_ = nullptr;
|
||||
Header *header_ = nullptr;
|
||||
SpscRingBuffer *free_ring_ = nullptr;
|
||||
SpscRingBuffer *ready_ring_ = nullptr;
|
||||
FrameSlotMeta *meta_ = nullptr;
|
||||
uint8_t *data_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
|
||||
#endif // OAK_IPC_FRAMESLOTPOOL_H
|
||||
@@ -0,0 +1,230 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 "ipcmessage.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QIODevice>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
bool write_message(QIODevice *device, const QJsonObject &obj)
|
||||
{
|
||||
QByteArray line = QJsonDocument(obj).toJson(QJsonDocument::Compact);
|
||||
line.append('\n');
|
||||
return device->write(line) == line.size();
|
||||
}
|
||||
|
||||
bool read_message(QByteArray *buffer, QJsonObject *out, bool *ok)
|
||||
{
|
||||
while (true) {
|
||||
const int newline = buffer->indexOf('\n');
|
||||
if (newline < 0) {
|
||||
// No complete line buffered yet.
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray line = buffer->left(newline);
|
||||
buffer->remove(0, newline + 1);
|
||||
|
||||
// Skip blank lines silently (e.g. a stray newline) without flagging an error.
|
||||
if (line.trimmed().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonParseError err;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(line, &err);
|
||||
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
if (ok) {
|
||||
*ok = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
*out = doc.object();
|
||||
if (ok) {
|
||||
*ok = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- HandshakeMsg ---------------------------------------------------------------------------
|
||||
|
||||
QJsonObject HandshakeMsg::to_json() const
|
||||
{
|
||||
QJsonObject o;
|
||||
o["type"] = msgtype::k_handshake;
|
||||
o["protocol_version"] = protocol_version;
|
||||
o["shm_key"] = shm_key;
|
||||
o["input_shm_key"] = input_shm_key;
|
||||
o["input_slots"] = input_slots;
|
||||
o["output_slots"] = output_slots;
|
||||
o["slot_data_bytes"] = double(slot_data_bytes);
|
||||
o["input_slot_data_bytes"] = double(input_slot_data_bytes);
|
||||
return o;
|
||||
}
|
||||
|
||||
bool HandshakeMsg::from_json(const QJsonObject &o, HandshakeMsg *out)
|
||||
{
|
||||
if (o["type"].toString() != QLatin1String(msgtype::k_handshake)) {
|
||||
return false;
|
||||
}
|
||||
out->protocol_version = o["protocol_version"].toInt();
|
||||
out->shm_key = o["shm_key"].toString();
|
||||
out->input_shm_key = o["input_shm_key"].toString();
|
||||
out->input_slots = o["input_slots"].toInt();
|
||||
out->output_slots = o["output_slots"].toInt();
|
||||
out->slot_data_bytes = qint64(o["slot_data_bytes"].toDouble());
|
||||
out->input_slot_data_bytes = qint64(o["input_slot_data_bytes"].toDouble());
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- RenderFrameMsg -------------------------------------------------------------------------
|
||||
|
||||
QJsonObject RenderFrameMsg::to_json() const
|
||||
{
|
||||
QJsonObject o;
|
||||
o["type"] = msgtype::k_render_frame;
|
||||
o["ticket"] = double(ticket_id);
|
||||
o["node"] = node_uuid;
|
||||
o["time_num"] = double(time_num);
|
||||
o["time_den"] = double(time_den);
|
||||
o["width"] = width;
|
||||
o["height"] = height;
|
||||
o["format"] = format;
|
||||
o["channels"] = channel_count;
|
||||
o["mode"] = mode;
|
||||
o["input_slot"] = input_slot;
|
||||
QJsonArray input_slot_array;
|
||||
for (int slot : input_slots) {
|
||||
input_slot_array.append(slot);
|
||||
}
|
||||
o["input_slots"] = input_slot_array;
|
||||
|
||||
if (has_color_transform) {
|
||||
o["has_color_transform"] = true;
|
||||
o["color_is_display"] = color_is_display;
|
||||
o["color_output"] = color_output;
|
||||
o["color_view"] = color_view;
|
||||
o["color_look"] = color_look;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
bool RenderFrameMsg::from_json(const QJsonObject &o, RenderFrameMsg *out)
|
||||
{
|
||||
if (o["type"].toString() != QLatin1String(msgtype::k_render_frame)) {
|
||||
return false;
|
||||
}
|
||||
out->ticket_id = qint64(o["ticket"].toDouble());
|
||||
out->node_uuid = o["node"].toString();
|
||||
out->time_num = qint64(o["time_num"].toDouble());
|
||||
out->time_den = qint64(o["time_den"].toDouble(1));
|
||||
out->width = o["width"].toInt();
|
||||
out->height = o["height"].toInt();
|
||||
out->format = o["format"].toInt(-1);
|
||||
out->channel_count = o["channels"].toInt();
|
||||
out->mode = o["mode"].toInt();
|
||||
out->input_slot = o["input_slot"].toInt(-1);
|
||||
out->input_slots.clear();
|
||||
const QJsonArray input_slot_array = o["input_slots"].toArray();
|
||||
for (const QJsonValue &slot : input_slot_array) {
|
||||
out->input_slots.append(slot.toInt(-1));
|
||||
}
|
||||
if (out->input_slots.isEmpty() && out->input_slot >= 0) {
|
||||
out->input_slots.append(out->input_slot);
|
||||
}
|
||||
|
||||
out->has_color_transform = o["has_color_transform"].toBool(false);
|
||||
if (out->has_color_transform) {
|
||||
out->color_is_display = o["color_is_display"].toBool(false);
|
||||
out->color_output = o["color_output"].toString();
|
||||
out->color_view = o["color_view"].toString();
|
||||
out->color_look = o["color_look"].toString();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- FrameReadyMsg --------------------------------------------------------------------------
|
||||
|
||||
QJsonObject FrameReadyMsg::to_json() const
|
||||
{
|
||||
QJsonObject o;
|
||||
o["type"] = msgtype::k_frame_ready;
|
||||
o["ticket"] = double(ticket_id);
|
||||
o["slot"] = output_slot;
|
||||
return o;
|
||||
}
|
||||
|
||||
bool FrameReadyMsg::from_json(const QJsonObject &o, FrameReadyMsg *out)
|
||||
{
|
||||
if (o["type"].toString() != QLatin1String(msgtype::k_frame_ready)) {
|
||||
return false;
|
||||
}
|
||||
out->ticket_id = qint64(o["ticket"].toDouble());
|
||||
out->output_slot = o["slot"].toInt();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- CancelMsg ------------------------------------------------------------------------------
|
||||
|
||||
QJsonObject CancelMsg::to_json() const
|
||||
{
|
||||
QJsonObject o;
|
||||
o["type"] = msgtype::k_cancel;
|
||||
o["ticket"] = double(ticket_id);
|
||||
return o;
|
||||
}
|
||||
|
||||
bool CancelMsg::from_json(const QJsonObject &o, CancelMsg *out)
|
||||
{
|
||||
if (o["type"].toString() != QLatin1String(msgtype::k_cancel)) {
|
||||
return false;
|
||||
}
|
||||
out->ticket_id = qint64(o["ticket"].toDouble());
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- LoadGraphMsg ---------------------------------------------------------------------------
|
||||
|
||||
QJsonObject LoadGraphMsg::to_json() const
|
||||
{
|
||||
QJsonObject o;
|
||||
o["type"] = msgtype::k_load_graph;
|
||||
o["path"] = path;
|
||||
return o;
|
||||
}
|
||||
|
||||
bool LoadGraphMsg::from_json(const QJsonObject &o, LoadGraphMsg *out)
|
||||
{
|
||||
if (o["type"].toString() != QLatin1String(msgtype::k_load_graph)) {
|
||||
return false;
|
||||
}
|
||||
out->path = o["path"].toString();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
@@ -0,0 +1,161 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_IPC_IPCMESSAGE_H
|
||||
#define OAK_IPC_IPCMESSAGE_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <QByteArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
class QIODevice;
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Control-plane protocol exchanged over stdio between main and render worker.
|
||||
*
|
||||
* The wire format is NDJSON: one compact QJsonObject per line, terminated by '\n'. This is
|
||||
* deliberately human-readable so the channel can be inspected live with `tee`/`cat` and test
|
||||
* messages can be injected by hand. The stdio channel carries only low-frequency control traffic;
|
||||
* bulk pixel data travels through the shared-memory FrameSlotPool, and the (potentially large)
|
||||
* serialized node graph travels via a temporary file referenced by path.
|
||||
*
|
||||
* Every message object has a "type" string field. Directionality (M = main, W = worker):
|
||||
* "handshake" M<->W Negotiate protocol version and announce shared-memory key/geometry.
|
||||
* "load_graph" M ->W Path to a temporary file holding the serialized node graph.
|
||||
* "render_frame" M ->W Request a frame: node uuid, time, video params.
|
||||
* "frame_ready" W ->M A rendered frame is published; carries the output-slot index + ticket.
|
||||
* "cancel" M ->W Abandon an in-flight ticket by id.
|
||||
* "graph_update" M ->W (Reserved, Phase 6) Incremental graph mutation, mirrors ProjectCopier.
|
||||
* "shutdown" M ->W Finish current work and exit cleanly.
|
||||
* "error" W ->M Worker-side failure report (human-readable "message" field).
|
||||
*/
|
||||
namespace msgtype
|
||||
{
|
||||
constexpr const char *k_handshake = "handshake";
|
||||
constexpr const char *k_load_graph = "load_graph";
|
||||
constexpr const char *k_render_frame = "render_frame";
|
||||
constexpr const char *k_frame_ready = "frame_ready";
|
||||
constexpr const char *k_cancel = "cancel";
|
||||
constexpr const char *k_graph_update = "graph_update";
|
||||
constexpr const char *k_shutdown = "shutdown";
|
||||
constexpr const char *k_error = "error";
|
||||
} // namespace msgtype
|
||||
|
||||
/**
|
||||
* @brief Write one NDJSON message line to `device`.
|
||||
*
|
||||
* Serializes `obj` to compact JSON, appends '\n', and writes the whole line in one call. Returns
|
||||
* true only if the full line was written.
|
||||
*/
|
||||
bool write_message(QIODevice *device, const QJsonObject &obj);
|
||||
|
||||
/**
|
||||
* @brief Pull one complete NDJSON line out of `buffer` and parse it.
|
||||
*
|
||||
* If `buffer` contains at least one '\n', the leading line is removed, parsed as JSON, and returned
|
||||
* via `out` (true). If no complete line is buffered yet, leaves `buffer` untouched and returns
|
||||
* false. Malformed lines are skipped (removed) and reported via `*ok = false` so the reader can log
|
||||
* and continue rather than wedge. Supports the typical "append bytes as they arrive, then drain
|
||||
* complete lines" reader loop on a pipe.
|
||||
*/
|
||||
bool read_message(QByteArray *buffer, QJsonObject *out, bool *ok = nullptr);
|
||||
|
||||
// ---- Typed message builders / parsers -------------------------------------------------------
|
||||
//
|
||||
// Thin helpers that construct or read the QJsonObject for each message type, keeping field names in
|
||||
// one place so main and worker agree. Fields use plain JSON numbers/strings; 64-bit ids are stored
|
||||
// as JSON numbers (doubles exactly represent integers up to 2^53, ample for our counters).
|
||||
|
||||
struct HandshakeMsg {
|
||||
int protocol_version = 0;
|
||||
QString shm_key; ///< Worker->main output shared-memory segment key.
|
||||
QString
|
||||
input_shm_key; ///< Main->worker input shared-memory segment key (optional).
|
||||
int input_slots = 0; ///< Number of main->worker input frame slots.
|
||||
int output_slots = 0; ///< Number of worker->main output frame slots.
|
||||
qint64 slot_data_bytes = 0; ///< Per-output-slot pixel block size.
|
||||
qint64 input_slot_data_bytes = 0; ///< Per-input-slot pixel block size.
|
||||
|
||||
QJsonObject to_json() const;
|
||||
static bool from_json(const QJsonObject &o, HandshakeMsg *out);
|
||||
};
|
||||
|
||||
struct RenderFrameMsg {
|
||||
qint64 ticket_id =
|
||||
0; ///< Correlates this request with the eventual frame_ready.
|
||||
QString
|
||||
node_uuid; ///< Output/viewer node to render, by stable uuid in the loaded graph.
|
||||
qint64 time_num = 0;
|
||||
qint64 time_den = 1;
|
||||
int width = 0; ///< Forced output size (0 = use graph default).
|
||||
int height = 0;
|
||||
int format = -1; ///< Forced PixelFormat::Format (-1 = default/INVALID).
|
||||
int channel_count = 0; ///< 0 = default.
|
||||
int mode = 0; ///< RenderMode::Mode.
|
||||
int input_slot =
|
||||
-1; ///< Optional main->worker decoded input slot for footage nodes.
|
||||
QVector<int>
|
||||
input_slots; ///< Optional ordered decoded input slots for footage nodes.
|
||||
|
||||
// Output color transform to apply before returning the frame. When empty,
|
||||
// the worker returns the image in the project's reference space.
|
||||
bool has_color_transform = false;
|
||||
bool color_is_display = false;
|
||||
QString color_output;
|
||||
QString color_view;
|
||||
QString color_look;
|
||||
|
||||
QJsonObject to_json() const;
|
||||
static bool from_json(const QJsonObject &o, RenderFrameMsg *out);
|
||||
};
|
||||
|
||||
struct FrameReadyMsg {
|
||||
qint64 ticket_id = 0;
|
||||
int output_slot = 0; ///< Index into the worker->main output FrameSlotPool.
|
||||
|
||||
QJsonObject to_json() const;
|
||||
static bool from_json(const QJsonObject &o, FrameReadyMsg *out);
|
||||
};
|
||||
|
||||
struct CancelMsg {
|
||||
qint64 ticket_id = 0;
|
||||
|
||||
QJsonObject to_json() const;
|
||||
static bool from_json(const QJsonObject &o, CancelMsg *out);
|
||||
};
|
||||
|
||||
struct LoadGraphMsg {
|
||||
QString path; ///< Temporary file holding the serialized node graph.
|
||||
|
||||
QJsonObject to_json() const;
|
||||
static bool from_json(const QJsonObject &o, LoadGraphMsg *out);
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
|
||||
#endif // OAK_IPC_IPCMESSAGE_H
|
||||
@@ -0,0 +1,231 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 "sharedmemoryregion.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
SharedMemoryRegion::SharedMemoryRegion()
|
||||
: size_(0)
|
||||
, data_(nullptr)
|
||||
, mode_(k_attach)
|
||||
#if defined(Q_OS_WIN)
|
||||
, handle_(nullptr)
|
||||
#else
|
||||
, fd_(-1)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
SharedMemoryRegion::~SharedMemoryRegion()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
QString SharedMemoryRegion::make_key(qint64 owner_pid, int worker_index)
|
||||
{
|
||||
return QStringLiteral("olive-rw-%1-%2").arg(owner_pid).arg(worker_index);
|
||||
}
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
|
||||
bool SharedMemoryRegion::Open(const QString &key, size_t size, Mode mode)
|
||||
{
|
||||
Close();
|
||||
|
||||
key_ = key;
|
||||
size_ = size;
|
||||
mode_ = mode;
|
||||
|
||||
// Windows global mapping names live in the Local\ namespace by default for the session.
|
||||
const QString mapping_name = QStringLiteral("Local\\") + key;
|
||||
const std::wstring wname = mapping_name.toStdWString();
|
||||
|
||||
if (mode == kCreate) {
|
||||
const DWORD size_high =
|
||||
static_cast<DWORD>((quint64(size) >> 32) & 0xFFFFFFFF);
|
||||
const DWORD size_low = static_cast<DWORD>(quint64(size) & 0xFFFFFFFF);
|
||||
handle_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr,
|
||||
PAGE_READWRITE, size_high, size_low,
|
||||
wname.c_str());
|
||||
if (!handle_) {
|
||||
error_ = QStringLiteral("CreateFileMapping failed: %1")
|
||||
.arg(GetLastError());
|
||||
return false;
|
||||
}
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
error_ =
|
||||
QStringLiteral("Shared memory key already exists: %1").arg(key);
|
||||
CloseHandle(handle_);
|
||||
handle_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
handle_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wname.c_str());
|
||||
if (!handle_) {
|
||||
error_ =
|
||||
QStringLiteral("OpenFileMapping failed: %1").arg(GetLastError());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
data_ = MapViewOfFile(handle_, FILE_MAP_ALL_ACCESS, 0, 0, size);
|
||||
if (!data_) {
|
||||
error_ = QStringLiteral("MapViewOfFile failed: %1").arg(GetLastError());
|
||||
CloseHandle(handle_);
|
||||
handle_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode == kCreate) {
|
||||
memset(data_, 0, size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SharedMemoryRegion::Close()
|
||||
{
|
||||
if (data_) {
|
||||
UnmapViewOfFile(data_);
|
||||
data_ = nullptr;
|
||||
}
|
||||
if (handle_) {
|
||||
CloseHandle(handle_);
|
||||
handle_ = nullptr;
|
||||
}
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
#else // POSIX
|
||||
|
||||
bool SharedMemoryRegion::open(const QString &key, size_t size, Mode mode)
|
||||
{
|
||||
close();
|
||||
|
||||
key_ = key;
|
||||
size_ = size;
|
||||
mode_ = mode;
|
||||
|
||||
// POSIX shared memory names must start with a single slash and contain no others.
|
||||
shm_name_ = QStringLiteral("/") + QString(key).replace('/', '_');
|
||||
const QByteArray name_bytes = shm_name_.toUtf8();
|
||||
|
||||
int oflag = O_RDWR;
|
||||
if (mode == k_create) {
|
||||
oflag |= O_CREAT | O_EXCL;
|
||||
// Clear any stale segment left by a crashed previous run with the same name.
|
||||
shm_unlink(name_bytes.constData());
|
||||
}
|
||||
|
||||
fd_ = shm_open(name_bytes.constData(), oflag, 0600);
|
||||
if (fd_ < 0) {
|
||||
error_ = QStringLiteral("shm_open(%1) failed: %2")
|
||||
.arg(shm_name_, QString::fromUtf8(strerror(errno)));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode == k_create) {
|
||||
if (ftruncate(fd_, off_t(size)) != 0) {
|
||||
error_ = QStringLiteral("ftruncate failed: %1")
|
||||
.arg(QString::fromUtf8(strerror(errno)));
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
shm_unlink(name_bytes.constData());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// mmap() succeeds even beyond the real segment size and only faults
|
||||
// (SIGBUS) on access, so verify the segment is large enough up front.
|
||||
struct stat st;
|
||||
if (fstat(fd_, &st) != 0) {
|
||||
error_ = QStringLiteral("fstat failed: %1")
|
||||
.arg(QString::fromUtf8(strerror(errno)));
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
return false;
|
||||
}
|
||||
if (st.st_size < off_t(size)) {
|
||||
error_ = QStringLiteral(
|
||||
"shared memory segment is %1 bytes, smaller than the requested %2")
|
||||
.arg(qint64(st.st_size))
|
||||
.arg(qint64(size));
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
data_ = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0);
|
||||
if (data_ == MAP_FAILED) {
|
||||
error_ = QStringLiteral("mmap failed: %1")
|
||||
.arg(QString::fromUtf8(strerror(errno)));
|
||||
data_ = nullptr;
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
if (mode == k_create) {
|
||||
shm_unlink(name_bytes.constData());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode == k_create) {
|
||||
memset(data_, 0, size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SharedMemoryRegion::close()
|
||||
{
|
||||
if (data_) {
|
||||
munmap(data_, size_);
|
||||
data_ = nullptr;
|
||||
}
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
}
|
||||
if (mode_ == k_create && !shm_name_.isEmpty()) {
|
||||
// Only the owner unlinks, so the name is freed once both sides have unmapped.
|
||||
shm_unlink(shm_name_.toUtf8().constData());
|
||||
shm_name_.clear();
|
||||
}
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
@@ -0,0 +1,123 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_IPC_SHAREDMEMORYREGION_H
|
||||
#define OAK_IPC_SHAREDMEMORYREGION_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <QString>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief A named, fixed-size shared memory segment mapped into the process address space.
|
||||
*
|
||||
* One process Create()s the segment (owner); the peer process Attach()es to it by the same key.
|
||||
* The mapping is a raw contiguous byte range accessible via data() — the IPC ring buffers and frame
|
||||
* slot pools are laid out inside it. Nothing here is locked; synchronization is entirely the
|
||||
* caller's responsibility via the lock-free structures placed in the mapping.
|
||||
*
|
||||
* We deliberately use the raw OS primitives (POSIX shm_open + mmap, Windows CreateFileMapping +
|
||||
* MapViewOfFile) rather than QSharedMemory: QSharedMemory carries an implicit semaphore and a 1-byte
|
||||
* header convention, attaches/detaches with reference counting we don't want, and historically has
|
||||
* cross-platform lifetime quirks. For a render pipeline pushing large frames we want a plain mmap.
|
||||
*/
|
||||
class SharedMemoryRegion {
|
||||
public:
|
||||
enum Mode {
|
||||
/// Create (and own) the segment. Fails if it already exists; unlinks on destruction.
|
||||
k_create,
|
||||
/// Attach to a segment created by the peer. Does not unlink on destruction.
|
||||
k_attach
|
||||
};
|
||||
|
||||
SharedMemoryRegion();
|
||||
~SharedMemoryRegion();
|
||||
|
||||
SharedMemoryRegion(const SharedMemoryRegion &) = delete;
|
||||
SharedMemoryRegion &operator=(const SharedMemoryRegion &) = delete;
|
||||
|
||||
/**
|
||||
* @brief Open the segment identified by `key` with the given `size` in bytes.
|
||||
*
|
||||
* `key` is a short identifier (no leading slash needed; the platform prefix is added internally).
|
||||
* Returns true on success. On failure, error() carries a human-readable reason.
|
||||
*/
|
||||
bool open(const QString &key, size_t size, Mode mode);
|
||||
|
||||
/**
|
||||
* @brief Unmap and (if owner) unlink the segment. Called automatically by the destructor.
|
||||
*/
|
||||
void close();
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return data_ != nullptr;
|
||||
}
|
||||
|
||||
void *data() const
|
||||
{
|
||||
return data_;
|
||||
}
|
||||
|
||||
size_t size() const
|
||||
{
|
||||
return size_;
|
||||
}
|
||||
|
||||
const QString &key() const
|
||||
{
|
||||
return key_;
|
||||
}
|
||||
|
||||
const QString &error() const
|
||||
{
|
||||
return error_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a unique segment key for a worker, e.g. "olive-rw-<pid>-<index>".
|
||||
*
|
||||
* Centralized so the owner and the spawned worker agree on the same name.
|
||||
*/
|
||||
static QString make_key(qint64 owner_pid, int worker_index);
|
||||
|
||||
private:
|
||||
QString key_;
|
||||
size_t size_;
|
||||
void *data_;
|
||||
Mode mode_;
|
||||
QString error_;
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
void *handle_; // HANDLE from CreateFileMapping/OpenFileMapping
|
||||
#else
|
||||
int fd_; // file descriptor from shm_open
|
||||
QString shm_name_; // the platform-prefixed name actually passed to shm_open
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
|
||||
#endif // OAK_IPC_SHAREDMEMORYREGION_H
|
||||
@@ -0,0 +1,185 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_IPC_SPSCRINGBUFFER_H
|
||||
#define OAK_IPC_SPSCRINGBUFFER_H
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief A lock-free single-producer / single-consumer ring buffer of uint32 indices.
|
||||
*
|
||||
* This is the core synchronization primitive for cross-process communication. It is designed to
|
||||
* live in a shared memory segment: the control block (head/tail cursors) and the slot array are a
|
||||
* single trivially-copyable, self-contained POD. Both the producer process and the consumer
|
||||
* process map the same memory and operate on it concurrently.
|
||||
*
|
||||
* Correctness relies on the classic SPSC invariant:
|
||||
* - Exactly ONE thread/process calls Push() (the producer).
|
||||
* - Exactly ONE thread/process calls Pop() (the consumer).
|
||||
* Under that invariant, no mutex is required. The producer only ever writes `head_`, the consumer
|
||||
* only ever writes `tail_`, and the acquire/release ordering on those two atomics publishes the
|
||||
* slot writes safely across the process boundary.
|
||||
*
|
||||
* Capacity note: one slot is always left empty to disambiguate the full and empty states, so a
|
||||
* buffer constructed with kCapacity slots can hold at most (kCapacity - 1) live entries.
|
||||
*
|
||||
* The payload stored is a single uint32_t per entry, intended to be an index into a separately
|
||||
* managed slot pool (see FrameSlotPool). We never put pointers in shared memory.
|
||||
*/
|
||||
class SpscRingBuffer {
|
||||
public:
|
||||
/**
|
||||
* @brief In-place construct a ring buffer header at `mem` with `capacity` index slots.
|
||||
*
|
||||
* `mem` must point to at least BytesNeeded(capacity) bytes of zero-initializable memory. This is
|
||||
* intended to be placement-style initialization performed exactly once by whichever process owns
|
||||
* the segment's creation; the peer process uses Attach() instead.
|
||||
*/
|
||||
static SpscRingBuffer *create(void *mem, uint32_t capacity)
|
||||
{
|
||||
auto *self = reinterpret_cast<SpscRingBuffer *>(mem);
|
||||
self->capacity_ = capacity;
|
||||
self->head_.store(0, std::memory_order_relaxed);
|
||||
self->tail_.store(0, std::memory_order_relaxed);
|
||||
for (uint32_t i = 0; i < capacity; i++) {
|
||||
self->slot_array()[i] = 0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Re-interpret already-initialized memory as a ring buffer (peer process side).
|
||||
*
|
||||
* No writes are performed; the cursors and capacity are assumed already set by Create().
|
||||
*/
|
||||
static SpscRingBuffer *attach(void *mem)
|
||||
{
|
||||
return reinterpret_cast<SpscRingBuffer *>(mem);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Total bytes required to hold the header plus `capacity` index slots.
|
||||
*/
|
||||
static size_t bytes_needed(uint32_t capacity)
|
||||
{
|
||||
return sizeof(SpscRingBuffer) + size_t(capacity) * sizeof(uint32_t);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Producer side: enqueue an index. Returns false if the buffer is full.
|
||||
*/
|
||||
bool push(uint32_t value)
|
||||
{
|
||||
const uint32_t head = head_.load(std::memory_order_relaxed);
|
||||
const uint32_t next = increment(head);
|
||||
|
||||
// Buffer is full if advancing head would collide with the consumer's tail.
|
||||
if (next == tail_.load(std::memory_order_acquire)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
slot_array()[head] = value;
|
||||
head_.store(next, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Consumer side: dequeue an index into `out`. Returns false if the buffer is empty.
|
||||
*/
|
||||
bool pop(uint32_t *out)
|
||||
{
|
||||
const uint32_t tail = tail_.load(std::memory_order_relaxed);
|
||||
|
||||
// Buffer is empty if the consumer has caught up to the producer.
|
||||
if (tail == head_.load(std::memory_order_acquire)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*out = slot_array()[tail];
|
||||
tail_.store(increment(tail), std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Approximate number of entries currently queued.
|
||||
*
|
||||
* Safe to call from either side, but the value may be stale the instant it returns. Intended for
|
||||
* metrics/backpressure heuristics, not for correctness decisions.
|
||||
*/
|
||||
uint32_t size_approx() const
|
||||
{
|
||||
const uint32_t head = head_.load(std::memory_order_acquire);
|
||||
const uint32_t tail = tail_.load(std::memory_order_acquire);
|
||||
return (head + capacity_ - tail) % capacity_;
|
||||
}
|
||||
|
||||
bool is_empty_approx() const
|
||||
{
|
||||
return head_.load(std::memory_order_acquire) ==
|
||||
tail_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
uint32_t capacity() const
|
||||
{
|
||||
return capacity_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t increment(uint32_t index) const
|
||||
{
|
||||
// capacity_ is small and this avoids requiring a power-of-two capacity.
|
||||
return (index + 1) % capacity_;
|
||||
}
|
||||
|
||||
// The index slot array is allocated immediately after this struct in the same contiguous block.
|
||||
// (Named slot_array() rather than slots() to avoid Qt's `slots` keyword macro.)
|
||||
uint32_t *slot_array()
|
||||
{
|
||||
return reinterpret_cast<uint32_t *>(this + 1);
|
||||
}
|
||||
const uint32_t *slot_array() const
|
||||
{
|
||||
return reinterpret_cast<const uint32_t *>(this + 1);
|
||||
}
|
||||
|
||||
// Producer writes head_, consumer writes tail_. Kept on separate cache lines would be ideal, but
|
||||
// since these live in shared memory with a trailing flexible array we keep the header compact and
|
||||
// rely on acquire/release ordering for correctness.
|
||||
std::atomic<uint32_t> head_;
|
||||
std::atomic<uint32_t> tail_;
|
||||
uint32_t capacity_;
|
||||
|
||||
static_assert(
|
||||
sizeof(std::atomic<uint32_t>) == sizeof(uint32_t),
|
||||
"atomic<uint32_t> must be lock-free POD-sized for shared memory use");
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
|
||||
#endif // OAK_IPC_SPSCRINGBUFFER_H
|
||||
@@ -0,0 +1,29 @@
|
||||
# Olive - Non-Linear Video Editor
|
||||
# Copyright (C) 2022 Olive Team
|
||||
# Modifications Copyright (C) 2025 mikesolar
|
||||
#
|
||||
# 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/job/acceleratedjob.cpp
|
||||
render/job/acceleratedjob.h
|
||||
render/job/footagejob.h
|
||||
render/job/generatejob.h
|
||||
render/job/samplejob.h
|
||||
render/job/shaderjob.h
|
||||
render/job/pluginjob.h
|
||||
render/job/pluginjob.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "acceleratedjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_ACCELERATEDJOB_H
|
||||
#define OAK_ACCELERATEDJOB_H
|
||||
|
||||
#include "node/param.h"
|
||||
#include "node/valuedatabase.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class AcceleratedJob {
|
||||
public:
|
||||
AcceleratedJob() = default;
|
||||
|
||||
virtual ~AcceleratedJob()
|
||||
{
|
||||
}
|
||||
|
||||
virtual NodeValue get(const QString &input) const
|
||||
{
|
||||
return value_map_.value(input);
|
||||
}
|
||||
|
||||
virtual void insert(const QString &input, const NodeValueRow &row)
|
||||
{
|
||||
value_map_.insert(input, row.value(input));
|
||||
}
|
||||
|
||||
virtual void insert(const QString &input, const NodeValue &value)
|
||||
{
|
||||
value_map_.insert(input, value);
|
||||
}
|
||||
|
||||
virtual void insert(const NodeValueRow &row)
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
value_map_.insert(row);
|
||||
#else
|
||||
for (auto it = row.cbegin(); it != row.cend(); it++) {
|
||||
value_map_.insert(it.key(), it.value());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
virtual const NodeValueRow &get_values() const
|
||||
{
|
||||
return value_map_;
|
||||
}
|
||||
virtual NodeValueRow &get_values()
|
||||
{
|
||||
return value_map_;
|
||||
}
|
||||
|
||||
protected:
|
||||
NodeValueRow value_map_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_ACCELERATEDJOB_H
|
||||
@@ -0,0 +1,68 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_CACHEJOB_H
|
||||
#define OAK_CACHEJOB_H
|
||||
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
|
||||
#include "node/value.h"
|
||||
#include "render/job/acceleratedjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class CacheJob : public AcceleratedJob {
|
||||
public:
|
||||
CacheJob() = default;
|
||||
CacheJob(const QString &filename, const NodeValue &fallback = NodeValue())
|
||||
{
|
||||
filename_ = filename;
|
||||
}
|
||||
|
||||
const QString &get_filename() const
|
||||
{
|
||||
return filename_;
|
||||
}
|
||||
void set_filename(const QString &s)
|
||||
{
|
||||
filename_ = s;
|
||||
}
|
||||
|
||||
const NodeValue &get_fallback() const
|
||||
{
|
||||
return fallback_;
|
||||
}
|
||||
void set_fallback(const NodeValue &val)
|
||||
{
|
||||
fallback_ = val;
|
||||
}
|
||||
|
||||
private:
|
||||
QString filename_;
|
||||
|
||||
NodeValue fallback_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_CACHEJOB_H
|
||||
@@ -0,0 +1,184 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_COLORTRANSFORMJOB_H
|
||||
#define OAK_COLORTRANSFORMJOB_H
|
||||
|
||||
#include <QMatrix4x4>
|
||||
#include <QString>
|
||||
|
||||
#include "acceleratedjob.h"
|
||||
#include "render/alphaassoc.h"
|
||||
#include "render/colorprocessor.h"
|
||||
#include "render/texture.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class Node;
|
||||
|
||||
class ColorTransformJob : public AcceleratedJob {
|
||||
public:
|
||||
ColorTransformJob()
|
||||
{
|
||||
processor_ = nullptr;
|
||||
custom_shader_src_ = nullptr;
|
||||
input_alpha_association_ = k_alpha_none;
|
||||
clear_destination_ = true;
|
||||
force_opaque_ = false;
|
||||
}
|
||||
|
||||
ColorTransformJob(const NodeValueRow &row)
|
||||
: ColorTransformJob()
|
||||
{
|
||||
insert(row);
|
||||
}
|
||||
|
||||
QString id() const
|
||||
{
|
||||
if (id_.isEmpty()) {
|
||||
return processor_->id();
|
||||
} else {
|
||||
return id_;
|
||||
}
|
||||
}
|
||||
|
||||
void set_override_id(const QString &id)
|
||||
{
|
||||
id_ = id;
|
||||
}
|
||||
|
||||
const NodeValue &get_input_texture() const
|
||||
{
|
||||
return input_texture_;
|
||||
}
|
||||
void set_input_texture(const NodeValue &tex)
|
||||
{
|
||||
input_texture_ = tex;
|
||||
}
|
||||
void set_input_texture(TexturePtr tex)
|
||||
{
|
||||
Q_ASSERT(!tex->is_dummy());
|
||||
input_texture_ = NodeValue(NodeValue::k_texture, tex);
|
||||
}
|
||||
|
||||
ColorProcessorPtr get_color_processor() const
|
||||
{
|
||||
return processor_;
|
||||
}
|
||||
void set_color_processor(ColorProcessorPtr p)
|
||||
{
|
||||
processor_ = p;
|
||||
}
|
||||
|
||||
const AlphaAssociated &get_input_alpha_association() const
|
||||
{
|
||||
return input_alpha_association_;
|
||||
}
|
||||
void set_input_alpha_association(const AlphaAssociated &e)
|
||||
{
|
||||
input_alpha_association_ = e;
|
||||
}
|
||||
|
||||
const Node *custom_shader_source() const
|
||||
{
|
||||
return custom_shader_src_;
|
||||
}
|
||||
const QString &custom_shader_id() const
|
||||
{
|
||||
return custom_shader_id_;
|
||||
}
|
||||
void set_needs_custom_shader(const Node *node, const QString &id = QString())
|
||||
{
|
||||
custom_shader_src_ = node;
|
||||
custom_shader_id_ = id;
|
||||
}
|
||||
|
||||
bool is_clear_destination_enabled() const
|
||||
{
|
||||
return clear_destination_;
|
||||
}
|
||||
void set_clear_destination_enabled(bool e)
|
||||
{
|
||||
clear_destination_ = e;
|
||||
}
|
||||
|
||||
const QMatrix4x4 &get_transform_matrix() const
|
||||
{
|
||||
return matrix_;
|
||||
}
|
||||
void set_transform_matrix(const QMatrix4x4 &m)
|
||||
{
|
||||
matrix_ = m;
|
||||
}
|
||||
|
||||
const QMatrix4x4 &get_crop_matrix() const
|
||||
{
|
||||
return crop_matrix_;
|
||||
}
|
||||
void set_crop_matrix(const QMatrix4x4 &m)
|
||||
{
|
||||
crop_matrix_ = m;
|
||||
}
|
||||
|
||||
const QString &get_function_name() const
|
||||
{
|
||||
return function_name_;
|
||||
}
|
||||
void set_function_name(const QString &function_name = QString())
|
||||
{
|
||||
function_name_ = function_name;
|
||||
};
|
||||
|
||||
bool get_force_opaque() const
|
||||
{
|
||||
return force_opaque_;
|
||||
}
|
||||
void set_force_opaque(bool e)
|
||||
{
|
||||
force_opaque_ = e;
|
||||
}
|
||||
|
||||
private:
|
||||
ColorProcessorPtr processor_;
|
||||
QString id_;
|
||||
|
||||
NodeValue input_texture_;
|
||||
|
||||
const Node *custom_shader_src_;
|
||||
QString custom_shader_id_;
|
||||
|
||||
AlphaAssociated input_alpha_association_;
|
||||
|
||||
bool clear_destination_;
|
||||
|
||||
QMatrix4x4 matrix_;
|
||||
|
||||
QMatrix4x4 crop_matrix_;
|
||||
|
||||
QString function_name_;
|
||||
|
||||
bool force_opaque_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_COLORTRANSFORMJOB_H
|
||||
@@ -0,0 +1,196 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_FOOTAGEJOB_H
|
||||
#define OAK_FOOTAGEJOB_H
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "node/project/footage/footage.h"
|
||||
#include "render/rendermodes.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class FootageJob : public AcceleratedJob {
|
||||
public:
|
||||
FootageJob()
|
||||
: type_(Track::k_none)
|
||||
{
|
||||
}
|
||||
|
||||
FootageJob(const TimeRange &time, const QString &decoder,
|
||||
const QString &filename, Track::Type type,
|
||||
const Rational &length, LoopMode loop_mode)
|
||||
: time_(time)
|
||||
, decoder_(decoder)
|
||||
, filename_(filename)
|
||||
, type_(type)
|
||||
, length_(length)
|
||||
, loop_mode_(loop_mode)
|
||||
{
|
||||
}
|
||||
|
||||
const QString &decoder() const
|
||||
{
|
||||
return decoder_;
|
||||
}
|
||||
|
||||
const QString &filename() const
|
||||
{
|
||||
return filename_;
|
||||
}
|
||||
|
||||
bool has_proxy() const
|
||||
{
|
||||
return has_proxy_;
|
||||
}
|
||||
|
||||
const QString &proxy_filename() const
|
||||
{
|
||||
return proxy_filename_;
|
||||
}
|
||||
|
||||
const QString &proxy_decoder() const
|
||||
{
|
||||
return proxy_decoder_;
|
||||
}
|
||||
|
||||
int proxy_stream_index() const
|
||||
{
|
||||
return proxy_stream_index_;
|
||||
}
|
||||
|
||||
void set_proxy(const QString &filename, const QString &decoder,
|
||||
int stream_index)
|
||||
{
|
||||
proxy_filename_ = filename;
|
||||
proxy_decoder_ = decoder;
|
||||
proxy_stream_index_ = stream_index;
|
||||
has_proxy_ = !filename.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether decoding for the given render mode should use the proxy
|
||||
*
|
||||
* Proxies are a preview accelerator only: offline (realtime preview)
|
||||
* renders may decode from them, online (export/master) renders must
|
||||
* always decode the original media. The proxy file must also still
|
||||
* exist on disk, otherwise decoding falls back to the original.
|
||||
*/
|
||||
bool should_use_proxy(RenderMode::Mode mode) const
|
||||
{
|
||||
return mode == RenderMode::k_offline && has_proxy() &&
|
||||
QFileInfo::exists(proxy_filename_);
|
||||
}
|
||||
|
||||
Track::Type type() const
|
||||
{
|
||||
return type_;
|
||||
}
|
||||
|
||||
const VideoParams &video_params() const
|
||||
{
|
||||
return video_params_;
|
||||
}
|
||||
|
||||
void set_video_params(const VideoParams &p)
|
||||
{
|
||||
video_params_ = p;
|
||||
}
|
||||
|
||||
const AudioParams &audio_params() const
|
||||
{
|
||||
return audio_params_;
|
||||
}
|
||||
|
||||
void set_audio_params(const AudioParams &p)
|
||||
{
|
||||
audio_params_ = p;
|
||||
}
|
||||
|
||||
const QString &cache_path() const
|
||||
{
|
||||
return cache_path_;
|
||||
}
|
||||
|
||||
void set_cache_path(const QString &p)
|
||||
{
|
||||
cache_path_ = p;
|
||||
}
|
||||
|
||||
const Rational &length() const
|
||||
{
|
||||
return length_;
|
||||
}
|
||||
|
||||
void set_length(const Rational &length)
|
||||
{
|
||||
length_ = length;
|
||||
}
|
||||
|
||||
const TimeRange &time() const
|
||||
{
|
||||
return time_;
|
||||
}
|
||||
|
||||
LoopMode loop_mode() const
|
||||
{
|
||||
return loop_mode_;
|
||||
}
|
||||
void set_loop_mode(LoopMode m)
|
||||
{
|
||||
loop_mode_ = m;
|
||||
}
|
||||
|
||||
private:
|
||||
TimeRange time_;
|
||||
|
||||
QString decoder_;
|
||||
|
||||
QString filename_;
|
||||
|
||||
bool has_proxy_ = false;
|
||||
|
||||
QString proxy_filename_;
|
||||
|
||||
QString proxy_decoder_;
|
||||
|
||||
int proxy_stream_index_ = -1;
|
||||
|
||||
Track::Type type_;
|
||||
|
||||
VideoParams video_params_;
|
||||
|
||||
AudioParams audio_params_;
|
||||
|
||||
QString cache_path_;
|
||||
|
||||
Rational length_;
|
||||
|
||||
LoopMode loop_mode_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(olive::FootageJob)
|
||||
|
||||
#endif // OAK_FOOTAGEJOB_H
|
||||
@@ -0,0 +1,43 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_GENERATEJOB_H
|
||||
#define OAK_GENERATEJOB_H
|
||||
|
||||
#include "acceleratedjob.h"
|
||||
#include "codec/frame.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class GenerateJob : public AcceleratedJob {
|
||||
public:
|
||||
GenerateJob() = default;
|
||||
GenerateJob(const NodeValueRow &row)
|
||||
: GenerateJob()
|
||||
{
|
||||
insert(row);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_GENERATEJOB_H
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Oak Video Editor - Non-Linear Video Editor
|
||||
* Copyright (C) 2025 Olive CE 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 "pluginjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
} // plugin
|
||||
} // olive
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Oak Video Editor - Non-Linear Video Editor
|
||||
* Copyright (C) 2025 Olive CE 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 OAK_PLUGINJOB_H
|
||||
#define OAK_PLUGINJOB_H
|
||||
#include "acceleratedjob.h"
|
||||
#include "pluginSupport/oliveplugininstance.h"
|
||||
#include "olive/core/util/rational.h"
|
||||
|
||||
#include <any>
|
||||
#include <chrono>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
|
||||
class PluginJob : public AcceleratedJob {
|
||||
public:
|
||||
explicit PluginJob(const OFX::Host::ImageEffect::Instance *plugin_instance,
|
||||
const PluginNode *node, NodeValueRow row,
|
||||
const olive::core::Rational &time)
|
||||
: AcceleratedJob()
|
||||
, time_seconds_(time.to_double())
|
||||
{
|
||||
this->pluginInstance_ = plugin_instance;
|
||||
this->node_ = node;
|
||||
insert(row);
|
||||
}
|
||||
explicit PluginJob(const OFX::Host::ImageEffect::Instance *plugin_instance,
|
||||
const PluginNode *node, NodeValueRow row)
|
||||
: PluginJob(plugin_instance, node, row, olive::core::Rational(0))
|
||||
{
|
||||
}
|
||||
|
||||
PluginNode *node() const
|
||||
{
|
||||
return const_cast<PluginNode *>(node_);
|
||||
}
|
||||
|
||||
OFX::Host::ImageEffect::Instance *plugin_instance()
|
||||
{
|
||||
return const_cast<OFX::Host::ImageEffect::Instance *>(pluginInstance_);
|
||||
}
|
||||
|
||||
double time_seconds() const
|
||||
{
|
||||
return time_seconds_;
|
||||
}
|
||||
|
||||
private:
|
||||
const OFX::Host::ImageEffect::Instance *pluginInstance_ = nullptr;
|
||||
|
||||
QHash<OfxTime, QHash<QString, std::any>> paramsOnTime_;
|
||||
|
||||
QHash<QString, std::any> params_;
|
||||
|
||||
const PluginNode *node_ = nullptr;
|
||||
double time_seconds_ = 0.0;
|
||||
};
|
||||
|
||||
} // plugin
|
||||
} // olive
|
||||
|
||||
#endif //OAK_PLUGINJOB_H
|
||||
@@ -0,0 +1,74 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_SAMPLEJOB_H
|
||||
#define OAK_SAMPLEJOB_H
|
||||
|
||||
#include "acceleratedjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class SampleJob : public AcceleratedJob {
|
||||
public:
|
||||
SampleJob()
|
||||
{
|
||||
}
|
||||
|
||||
SampleJob(const TimeRange &time, const NodeValue &value)
|
||||
{
|
||||
samples_ = value.to_samples();
|
||||
time_ = time;
|
||||
}
|
||||
|
||||
SampleJob(const TimeRange &time, const QString &from,
|
||||
const NodeValueRow &row)
|
||||
{
|
||||
samples_ = row[from].to_samples();
|
||||
time_ = time;
|
||||
}
|
||||
|
||||
const SampleBuffer &samples() const
|
||||
{
|
||||
return samples_;
|
||||
}
|
||||
|
||||
bool has_samples() const
|
||||
{
|
||||
return samples_.is_allocated();
|
||||
}
|
||||
|
||||
const TimeRange &time() const
|
||||
{
|
||||
return time_;
|
||||
}
|
||||
|
||||
private:
|
||||
SampleBuffer samples_;
|
||||
|
||||
TimeRange time_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(olive::SampleJob)
|
||||
|
||||
#endif // OAK_SAMPLEJOB_H
|
||||
@@ -0,0 +1,123 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_SHADERJOB_H
|
||||
#define OAK_SHADERJOB_H
|
||||
|
||||
#include <QMatrix4x4>
|
||||
#include <QVector>
|
||||
|
||||
#include "acceleratedjob.h"
|
||||
#include "render/texture.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class ShaderJob : public AcceleratedJob {
|
||||
public:
|
||||
ShaderJob()
|
||||
{
|
||||
iterations_ = 1;
|
||||
iterative_input_ = nullptr;
|
||||
}
|
||||
|
||||
ShaderJob(const NodeValueRow &row)
|
||||
: ShaderJob()
|
||||
{
|
||||
insert(row);
|
||||
}
|
||||
|
||||
const QString &get_shader_id() const
|
||||
{
|
||||
return shader_id_;
|
||||
}
|
||||
|
||||
void set_shader_id(const QString &id)
|
||||
{
|
||||
shader_id_ = id;
|
||||
}
|
||||
|
||||
void set_iterations(int iterations, const NodeInput &iterative_input)
|
||||
{
|
||||
set_iterations(iterations, iterative_input.input());
|
||||
}
|
||||
|
||||
void set_iterations(int iterations, const QString &iterative_input)
|
||||
{
|
||||
iterations_ = iterations;
|
||||
iterative_input_ = iterative_input;
|
||||
}
|
||||
|
||||
int get_iteration_count() const
|
||||
{
|
||||
return iterations_;
|
||||
}
|
||||
|
||||
const QString &get_iterative_input() const
|
||||
{
|
||||
return iterative_input_;
|
||||
}
|
||||
|
||||
Texture::Interpolation get_interpolation(const QString &id) const
|
||||
{
|
||||
return interpolation_.value(id, Texture::k_default_interpolation);
|
||||
}
|
||||
|
||||
const QHash<QString, Texture::Interpolation> &get_interpolation_map() const
|
||||
{
|
||||
return interpolation_;
|
||||
}
|
||||
|
||||
void set_interpolation(const NodeInput &input, Texture::Interpolation interp)
|
||||
{
|
||||
interpolation_.insert(input.input(), interp);
|
||||
}
|
||||
|
||||
void set_interpolation(const QString &id, Texture::Interpolation interp)
|
||||
{
|
||||
interpolation_.insert(id, interp);
|
||||
}
|
||||
|
||||
void set_vertex_coordinates(const QVector<float> &vertex_coords)
|
||||
{
|
||||
vertex_overrides_ = vertex_coords;
|
||||
}
|
||||
|
||||
const QVector<float> &get_vertex_coordinates()
|
||||
{
|
||||
return vertex_overrides_;
|
||||
}
|
||||
|
||||
private:
|
||||
QString shader_id_;
|
||||
|
||||
int iterations_;
|
||||
|
||||
QString iterative_input_;
|
||||
|
||||
QHash<QString, Texture::Interpolation> interpolation_;
|
||||
|
||||
QVector<float> vertex_overrides_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_SHADERJOB_H
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Oak Video Editor - Non-Linear Video Editor
|
||||
* Copyright (C) 2025 Olive CE 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 OAK_LOOPMODE_H
|
||||
#define OAK_LOOPMODE_H
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
enum class LoopMode { k_loop_mode_off, k_loop_mode_loop, k_loop_mode_clamp };
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_LOOPMODE_H
|
||||
@@ -0,0 +1,96 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 "lutlibrary.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "config/config.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
const QStringList &LUTLibrary::supported_extensions()
|
||||
{
|
||||
// LUT formats OCIO FileTransform can load
|
||||
static const QStringList extensions = {
|
||||
QStringLiteral("cube"), QStringLiteral("3dl"), QStringLiteral("spi1d"),
|
||||
QStringLiteral("spi3d"), QStringLiteral("spimtx"), QStringLiteral("csp"),
|
||||
QStringLiteral("clf"), QStringLiteral("ctf"), QStringLiteral("cub"),
|
||||
};
|
||||
return extensions;
|
||||
}
|
||||
|
||||
bool LUTLibrary::is_supported_extension(const QString &suffix)
|
||||
{
|
||||
QString s = suffix;
|
||||
if (s.startsWith(QLatin1Char('.'))) {
|
||||
s.remove(0, 1);
|
||||
}
|
||||
|
||||
return supported_extensions().contains(s.toLower());
|
||||
}
|
||||
|
||||
QStringList LUTLibrary::get_directories()
|
||||
{
|
||||
const QString serialized = OAK_CONFIG("LUTLibraryPaths").toString();
|
||||
|
||||
QStringList dirs = serialized.split(QLatin1Char(';'), Qt::SkipEmptyParts);
|
||||
for (QString &dir : dirs) {
|
||||
dir = QDir::fromNativeSeparators(dir.trimmed());
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
void LUTLibrary::set_directories(const QStringList &dirs)
|
||||
{
|
||||
QStringList cleaned;
|
||||
for (const QString &dir : dirs) {
|
||||
const QString trimmed = dir.trimmed();
|
||||
if (!trimmed.isEmpty() && !cleaned.contains(trimmed)) {
|
||||
cleaned.append(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
Config::current()[QStringLiteral("LUTLibraryPaths")] =
|
||||
cleaned.join(QLatin1Char(';'));
|
||||
}
|
||||
|
||||
QStringList LUTLibrary::get_lut_files()
|
||||
{
|
||||
QStringList files;
|
||||
|
||||
static const QStringList k_filters = { QStringLiteral("*.cube"),
|
||||
QStringLiteral("*.3dl") };
|
||||
|
||||
for (const QString &dir : get_directories()) {
|
||||
QDirIterator it(dir, k_filters, QDir::Files,
|
||||
QDirIterator::Subdirectories);
|
||||
while (it.hasNext()) {
|
||||
files.append(it.next());
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_LUTLIBRARY_H
|
||||
#define OAK_LUTLIBRARY_H
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief A global, user-configurable library of LUT files
|
||||
*
|
||||
* The library is a list of directories (stored in the application config
|
||||
* under "LUTLibraryPaths") that are scanned for supported LUT files. LUT
|
||||
* nodes can offer the library contents as quick picks instead of forcing
|
||||
* the user to browse for a file path on every node.
|
||||
*/
|
||||
class LUTLibrary {
|
||||
public:
|
||||
/**
|
||||
* @brief All LUT file extensions supported by the library
|
||||
*
|
||||
* Extensions OCIO FileTransform can load, lowercase, without the dot.
|
||||
*/
|
||||
static const QStringList &supported_extensions();
|
||||
|
||||
/**
|
||||
* @brief Returns true if the given file suffix is a supported LUT
|
||||
* extension (case-insensitive, leading dot tolerated)
|
||||
*/
|
||||
static bool is_supported_extension(const QString &suffix);
|
||||
|
||||
/**
|
||||
* @brief The directories that make up the LUT library
|
||||
*/
|
||||
static QStringList get_directories();
|
||||
|
||||
/**
|
||||
* @brief Replaces the LUT library directories and saves them to the
|
||||
* application config
|
||||
*/
|
||||
static void set_directories(const QStringList &dirs);
|
||||
|
||||
/**
|
||||
* @brief All supported LUT files found under the library directories
|
||||
*
|
||||
* Directories are scanned recursively. Files in earlier directories
|
||||
* are listed first.
|
||||
*/
|
||||
static QStringList get_lut_files();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_LUTLIBRARY_H
|
||||
@@ -0,0 +1,68 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "managedcolor.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
ManagedColor::ManagedColor()
|
||||
{
|
||||
}
|
||||
|
||||
ManagedColor::ManagedColor(const double &r, const double &g, const double &b,
|
||||
const double &a)
|
||||
: Color(r, g, b, a)
|
||||
{
|
||||
}
|
||||
|
||||
ManagedColor::ManagedColor(const char *data, const PixelFormat &format,
|
||||
int channel_layout)
|
||||
: Color(data, format, channel_layout)
|
||||
{
|
||||
}
|
||||
|
||||
ManagedColor::ManagedColor(const Color &c)
|
||||
: Color(c)
|
||||
{
|
||||
}
|
||||
|
||||
const QString &ManagedColor::color_input() const
|
||||
{
|
||||
return color_input_;
|
||||
}
|
||||
|
||||
void ManagedColor::set_color_input(const QString &color_input)
|
||||
{
|
||||
color_input_ = color_input;
|
||||
}
|
||||
|
||||
const ColorTransform &ManagedColor::color_output() const
|
||||
{
|
||||
return color_transform_;
|
||||
}
|
||||
|
||||
void ManagedColor::set_color_output(const ColorTransform &color_output)
|
||||
{
|
||||
color_transform_ = color_output;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_MANAGEDCOLOR_H
|
||||
#define OAK_MANAGEDCOLOR_H
|
||||
|
||||
#include <olive/core/core.h>
|
||||
|
||||
#include "colortransform.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class ManagedColor : public Color {
|
||||
public:
|
||||
ManagedColor();
|
||||
ManagedColor(const double &r, const double &g, const double &b,
|
||||
const double &a = 1.0);
|
||||
ManagedColor(const char *data, const PixelFormat &format,
|
||||
int channel_layout);
|
||||
ManagedColor(const Color &c);
|
||||
|
||||
const QString &color_input() const;
|
||||
void set_color_input(const QString &color_input);
|
||||
|
||||
const ColorTransform &color_output() const;
|
||||
void set_color_output(const ColorTransform &color_output);
|
||||
|
||||
private:
|
||||
QString color_input_;
|
||||
|
||||
ColorTransform color_transform_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_MANAGEDCOLOR_H
|
||||
@@ -0,0 +1,29 @@
|
||||
# Olive - Non-Linear Video Editor
|
||||
# Copyright (C) 2022 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/>.
|
||||
|
||||
file(GLOB_RECURSE OCIOCONF_RESOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.ocio *.spi3d *.spi1d)
|
||||
set(QRC_BODY "")
|
||||
foreach (OCIOCONF_FILE ${OCIOCONF_RESOURCES})
|
||||
string(APPEND QRC_BODY "<file>${OCIOCONF_FILE}</file>\n")
|
||||
configure_file(${OCIOCONF_FILE} ${OCIOCONF_FILE} COPYONLY)
|
||||
endforeach ()
|
||||
configure_file(ocioconf.qrc.in ocioconf.qrc @ONLY)
|
||||
|
||||
set(OLIVE_RESOURCES
|
||||
${OLIVE_RESOURCES}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/ocioconf.qrc
|
||||
PARENT_SCOPE
|
||||
)
|
||||
Executable
+401
@@ -0,0 +1,401 @@
|
||||
# Film Emulsion-like configuration for
|
||||
# Blender. Crafted by Troy James Sobotka with
|
||||
# special thanks, feedback, and knowledge from Guillermo
|
||||
# Espertino, Claudio Rocha, Bassam Kurdali, Eugenio
|
||||
# Pignataro, Henri Hebeisen, Jason Clarke,
|
||||
# Haarm-Peter Duiker, Thomas Mansencal, Andrew
|
||||
# Price, Nick Shaw, and Timothy
|
||||
# Lottes.
|
||||
|
||||
ocio_profile_version: 2
|
||||
|
||||
search_path: "luts:looks"
|
||||
strictparsing: true
|
||||
luma: [0.2126, 0.7152, 0.0722]
|
||||
|
||||
description: A filmlike dynamic range encoding set for Blender
|
||||
|
||||
roles:
|
||||
default: Rec.709 OETF
|
||||
reference: Linear
|
||||
scene_linear: Linear
|
||||
data: Non-Colour Data
|
||||
compositing_log: Filmic Log Encoding
|
||||
color_timing: Filmic Log Encoding
|
||||
default_byte: sRGB OETF
|
||||
default_float: Linear
|
||||
default_sequencer: sRGB OETF
|
||||
color_picking: sRGB OETF
|
||||
texture_paint: sRGB OETF
|
||||
matte_paint: Filmic Log Encoding
|
||||
cie_xyz_d65_interchange: CIE-XYZ D65
|
||||
|
||||
displays:
|
||||
sRGB:
|
||||
- !<View> {name: sRGB OETF, colorspace: sRGB OETF}
|
||||
- !<View> {name: Non-Colour Data, colorspace: Non-Colour Data}
|
||||
- !<View> {name: Linear Raw, colorspace: Linear}
|
||||
- !<View> {name: Filmic Log Encoding Base, colorspace: Filmic Log Encoding}
|
||||
BT.1886:
|
||||
- !<View> {name: BT.1886 EOTF, colorspace: BT.1886 EOTF}
|
||||
- !<View> {name: Non-Colour Data, colorspace: Non-Colour Data}
|
||||
- !<View> {name: Linear Raw, colorspace: Linear}
|
||||
- !<View> {name: Filmic Log Encoding Base, colorspace: BT.1886 Filmic Log Encoding}
|
||||
Apple Display P3:
|
||||
- !<View> {name: sRGB OETF, colorspace: AppleP3 sRGB OETF}
|
||||
- !<View> {name: Non-Colour Data, colorspace: Non-Colour Data}
|
||||
- !<View> {name: Linear Raw, colorspace: Linear}
|
||||
- !<View> {name: Filmic Log Encoding Base, colorspace: AppleP3 Filmic Log Encoding}
|
||||
|
||||
active_displays: [sRGB, BT.1886, Apple Display P3, None]
|
||||
#active_views: [Filmic Log Encoding Base, sRGB OETF, Non-Colour Data, Linear Raw, No View]
|
||||
|
||||
inactive_colorspaces: [CIE-XYZ D65]
|
||||
|
||||
colorspaces:
|
||||
- !<ColorSpace>
|
||||
name: Linear
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
ITU BT.709 primaries based scene referred linear space.
|
||||
isdata: false
|
||||
allocation: lg2
|
||||
allocationvars: [-12.4739311883, 12.5260688117, 0.00392156862]
|
||||
|
||||
- !<ColorSpace>
|
||||
name: CIE-XYZ D65
|
||||
family: display
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Linear CIE XYZ space with D65 white point
|
||||
isdata: false
|
||||
allocation: lg2
|
||||
allocationvars: [-12.4739311883, 12.5260688117, 0.00392156862]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<MatrixTransform> {matrix: [0.4124, 0.3576, 0.1805, 0, 0.2126, 0.7152, 0.0722, 0, 0.0193, 0.1192, 0.9505, 0, 0, 0, 0, 1], direction: inverse}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Filmic Log Encoding
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Log based filmic shaper with 16.5 stops of latitude, and 25 stops of dynamic range.
|
||||
isdata: false
|
||||
allocation: lg2
|
||||
allocationvars: [-12.4739311883, 12.5260688117, 0.00392156862]
|
||||
from_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<AllocationTransform> {allocation: lg2, vars: [-12.4739311883, 12.5260688117, 0.00392156862]}
|
||||
- !<FileTransform> {src: desat65cube.spi3d, interpolation: best}
|
||||
- !<AllocationTransform> {allocation: uniform, vars: [0, 0.66]}
|
||||
to_reference: !<AllocationTransform> {allocation: lg2, vars: [-12.4739311883, 4.02606881167, 0.00392156862], direction: inverse}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: sRGB OETF
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
sRGB specification display referred Optical-Electro Transfer Function.
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0.0, 1.0]
|
||||
to_reference: !<FileTransform> {src: sRGB_OETF_to_Linear.spi1d, interpolation: linear}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Apple DCI-P3 D65
|
||||
family: display
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
isdata: false
|
||||
allocation: lg2
|
||||
allocationvars: [-12.4739311883, 12.5260688117, 0.00392156862]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<MatrixTransform> {matrix: [0.515121, 0.291977, 0.157104, 0, 0.241196, 0.692245, 0.0665741, 0, -0.00105286, 0.0418854, 0.784073, 0, 0, 0, 0, 1]}
|
||||
- !<MatrixTransform> {matrix: [1.04788, 0.0229187, -0.0502014, 0, 0.0295868, 0.990479, -0.0170593, 0, -0.00923157, 0.0150757, 0.751678, 0, 0, 0, 0, 1], direction: inverse}
|
||||
- !<MatrixTransform> {matrix: [0.412391, 0.357584, 0.180481, 0, 0.212639, 0.715169, 0.0721923, 0, 0.0193308, 0.119195, 0.950532, 0, 0, 0, 0, 1], direction: inverse}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: AppleP3 sRGB OETF
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
sRGB specification display referred Optical-Electro Transfer Function with Apple DCI-P3 primaries.
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0.0, 1.0]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<FileTransform> {src: sRGB_OETF_to_Linear.spi1d, interpolation: linear}
|
||||
- !<ColorSpaceTransform> {src: Apple DCI-P3 D65, dst: Linear}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: BT.1886 EOTF
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
BT.1886 specification display referred Electro-Optical Transfer Function with REC.709 primaries.
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0.0, 1.0]
|
||||
to_reference: !<ExponentTransform> {value: [2.4, 2.4, 2.4, 1.0]}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: AppleP3 Filmic Log Encoding
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Log based filmic shaper with 16.5 stops of latitude, and 25 stops of dynamic range with Apple P3 primaries.
|
||||
isdata: false
|
||||
allocation: lg2
|
||||
allocationvars: [-12.4739311883, 12.5260688117, 0.00392156862]
|
||||
from_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<ColorSpaceTransform> {src: Linear, dst: Filmic Log Encoding}
|
||||
- !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1.0]}
|
||||
- !<ColorSpaceTransform> {src: Linear, dst: Apple DCI-P3 D65}
|
||||
- !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1.0], direction: inverse}
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1.0]}
|
||||
- !<ColorSpaceTransform> {src: Apple DCI-P3 D65, dst: Linear}
|
||||
- !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1.0], direction: inverse}
|
||||
- !<AllocationTransform> {allocation: lg2, vars: [-12.4739311883, 4.02606881167, 0.00392156862], direction: inverse}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: BT.1886 Filmic Log Encoding
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Log based filmic shaper with 16.5 stops of latitude, and 25 stops of dynamic range with REC.709 primaries.
|
||||
isdata: false
|
||||
allocation: lg2
|
||||
allocationvars: [-12.4739311883, 12.5260688117, 0.00392156862]
|
||||
from_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<ColorSpaceTransform> {src: Linear, dst: Filmic Log Encoding}
|
||||
- !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1.0]}
|
||||
- !<ExponentTransform> {value: [2.4, 2.4, 2.4, 1.0], direction: inverse}
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<ExponentTransform> {value: [2.4, 2.4, 2.4, 1.0]}
|
||||
- !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1.0], direction: inverse}
|
||||
- !<AllocationTransform> {allocation: lg2, vars: [-12.4739311883, 4.02606881167, 0.00392156862], direction: inverse}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Fuji F-Log OETF
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Fuji F-Log transfer function
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<FileTransform> {src: F-Log_to_Linear.spi1d, interpolation: linear}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Fuji F-Log F-Gamut
|
||||
family: ""
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Fuji F-Log / F-Gamut
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<ColorSpaceTransform> {src: Fuji F-Log OETF, dst: Linear}
|
||||
- !<MatrixTransform> {matrix: [0.636958048000, 0.144616904000, 0.168880975000, 0.000000000000, 0.262700212000, 0.677998072000, 0.059301716500, 0.000000000000, 4.994106570E-17, 0.028072693000, 1.060985060000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 1.000000000000]}
|
||||
- !<MatrixTransform> {matrix: [0.412390800000, 0.357584340000, 0.180480790000, 0.000000000000, 0.212639010000, 0.715168680000, 0.072192320000, 0.000000000000, 0.019330820000, 0.119194780000, 0.950532150000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 1.000000000000], direction: inverse}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Panasonic V-Log V-Gamut
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Panasonic V-Log / V-Gamut
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<FileTransform> {src: V-Log_to_linear.spi1d, interpolation: linear}
|
||||
- !<MatrixTransform> {matrix: [1.806576, -0.695697, -0.110879, 0, -0.170090 , 1.305955, -0.135865, 0, -0.025206, -0.154468, 1.179674, 0, 0, 0, 0, 1]}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Arri Wide Gamut / LogC EI 800
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Panasonic V-Log / V-Gamut
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<FileTransform> {src: V3_LogC_800_to_linear.spi1d, interpolation: linear}
|
||||
- !<MatrixTransform> {matrix: [1.617523, -0.537287, -0.080237, 0, -0.070573, 1.334613, -0.26404, 0, -0.021102, -0.226954, 1.248056, 0, 0, 0, 0, 1]}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Arri Wide Gamut / LogC EI 400
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Panasonic V-Log / V-Gamut
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<FileTransform> {src: V3_LogC_400_to_linear.spi1d, interpolation: linear}
|
||||
- !<MatrixTransform> {matrix: [1.617523, -0.537287, -0.080237, 0, -0.070573, 1.334613, -0.26404, 0, -0.021102, -0.226954, 1.248056, 0, 0, 0, 0, 1]}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Arri Wide Gamut 4/ LogC4
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Arri Wide Gamut 4 LogC4 input
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<LogCameraTransform> {log_side_slope: 0.0647954196341293, log_side_offset: -0.295908392682586, lin_side_slope: 2231.82630906769, lin_side_offset: 64, lin_side_break: -0.0180569961199113, direction: inverse}
|
||||
- !<MatrixTransform> {matrix: [1.893123, -0.780882, -0.112242, 0, -0.205700, 1.340257, -0.134557, 0, -0.012706, -0.152185, 1.164891, 0, 0, 0, 0, 1]}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Blackmagic Film Wide Gamut (Gen 5)
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Blackmagic Film Wide Gamut (Gen 5)
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<FileTransform> {src: Blackmagic_FilmWideGamut_Gen5_to_linear.spi1d, interpolation: linear}
|
||||
- !<MatrixTransform> {matrix: [0.606530, 0.220408, 0.123479, 0, 0.267989, 0.832731, -0.100720, 0, -0.029442, -0.086611, 1.204861, 0, 0, 0, 0, 1]}
|
||||
- !<ColorSpaceTransform> {src: CIE-XYZ D65, dst: reference}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Rec.709 OETF
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Rec.709 OETF
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<FileTransform> {src: rec709_to_linear.spi1d, interpolation: linear}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Rec.601 OETF (NTSC)
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Rec.601 Optical-Electro Transfer Function.
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0.0, 1.0]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<MatrixTransform> {matrix: [0.939542, 0.050181, 0.010277, 0, 0.017772, 0.965793, 0.016435, 0, -0.001622, -0.004370, 1.005991, 0, 0, 0, 0, 1]}
|
||||
- !<ColorSpaceTransform> {src: Rec.709 OETF, dst: Linear}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Rec.601 OETF (PAL)
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Rec.601 Optical-Electro Transfer Function.
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0.0, 1.0]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<MatrixTransform> {matrix: [1.044043, -0.044043, 0.000000, 0, 0.000000, 1.000000, -0.000000, 0, -0.000000, 0.011793, 0.988207, 0, 0, 0, 0, 1]}
|
||||
- !<ColorSpaceTransform> {src: Rec.709 OETF, dst: Linear}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Non-Colour Data
|
||||
family:
|
||||
description: |
|
||||
Transform to flag data as non-colour, strictly data, and avoid OCIO colour specific transforms.
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
isdata: true
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
|
||||
looks:
|
||||
- !<Look>
|
||||
name: Greyscale
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<MatrixTransform> {matrix: [0.2126729, 0.7151521, 0.0721750, 0, 0.2126729, 0.7151521, 0.0721750, 0, 0.2126729, 0.7151521, 0.0721750, 0, 0, 0, 0, 1]}
|
||||
|
||||
- !<Look>
|
||||
name: False Colour
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<GroupTransform>
|
||||
children:
|
||||
- !<MatrixTransform> {matrix: [0.2126729, 0.7151521, 0.0721750, 0, 0.2126729, 0.7151521, 0.0721750, 0, 0.2126729, 0.7151521, 0.0721750, 0, 0, 0, 0, 1]}
|
||||
- !<FileTransform> {src: Filmic_False_Colour.spi3d, interpolation: best}
|
||||
|
||||
- !<Look>
|
||||
name: Very High Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_1.20_1-00.spi1d, interpolation: linear}
|
||||
|
||||
- !<Look>
|
||||
name: High Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_0.99_1-0075.spi1d, interpolation: linear}
|
||||
|
||||
- !<Look>
|
||||
name: Medium High Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_0-85_1-011.spi1d, interpolation: best}
|
||||
|
||||
- !<Look>
|
||||
name: Base Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_0-70_1-03.spi1d, interpolation: linear}
|
||||
|
||||
- !<Look>
|
||||
name: Medium Low Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_0-60_1-04.spi1d, interpolation: linear}
|
||||
|
||||
- !<Look>
|
||||
name: Low Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_0-48_1-09.spi1d, interpolation: linear}
|
||||
|
||||
- !<Look>
|
||||
name: Very Low Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_0-35_1-30.spi1d, interpolation: linear}
|
||||
+274628
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+65542
File diff suppressed because it is too large
Load Diff
+65542
File diff suppressed because it is too large
Load Diff
+274628
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
<RCC>
|
||||
<qresource prefix="/ocioconf">
|
||||
@QRC_BODY@
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1,22 @@
|
||||
# Olive - Non-Linear Video Editor
|
||||
# Copyright (C) 2022 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/opengl/openglrenderer.cpp
|
||||
render/opengl/openglrenderer.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1,237 @@
|
||||
#include "render/backend/renderbackend_c.h"
|
||||
|
||||
#include <QOpenGLContext>
|
||||
#include <QPointF>
|
||||
#include <QVariant>
|
||||
|
||||
#include "render/job/acceleratedjob.h"
|
||||
#include "render/opengl/openglrenderer.h"
|
||||
#include "render/shadercode.h"
|
||||
#include "render/texture.h"
|
||||
#include "render/videoparams.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class BackendOpenGLRenderer : public olive::OpenGLRenderer {
|
||||
public:
|
||||
using olive::OpenGLRenderer::OpenGLRenderer;
|
||||
using olive::OpenGLRenderer::blit;
|
||||
using olive::OpenGLRenderer::create_native_texture;
|
||||
using olive::OpenGLRenderer::destroy_internal;
|
||||
using olive::OpenGLRenderer::destroy_native_texture;
|
||||
using olive::OpenGLRenderer::attach_texture_as_destination;
|
||||
using olive::OpenGLRenderer::detach_texture_as_destination;
|
||||
};
|
||||
|
||||
// Converts the opaque C ABI handle back to the C++ renderer used internally.
|
||||
BackendOpenGLRenderer *renderer(OakRenderBackendHandle handle)
|
||||
{
|
||||
return static_cast<BackendOpenGLRenderer *>(handle);
|
||||
}
|
||||
|
||||
// Interprets ABI QVariant payloads without copying; both modules are built
|
||||
// against the same Qt/C++ ABI in this first-generation dynamic backend.
|
||||
const QVariant &variant_ref(const void *variant)
|
||||
{
|
||||
return *static_cast<const QVariant *>(variant);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Creates the backend object and returns it as an opaque C handle.
|
||||
OAK_RENDER_BACKEND_EXPORT OakRenderBackendHandle
|
||||
oak_renderer_create(void *parent)
|
||||
{
|
||||
return new BackendOpenGLRenderer(static_cast<QObject *>(parent));
|
||||
}
|
||||
|
||||
// Destroys the opaque backend object created by oak_renderer_create().
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_destroy(OakRenderBackendHandle handle)
|
||||
{
|
||||
delete renderer(handle);
|
||||
}
|
||||
|
||||
// Reports static OpenGL backend capabilities to the adapter.
|
||||
OAK_RENDER_BACKEND_EXPORT bool
|
||||
oak_renderer_get_info(OakRenderBackendHandle handle,
|
||||
OakRenderBackendInfo *out_info)
|
||||
{
|
||||
if (!handle || !out_info) {
|
||||
return false;
|
||||
}
|
||||
out_info->abi_version = 1;
|
||||
out_info->kind = oak_render_backend_opengl;
|
||||
out_info->capabilities =
|
||||
oak_render_backend_cap_textures | oak_render_backend_cap_shaders |
|
||||
oak_render_backend_cap_blit | oak_render_backend_cap_readback |
|
||||
oak_render_backend_cap_viewer_context;
|
||||
out_info->name = "opengl";
|
||||
out_info->status = "available";
|
||||
return true;
|
||||
}
|
||||
|
||||
// OpenGL availability is context-dependent, so object creation is the minimum
|
||||
// availability signal for this backend.
|
||||
OAK_RENDER_BACKEND_EXPORT bool
|
||||
oak_renderer_is_available(OakRenderBackendHandle handle)
|
||||
{
|
||||
return handle != nullptr;
|
||||
}
|
||||
|
||||
// Initializes an offscreen OpenGL context for non-viewer users.
|
||||
OAK_RENDER_BACKEND_EXPORT bool oak_renderer_init(OakRenderBackendHandle handle)
|
||||
{
|
||||
return renderer(handle)->init();
|
||||
}
|
||||
|
||||
// Initializes the backend against a caller-owned viewer OpenGL context.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_init_with_context(OakRenderBackendHandle handle, void *context)
|
||||
{
|
||||
renderer(handle)->init(static_cast<QOpenGLContext *>(context));
|
||||
}
|
||||
|
||||
// Runs renderer post-initialization once the GL context is available.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_post_init(OakRenderBackendHandle handle)
|
||||
{
|
||||
renderer(handle)->post_init();
|
||||
}
|
||||
|
||||
// Releases post-init OpenGL surface/context state.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_post_destroy(OakRenderBackendHandle handle)
|
||||
{
|
||||
renderer(handle)->post_destroy();
|
||||
}
|
||||
|
||||
// Releases renderer-owned GL resources before object destruction.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_destroy_internal(OakRenderBackendHandle handle)
|
||||
{
|
||||
renderer(handle)->destroy_internal();
|
||||
}
|
||||
|
||||
// Clears either the widget framebuffer or a texture destination.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_clear_destination(OakRenderBackendHandle handle, void *texture,
|
||||
double r, double g, double b, double a)
|
||||
{
|
||||
renderer(handle)->clear_destination(static_cast<olive::Texture *>(texture),
|
||||
r, g, b, a);
|
||||
}
|
||||
|
||||
// Creates an OpenGL texture and writes its QVariant handle to out_variant.
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_texture(
|
||||
OakRenderBackendHandle handle, int width, int height, int depth, int format,
|
||||
int channel_count, const void *data, int linesize, void *out_variant)
|
||||
{
|
||||
*static_cast<QVariant *>(out_variant) =
|
||||
renderer(handle)->create_native_texture(
|
||||
width, height, depth,
|
||||
static_cast<olive::PixelFormat::Format>(format), channel_count,
|
||||
data, linesize);
|
||||
}
|
||||
|
||||
// Destroys an OpenGL texture represented by a QVariant handle.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_destroy_native_texture(OakRenderBackendHandle handle,
|
||||
const void *variant)
|
||||
{
|
||||
renderer(handle)->destroy_native_texture(variant_ref(variant));
|
||||
}
|
||||
|
||||
// Compiles an OpenGL shader program and returns its QVariant handle.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_create_native_shader(OakRenderBackendHandle handle,
|
||||
const void *shader_code, void *out_variant)
|
||||
{
|
||||
*static_cast<QVariant *>(out_variant) =
|
||||
renderer(handle)->create_native_shader(
|
||||
*static_cast<const olive::ShaderCode *>(shader_code));
|
||||
}
|
||||
|
||||
// Destroys an OpenGL shader program represented by a QVariant handle.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_destroy_native_shader(OakRenderBackendHandle handle,
|
||||
const void *variant)
|
||||
{
|
||||
renderer(handle)->destroy_native_shader(variant_ref(variant));
|
||||
}
|
||||
|
||||
// Uploads CPU pixel data into an OpenGL texture.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_upload_to_texture(OakRenderBackendHandle handle,
|
||||
const void *variant, const void *video_params,
|
||||
const void *data, int linesize)
|
||||
{
|
||||
renderer(handle)->upload_to_texture(
|
||||
variant_ref(variant),
|
||||
*static_cast<const olive::VideoParams *>(video_params), data, linesize);
|
||||
}
|
||||
|
||||
// Reads an OpenGL texture back to CPU memory.
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_download_from_texture(
|
||||
OakRenderBackendHandle handle, const void *variant,
|
||||
const void *video_params, void *data, int linesize)
|
||||
{
|
||||
renderer(handle)->download_from_texture(
|
||||
variant_ref(variant),
|
||||
*static_cast<const olive::VideoParams *>(video_params), data, linesize);
|
||||
}
|
||||
|
||||
// Flushes/waits for pending OpenGL work as required by the renderer.
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_flush(OakRenderBackendHandle handle)
|
||||
{
|
||||
renderer(handle)->flush();
|
||||
}
|
||||
|
||||
// Reads one pixel from an OpenGL texture.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_get_pixel_from_texture(OakRenderBackendHandle handle,
|
||||
void *texture, const void *point,
|
||||
void *out_color)
|
||||
{
|
||||
*static_cast<olive::Color *>(out_color) =
|
||||
renderer(handle)->get_pixel_from_texture(
|
||||
static_cast<olive::Texture *>(texture),
|
||||
*static_cast<const QPointF *>(point));
|
||||
}
|
||||
|
||||
// Executes a shader blit through the wrapped C++ OpenGL renderer.
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_blit(OakRenderBackendHandle handle,
|
||||
const void *shader, void *job,
|
||||
void *destination,
|
||||
const void *destination_params,
|
||||
bool clear_destination)
|
||||
{
|
||||
renderer(handle)->blit(
|
||||
variant_ref(shader), *static_cast<olive::AcceleratedJob *>(job),
|
||||
static_cast<olive::Texture *>(destination),
|
||||
*static_cast<const olive::VideoParams *>(destination_params),
|
||||
clear_destination);
|
||||
}
|
||||
|
||||
// Exposes the wrapped OpenGL context for GL-specific integrations.
|
||||
OAK_RENDER_BACKEND_EXPORT void *
|
||||
oak_renderer_opengl_context(OakRenderBackendHandle handle)
|
||||
{
|
||||
return renderer(handle)->context();
|
||||
}
|
||||
|
||||
// Binds an output texture for OFX OpenGL rendering.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_attach_output_texture(OakRenderBackendHandle handle,
|
||||
const void *texture_id)
|
||||
{
|
||||
renderer(handle)->attach_texture_as_destination(variant_ref(texture_id));
|
||||
}
|
||||
|
||||
// Detaches any OFX OpenGL output texture binding.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_detach_output_texture(OakRenderBackendHandle handle)
|
||||
{
|
||||
renderer(handle)->detach_texture_as_destination();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef OAK_OPENGLCONTEXTPROVIDER_H
|
||||
#define OAK_OPENGLCONTEXTPROVIDER_H
|
||||
|
||||
class QOpenGLContext;
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class OpenGLContextProvider {
|
||||
public:
|
||||
virtual ~OpenGLContextProvider() = default;
|
||||
virtual QOpenGLContext *open_gl_context() const = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_OPENGLCONTEXTPROVIDER_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,164 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_OPENGLCONTEXT_H
|
||||
#define OAK_OPENGLCONTEXT_H
|
||||
|
||||
#include <QOffscreenSurface>
|
||||
#include <QOpenGLBuffer>
|
||||
#include <QOpenGLFunctions>
|
||||
#include <QOpenGLShader>
|
||||
#include <QOpenGLVertexArrayObject>
|
||||
#include <QPointer>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
|
||||
#include "render/opengl/openglcontextprovider.h"
|
||||
#include "render/renderer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class OpenGLRenderer : public Renderer, public OpenGLContextProvider {
|
||||
Q_OBJECT
|
||||
public:
|
||||
OpenGLRenderer(QObject *parent = nullptr);
|
||||
|
||||
virtual ~OpenGLRenderer() override;
|
||||
|
||||
void init(QOpenGLContext *existing_ctx);
|
||||
|
||||
virtual bool init() override;
|
||||
|
||||
virtual void post_destroy() override;
|
||||
|
||||
virtual void post_init() override;
|
||||
|
||||
virtual void clear_destination(olive::Texture *texture = nullptr,
|
||||
double r = 0.0, double g = 0.0,
|
||||
double b = 0.0, double a = 0.0) override;
|
||||
|
||||
virtual QVariant create_native_shader(olive::ShaderCode code) override;
|
||||
|
||||
virtual void destroy_native_shader(QVariant shader) override;
|
||||
|
||||
virtual void upload_to_texture(const QVariant &handle,
|
||||
const VideoParams ¶ms, const void *data,
|
||||
int linesize) override;
|
||||
|
||||
virtual void download_from_texture(const QVariant &handle,
|
||||
const VideoParams ¶ms, void *data,
|
||||
int linesize) override;
|
||||
|
||||
virtual void flush() override;
|
||||
|
||||
virtual Color get_pixel_from_texture(olive::Texture *texture,
|
||||
const QPointF &pt) override;
|
||||
|
||||
QOpenGLContext *context() const
|
||||
{
|
||||
return context_.data();
|
||||
}
|
||||
|
||||
virtual QOpenGLContext *open_gl_context() const override
|
||||
{
|
||||
return context();
|
||||
}
|
||||
|
||||
virtual bool is_open_gl() const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void attach_output_texture(olive::Texture *texture) override;
|
||||
|
||||
virtual void detach_output_texture() override;
|
||||
|
||||
bool ensure_context_current(const char *caller);
|
||||
|
||||
protected:
|
||||
virtual void blit(QVariant shader, olive::AcceleratedJob &job,
|
||||
olive::Texture *destination,
|
||||
olive::VideoParams destination_params,
|
||||
bool clear_destination) override;
|
||||
|
||||
virtual QVariant create_native_texture(int width, int height, int depth,
|
||||
PixelFormat format, int channel_count,
|
||||
const void *data = nullptr,
|
||||
int linesize = 0) override;
|
||||
|
||||
virtual void destroy_native_texture(QVariant texture) override;
|
||||
|
||||
virtual void destroy_internal() override;
|
||||
|
||||
void attach_texture_as_destination(const QVariant &texture);
|
||||
|
||||
void detach_texture_as_destination();
|
||||
|
||||
private:
|
||||
static GLint get_internal_format(PixelFormat format, int channel_layout);
|
||||
|
||||
static GLenum get_pixel_type(PixelFormat format);
|
||||
|
||||
static GLenum get_pixel_format(int channel_count);
|
||||
|
||||
void prepare_input_texture(GLenum target, Texture::Interpolation interp);
|
||||
|
||||
void clear_destination_internal(double r = 0.0, double g = 0.0,
|
||||
double b = 0.0, double a = 0.0);
|
||||
|
||||
GLuint compile_shader(GLenum type, const QString &code);
|
||||
|
||||
// Guarded pointer: viewer contexts are owned by the widget that created
|
||||
// them and may be destroyed before this renderer (e.g. when a QOpenGLWidget
|
||||
// tears down its shared context). QPointer auto-nulls in that case so
|
||||
// DestroyInternal() never dereferences a dangling context.
|
||||
QPointer<QOpenGLContext> context_;
|
||||
|
||||
QOpenGLFunctions *functions_;
|
||||
|
||||
QOffscreenSurface surface_;
|
||||
|
||||
GLuint framebuffer_;
|
||||
|
||||
struct TextureCacheKey {
|
||||
int width;
|
||||
int height;
|
||||
int depth;
|
||||
PixelFormat format;
|
||||
int channel_count;
|
||||
|
||||
bool operator==(const TextureCacheKey &rhs) const
|
||||
{
|
||||
return width == rhs.width && height == rhs.height &&
|
||||
depth == rhs.depth && format == rhs.format &&
|
||||
channel_count == rhs.channel_count;
|
||||
}
|
||||
};
|
||||
|
||||
QMap<GLuint, TextureCacheKey> texture_params_;
|
||||
|
||||
static const int k_texture_cache_max_size;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_OPENGLCONTEXT_H
|
||||
@@ -0,0 +1,318 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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"
|
||||
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "node/project.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "render/diskmanager.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
void PlaybackCache::invalidate(const TimeRange &r)
|
||||
{
|
||||
if (r.in() == r.out()) {
|
||||
qWarning() << "Tried to invalidate zero-length range";
|
||||
return;
|
||||
}
|
||||
|
||||
validated_.remove(r);
|
||||
|
||||
if (!passthroughs_.empty()) {
|
||||
TimeRangeList::util_remove(&passthroughs_, r);
|
||||
}
|
||||
|
||||
InvalidateEvent(r);
|
||||
|
||||
emit invalidated(r);
|
||||
|
||||
if (saving_enabled_) {
|
||||
save_state();
|
||||
}
|
||||
}
|
||||
|
||||
Node *PlaybackCache::parent() const
|
||||
{
|
||||
return dynamic_cast<Node *>(QObject::parent());
|
||||
}
|
||||
|
||||
QDir PlaybackCache::get_this_cache_directory() const
|
||||
{
|
||||
return get_this_cache_directory(get_cache_directory(), get_uuid());
|
||||
}
|
||||
|
||||
QDir PlaybackCache::get_this_cache_directory(const QString &cache_path,
|
||||
const QUuid &cache_id)
|
||||
{
|
||||
return QDir(cache_path).filePath(cache_id.toString());
|
||||
}
|
||||
|
||||
void PlaybackCache::load_state()
|
||||
{
|
||||
QDir cache_dir = get_this_cache_directory();
|
||||
QFile f(cache_dir.filePath(QStringLiteral("state")));
|
||||
|
||||
if (!f.exists()) {
|
||||
// No state exists, assume nothing valid
|
||||
validated_.clear();
|
||||
passthroughs_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 file_time =
|
||||
f.fileTime(QFileDevice::FileModificationTime).toMSecsSinceEpoch();
|
||||
if (file_time > last_loaded_state_ && f.open(QFile::ReadOnly)) {
|
||||
QDataStream s(&f);
|
||||
|
||||
uint32_t version;
|
||||
s >> version;
|
||||
|
||||
LoadStateEvent(s);
|
||||
|
||||
switch (version) {
|
||||
case 1: {
|
||||
int valid_count, pass_count;
|
||||
|
||||
s >> valid_count;
|
||||
for (int i = 0; i < valid_count; i++) {
|
||||
int in_num, in_den, out_num, out_den;
|
||||
|
||||
s >> in_num;
|
||||
s >> in_den;
|
||||
s >> out_num;
|
||||
s >> out_den;
|
||||
|
||||
validated_.insert(TimeRange(Rational(in_num, in_den),
|
||||
Rational(out_num, out_den)));
|
||||
}
|
||||
|
||||
s >> pass_count;
|
||||
for (int i = 0; i < pass_count; i++) {
|
||||
QUuid id;
|
||||
int in_num, in_den, out_num, out_den;
|
||||
|
||||
s >> in_num;
|
||||
s >> in_den;
|
||||
s >> out_num;
|
||||
s >> out_den;
|
||||
s >> id;
|
||||
|
||||
Passthrough p = TimeRange(Rational(in_num, in_den),
|
||||
Rational(out_num, out_den));
|
||||
p.cache = id;
|
||||
passthroughs_.push_back(p);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
f.close();
|
||||
|
||||
last_loaded_state_ = file_time;
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::save_state()
|
||||
{
|
||||
if (!DiskManager::instance()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QDir cache_dir = get_this_cache_directory();
|
||||
QFile f(cache_dir.filePath(QStringLiteral("state")));
|
||||
if (validated_.isEmpty() && passthroughs_.empty()) {
|
||||
if (f.exists()) {
|
||||
f.remove();
|
||||
}
|
||||
} else {
|
||||
if (FileFunctions::directory_is_valid(cache_dir)) {
|
||||
if (f.open(QFile::WriteOnly)) {
|
||||
QDataStream s(&f);
|
||||
|
||||
uint32_t version = 1;
|
||||
s << version;
|
||||
|
||||
SaveStateEvent(s);
|
||||
|
||||
// Using "int" for backwards compatibility with when we used QVector, could potentially overflow
|
||||
s << int(validated_.size());
|
||||
|
||||
for (const TimeRange &r : validated_) {
|
||||
s << r.in().numerator();
|
||||
s << r.in().denominator();
|
||||
s << r.out().numerator();
|
||||
s << r.out().denominator();
|
||||
}
|
||||
|
||||
// Using "int" for backwards compatibility with when we used QVector, could potentially overflow
|
||||
s << int(passthroughs_.size());
|
||||
|
||||
for (const Passthrough &p : passthroughs_) {
|
||||
s << p.in().numerator();
|
||||
s << p.in().denominator();
|
||||
s << p.out().numerator();
|
||||
s << p.out().denominator();
|
||||
s << p.cache;
|
||||
}
|
||||
|
||||
f.close();
|
||||
|
||||
last_loaded_state_ =
|
||||
f.fileTime(QFileDevice::FileModificationTime)
|
||||
.toMSecsSinceEpoch();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::draw(QPainter *p, const Rational &start, double scale,
|
||||
const QRect &rect) const
|
||||
{
|
||||
p->fillRect(rect, Qt::red);
|
||||
|
||||
foreach (const TimeRange &range, get_validated_ranges()) {
|
||||
int range_left = rect.left() + (range.in() - start).to_double() * scale;
|
||||
if (range_left >= rect.right()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int range_right =
|
||||
rect.left() + (range.out() - start).to_double() * scale;
|
||||
if (range_right < rect.left()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int adjusted_left = std::max(range_left, rect.left());
|
||||
int adjusted_right = std::min(range_right, rect.right());
|
||||
|
||||
p->fillRect(adjusted_left, rect.top(), adjusted_right - adjusted_left,
|
||||
rect.height(), Qt::green);
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::set_passthrough(PlaybackCache *cache)
|
||||
{
|
||||
for (const TimeRange &r : cache->get_validated_ranges()) {
|
||||
Passthrough p = r;
|
||||
p.cache = cache->get_uuid();
|
||||
passthroughs_.push_back(p);
|
||||
}
|
||||
|
||||
passthroughs_.insert(passthroughs_.end(), cache->get_passthroughs().begin(),
|
||||
cache->get_passthroughs().end());
|
||||
|
||||
if (saving_enabled_) {
|
||||
save_state();
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::invalidate_all()
|
||||
{
|
||||
invalidate(TimeRange(0, RATIONAL_MAX));
|
||||
}
|
||||
|
||||
void PlaybackCache::request(ViewerOutput *context, const TimeRange &r)
|
||||
{
|
||||
request_context_ = context;
|
||||
requested_.insert(r);
|
||||
|
||||
emit requested(request_context_, r);
|
||||
}
|
||||
|
||||
void PlaybackCache::validate(const TimeRange &r, bool signal)
|
||||
{
|
||||
validated_.insert(r);
|
||||
|
||||
if (signal) {
|
||||
emit validated(r);
|
||||
}
|
||||
|
||||
if (saving_enabled_) {
|
||||
save_state();
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::InvalidateEvent(const TimeRange &)
|
||||
{
|
||||
}
|
||||
|
||||
Project *PlaybackCache::get_project() const
|
||||
{
|
||||
return Project::get_project_from_object(this);
|
||||
}
|
||||
|
||||
PlaybackCache::PlaybackCache(QObject *parent)
|
||||
: QObject(parent)
|
||||
, saving_enabled_(true)
|
||||
, last_loaded_state_(0)
|
||||
{
|
||||
uuid_ = QUuid::createUuid();
|
||||
}
|
||||
|
||||
void PlaybackCache::set_uuid(const QUuid &u)
|
||||
{
|
||||
uuid_ = u;
|
||||
|
||||
load_state();
|
||||
}
|
||||
|
||||
TimeRangeList PlaybackCache::get_invalidated_ranges(TimeRange intersecting) const
|
||||
{
|
||||
TimeRangeList invalidated;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
foreach (const TimeRange &range, passthroughs_) {
|
||||
invalidated.remove(range);
|
||||
}
|
||||
|
||||
return invalidated;
|
||||
}
|
||||
|
||||
bool PlaybackCache::has_invalidated_ranges(const TimeRange &intersecting) const
|
||||
{
|
||||
return !validated_.contains(intersecting);
|
||||
}
|
||||
|
||||
QString PlaybackCache::get_cache_directory() const
|
||||
{
|
||||
Project *project = get_project();
|
||||
|
||||
if (project) {
|
||||
return project->cache_path();
|
||||
} else {
|
||||
return DiskManager::instance()->get_default_cache_path();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_PLAYBACKCACHE_H
|
||||
#define OAK_PLAYBACKCACHE_H
|
||||
|
||||
#include <olive/core/core.h>
|
||||
#include <QDir>
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QPainter>
|
||||
#include <QUuid>
|
||||
|
||||
#include "common/jobtime.h"
|
||||
|
||||
using namespace olive::core;
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class Node;
|
||||
class Project;
|
||||
class ViewerOutput;
|
||||
|
||||
class PlaybackCache : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
PlaybackCache(QObject *parent = nullptr);
|
||||
|
||||
const QUuid &get_uuid() const
|
||||
{
|
||||
return uuid_;
|
||||
}
|
||||
void set_uuid(const QUuid &u);
|
||||
|
||||
TimeRangeList get_invalidated_ranges(TimeRange intersecting) const;
|
||||
TimeRangeList get_invalidated_ranges(const Rational &length) const
|
||||
{
|
||||
return get_invalidated_ranges(TimeRange(0, length));
|
||||
}
|
||||
|
||||
bool has_invalidated_ranges(const TimeRange &intersecting) const;
|
||||
bool has_invalidated_ranges(const Rational &length) const
|
||||
{
|
||||
return has_invalidated_ranges(TimeRange(0, length));
|
||||
}
|
||||
|
||||
QString get_cache_directory() const;
|
||||
|
||||
void invalidate(const TimeRange &r);
|
||||
|
||||
bool has_validated_ranges() const
|
||||
{
|
||||
return !validated_.isEmpty();
|
||||
}
|
||||
const TimeRangeList &get_validated_ranges() const
|
||||
{
|
||||
return validated_;
|
||||
}
|
||||
|
||||
Node *parent() const;
|
||||
|
||||
QDir get_this_cache_directory() const;
|
||||
static QDir get_this_cache_directory(const QString &cache_path,
|
||||
const QUuid &cache_id);
|
||||
|
||||
void load_state();
|
||||
void save_state();
|
||||
|
||||
void draw(QPainter *painter, const Rational &start, double scale,
|
||||
const QRect &rect) const;
|
||||
|
||||
static int get_cache_indicator_height()
|
||||
{
|
||||
return QFontMetrics(QFont()).height() / 4;
|
||||
}
|
||||
|
||||
bool is_saving_enabled() const
|
||||
{
|
||||
return saving_enabled_;
|
||||
}
|
||||
void set_saving_enabled(bool e)
|
||||
{
|
||||
saving_enabled_ = e;
|
||||
}
|
||||
|
||||
virtual void set_passthrough(PlaybackCache *cache);
|
||||
|
||||
QMutex *mutex()
|
||||
{
|
||||
return &mutex_;
|
||||
}
|
||||
|
||||
class Passthrough : public TimeRange {
|
||||
public:
|
||||
Passthrough(const TimeRange &r)
|
||||
: TimeRange(r)
|
||||
{
|
||||
}
|
||||
|
||||
QUuid cache;
|
||||
};
|
||||
|
||||
const std::vector<Passthrough> &get_passthroughs() const
|
||||
{
|
||||
return passthroughs_;
|
||||
}
|
||||
|
||||
void clear_request_range(const TimeRange &r)
|
||||
{
|
||||
requested_.remove(r);
|
||||
}
|
||||
|
||||
void resignal_requests()
|
||||
{
|
||||
for (const TimeRange &r : requested_) {
|
||||
emit requested(request_context_, r);
|
||||
}
|
||||
}
|
||||
|
||||
public slots:
|
||||
void invalidate_all();
|
||||
|
||||
void request(ViewerOutput *context, const TimeRange &r);
|
||||
|
||||
signals:
|
||||
void invalidated(const TimeRange &r);
|
||||
|
||||
void validated(const TimeRange &r);
|
||||
|
||||
void requested(ViewerOutput *context, const TimeRange &r);
|
||||
|
||||
void cancel_all();
|
||||
|
||||
protected:
|
||||
void validate(const TimeRange &r, bool signal = true);
|
||||
|
||||
virtual void InvalidateEvent(const TimeRange &range);
|
||||
|
||||
virtual void LoadStateEvent(QDataStream &stream)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void SaveStateEvent(QDataStream &stream)
|
||||
{
|
||||
}
|
||||
|
||||
Project *get_project() const;
|
||||
|
||||
private:
|
||||
TimeRangeList validated_;
|
||||
|
||||
TimeRangeList requested_;
|
||||
ViewerOutput *request_context_;
|
||||
|
||||
QUuid uuid_;
|
||||
|
||||
bool saving_enabled_;
|
||||
|
||||
QMutex mutex_;
|
||||
|
||||
std::vector<Passthrough> passthroughs_;
|
||||
|
||||
qint64 last_loaded_state_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_PLAYBACKCACHE_H
|
||||
@@ -0,0 +1,6 @@
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
render/plugin/pluginrenderer.cpp
|
||||
render/plugin/pluginrenderer.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Oak Video Editor - Non-Linear Video Editor
|
||||
* Copyright (C) 2025 Olive CE 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
//
|
||||
// Created by mikesolar on 25-10-19.
|
||||
//
|
||||
|
||||
#ifndef OAK_PLUGINRENDERER_H
|
||||
#define OAK_PLUGINRENDERER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "render/renderer.h"
|
||||
#include "render/job/pluginjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
// 作用:将字节行跨度转换为像素跨度,便于纹理读写。
|
||||
// Purpose: Convert byte stride to pixel stride for texture I/O.
|
||||
int bytes_to_pixels(int byte_linesize, const olive::VideoParams ¶ms);
|
||||
}
|
||||
// 作用:OFX 插件渲染器,负责 CPU/GL 路径下的插件调用和纹理桥接。
|
||||
// Purpose: OFX plugin renderer that drives CPU/GL render paths and texture bridging.
|
||||
//
|
||||
// 不再继承 OpenGLRenderer,而是持有一个通用的 Renderer 指针。这样当主渲染器
|
||||
// 是 Vulkan 或动态加载的后端时,插件仍可通过 CPU readback/upload 路径工作;
|
||||
// 仅当底层渲染器真正支持 OpenGL 时才走 OFX OpenGL 渲染路径。
|
||||
class PluginRenderer : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PluginRenderer(olive::Renderer *renderer,
|
||||
QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
, renderer_(renderer)
|
||||
{
|
||||
}
|
||||
virtual ~PluginRenderer() override
|
||||
{
|
||||
}
|
||||
|
||||
olive::Renderer *renderer() const
|
||||
{
|
||||
return renderer_;
|
||||
}
|
||||
|
||||
// 作用:将目标纹理绑定为插件输出。
|
||||
// Purpose: Attach destination texture as OFX output.
|
||||
void attach_output_texture(olive::TexturePtr texture);
|
||||
// 作用:解除目标纹理绑定。
|
||||
// Purpose: Detach destination texture binding.
|
||||
void detach_output_texture();
|
||||
// 作用:执行插件渲染流程(参数配置、输入/输出、调用渲染动作)。
|
||||
// Purpose: Execute plugin render flow (params, inputs/outputs, render actions).
|
||||
void render_plugin(TexturePtr src, olive::plugin::PluginJob &job,
|
||||
olive::TexturePtr destination,
|
||||
olive::VideoParams destination_params,
|
||||
bool clear_destination, bool interactive);
|
||||
|
||||
private:
|
||||
olive::Renderer *renderer_;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif //OAK_PLUGINRENDERER_H
|
||||
@@ -0,0 +1,93 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "previewaudiodevice.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
PreviewAudioDevice::PreviewAudioDevice(QObject *parent)
|
||||
: QIODevice(parent)
|
||||
, bytes_per_frame_(0)
|
||||
, notify_interval_(0)
|
||||
, bytes_read_(0)
|
||||
{
|
||||
}
|
||||
|
||||
PreviewAudioDevice::~PreviewAudioDevice()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
bool PreviewAudioDevice::isSequential() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void PreviewAudioDevice::set_params(const core::AudioParams ¶ms)
|
||||
{
|
||||
set_bytes_per_frame(params.samples_to_bytes(1));
|
||||
}
|
||||
|
||||
qint64 PreviewAudioDevice::readData(char *data, qint64 max_size)
|
||||
{
|
||||
QMutexLocker locker(&lock_);
|
||||
|
||||
qint64 copy_length = qMin(max_size, qint64(buffer_.size()));
|
||||
|
||||
if (copy_length) {
|
||||
qint64 new_bytes_read = bytes_read_ + copy_length;
|
||||
|
||||
if (notify_interval_ > 0) {
|
||||
if ((bytes_read_ / notify_interval_) !=
|
||||
(new_bytes_read / notify_interval_)) {
|
||||
emit notify();
|
||||
}
|
||||
}
|
||||
|
||||
bytes_read_ = new_bytes_read;
|
||||
|
||||
memcpy(data, buffer_.constData(), copy_length);
|
||||
buffer_ = buffer_.mid(copy_length);
|
||||
}
|
||||
|
||||
return copy_length;
|
||||
}
|
||||
|
||||
qint64 PreviewAudioDevice::writeData(const char *data, qint64 length)
|
||||
{
|
||||
QMutexLocker locker(&lock_);
|
||||
|
||||
buffer_.append(data, length);
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
void PreviewAudioDevice::clear()
|
||||
{
|
||||
QMutexLocker locker(&lock_);
|
||||
|
||||
buffer_.clear();
|
||||
bytes_read_ = 0;
|
||||
output_frames_consumed_.store(0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_PREVIEWAUDIODEVICE_H
|
||||
#define OAK_PREVIEWAUDIODEVICE_H
|
||||
|
||||
#include <olive/core/render/audioparams.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include "previewautocacher.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class PreviewAudioDevice : public QIODevice {
|
||||
Q_OBJECT
|
||||
public:
|
||||
PreviewAudioDevice(QObject *parent = nullptr);
|
||||
|
||||
virtual ~PreviewAudioDevice() override;
|
||||
|
||||
void start_queuing();
|
||||
|
||||
virtual bool isSequential() const override;
|
||||
|
||||
virtual qint64 readData(char *data, qint64 max_size) override;
|
||||
|
||||
virtual qint64 writeData(const char *data, qint64 length) override;
|
||||
|
||||
// Derives the frame size from the audio format (bytes per sample per
|
||||
// channel * channel count). Until params are set, bytes_per_frame()
|
||||
// reports 0, i.e. "unknown".
|
||||
void set_params(const core::AudioParams ¶ms);
|
||||
|
||||
int bytes_per_frame() const
|
||||
{
|
||||
return bytes_per_frame_;
|
||||
}
|
||||
|
||||
void set_bytes_per_frame(int b)
|
||||
{
|
||||
bytes_per_frame_ = b;
|
||||
}
|
||||
|
||||
void set_notify_interval(qint64 i)
|
||||
{
|
||||
notify_interval_ = i;
|
||||
}
|
||||
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* @brief Frames consumed by the audio output callback
|
||||
*
|
||||
* Counted in the callback itself so underrun (zero-filled) frames are
|
||||
* included, making the value usable as a playback clock.
|
||||
*/
|
||||
void add_output_frames(qint64 frame_count)
|
||||
{
|
||||
output_frames_consumed_.fetch_add(frame_count);
|
||||
}
|
||||
|
||||
qint64 output_frames_consumed() const
|
||||
{
|
||||
return output_frames_consumed_.load();
|
||||
}
|
||||
|
||||
void reset_output_frames()
|
||||
{
|
||||
output_frames_consumed_.store(0);
|
||||
}
|
||||
|
||||
signals:
|
||||
void notify();
|
||||
|
||||
private:
|
||||
QMutex lock_;
|
||||
|
||||
QByteArray buffer_;
|
||||
|
||||
int bytes_per_frame_;
|
||||
|
||||
qint64 notify_interval_;
|
||||
|
||||
qint64 bytes_read_;
|
||||
|
||||
std::atomic<qint64> output_frames_consumed_{0};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_PREVIEWAUDIODEVICE_H
|
||||
@@ -0,0 +1,874 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "previewautocacher.h"
|
||||
|
||||
#include <QPointer>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#include "codec/conformmanager.h"
|
||||
#include "node/input/multicam/multicamnode.h"
|
||||
#include "node/inputdragger.h"
|
||||
#include "node/project.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/rendermanager.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
PreviewAutoCacher::PreviewAutoCacher(QObject *parent)
|
||||
: QObject(parent)
|
||||
, project_(nullptr)
|
||||
, use_custom_range_(false)
|
||||
, pause_renders_(false)
|
||||
, pause_thumbnails_(false)
|
||||
, single_frame_render_(nullptr)
|
||||
, display_color_processor_(nullptr)
|
||||
, multicam_(nullptr)
|
||||
, ignore_cache_requests_(false)
|
||||
{
|
||||
copier_ = new ProjectCopier(this);
|
||||
connect(copier_, &ProjectCopier::added_node, this,
|
||||
&PreviewAutoCacher::connect_to_node_cache);
|
||||
connect(copier_, &ProjectCopier::removed_node, this,
|
||||
&PreviewAutoCacher::disconnect_from_node_cache);
|
||||
|
||||
// Set defaults
|
||||
set_playhead(0);
|
||||
|
||||
// Wait a certain amount of time before requeuing when we receive an invalidate signal
|
||||
delayed_requeue_timer_.setInterval(OAK_CONFIG("AutoCacheDelay").toInt());
|
||||
delayed_requeue_timer_.setSingleShot(true);
|
||||
connect(&delayed_requeue_timer_, &QTimer::timeout, this,
|
||||
&PreviewAutoCacher::try_render);
|
||||
|
||||
// Catch when a conform is ready
|
||||
connect(ConformManager::instance(), &ConformManager::conform_ready, this,
|
||||
&PreviewAutoCacher::conform_finished);
|
||||
}
|
||||
|
||||
PreviewAutoCacher::~PreviewAutoCacher()
|
||||
{
|
||||
// Ensure everything is cleaned up appropriately
|
||||
set_project(nullptr);
|
||||
}
|
||||
|
||||
RenderTicketPtr PreviewAutoCacher::get_single_frame(ViewerOutput *viewer,
|
||||
const Rational &t, bool dry)
|
||||
{
|
||||
return get_single_frame(viewer->get_connected_texture_output(), viewer, t, dry);
|
||||
}
|
||||
|
||||
RenderTicketPtr PreviewAutoCacher::get_single_frame(Node *n, ViewerOutput *viewer,
|
||||
const Rational &t, bool dry)
|
||||
{
|
||||
// If we have a single frame render queued (but not yet sent to the RenderManager), cancel it now
|
||||
cancel_queued_single_frame_render();
|
||||
|
||||
// Create a new single frame render ticket
|
||||
auto sfr = std::make_shared<RenderTicket>();
|
||||
sfr->start();
|
||||
sfr->setProperty("time", QVariant::fromValue(t));
|
||||
sfr->setProperty("dry", dry);
|
||||
sfr->setProperty("node", QtUtils::ptr_to_value(n));
|
||||
sfr->setProperty("viewer", QtUtils::ptr_to_value(viewer));
|
||||
|
||||
// Queue it and try to render
|
||||
single_frame_render_ = sfr;
|
||||
try_render();
|
||||
|
||||
return sfr;
|
||||
}
|
||||
|
||||
RenderTicketPtr PreviewAutoCacher::get_range_of_audio(ViewerOutput *viewer,
|
||||
TimeRange range)
|
||||
{
|
||||
Node *copy = copier_->get_copy(viewer->get_connected_sample_output());
|
||||
return render_audio(copy, viewer, range, nullptr);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::clear_single_frame_renders()
|
||||
{
|
||||
// Snapshot the watchers as guarded pointers before doing anything that
|
||||
// might synchronously delete them (emitting Finished runs VideoRendered,
|
||||
// which deletes the watcher and removes it from the map). Iterating over a
|
||||
// raw-pointer copy of the map would leave dangling pointers.
|
||||
QList<QPointer<RenderTicketWatcher>> watchers;
|
||||
for (auto it = video_immediate_passthroughs_.cbegin();
|
||||
it != video_immediate_passthroughs_.cend(); it++) {
|
||||
watchers.append(it.key());
|
||||
}
|
||||
|
||||
foreach (const QPointer<RenderTicketWatcher> &w, watchers) {
|
||||
if (!w) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep already-running workers alive: cancelling an in-flight render
|
||||
// forces the worker process to be torn down, which defeats the process
|
||||
// pool. Frames that finish late are simply ignored by the viewer.
|
||||
if (w->is_running()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderTicketPtr ticket = w->get_ticket();
|
||||
w->cancel();
|
||||
RenderManager::instance()->remove_ticket(ticket);
|
||||
emit ticket->finished();
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::clear_single_frame_renders_that_arent_running()
|
||||
{
|
||||
QList<QPointer<RenderTicketWatcher>> watchers;
|
||||
for (auto it = video_immediate_passthroughs_.cbegin();
|
||||
it != video_immediate_passthroughs_.cend(); it++) {
|
||||
watchers.append(it.key());
|
||||
}
|
||||
|
||||
foreach (const QPointer<RenderTicketWatcher> &w, watchers) {
|
||||
if (!w || w->is_running()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderTicketPtr ticket = w->get_ticket();
|
||||
w->cancel();
|
||||
RenderManager::instance()->remove_ticket(ticket);
|
||||
emit ticket->finished();
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::video_invalidated_from_cache(ViewerOutput *context,
|
||||
const TimeRange &range)
|
||||
{
|
||||
PlaybackCache *cache = static_cast<PlaybackCache *>(sender());
|
||||
|
||||
cache->clear_request_range(range);
|
||||
|
||||
video_invalidated_from_node(context, cache, range);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::audio_invalidated_from_cache(ViewerOutput *context,
|
||||
const TimeRange &range)
|
||||
{
|
||||
PlaybackCache *cache = static_cast<PlaybackCache *>(sender());
|
||||
|
||||
cache->clear_request_range(range);
|
||||
|
||||
audio_invalidated_from_node(context, cache, range);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::cancel_for_cache()
|
||||
{
|
||||
PlaybackCache *cache = static_cast<PlaybackCache *>(sender());
|
||||
|
||||
if (dynamic_cast<FrameHashCache *>(cache) ||
|
||||
dynamic_cast<ThumbnailCache *>(cache)) {
|
||||
for (auto it = pending_video_jobs_.begin();
|
||||
it != pending_video_jobs_.end();) {
|
||||
if ((*it).cache == cache) {
|
||||
it = pending_video_jobs_.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
} else if (dynamic_cast<AudioPlaybackCache *>(cache) ||
|
||||
dynamic_cast<AudioWaveformCache *>(cache)) {
|
||||
for (auto it = pending_audio_jobs_.begin();
|
||||
it != pending_audio_jobs_.end();) {
|
||||
if ((*it).cache == cache) {
|
||||
it = pending_audio_jobs_.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::audio_rendered()
|
||||
{
|
||||
// Receive watcher
|
||||
RenderTicketWatcher *watcher = static_cast<RenderTicketWatcher *>(sender());
|
||||
|
||||
// If the task list doesn't contain this watcher, presumably it was cleared as a result of a
|
||||
// viewer switch, so we'll completely ignore this watcher
|
||||
if (running_audio_tasks_.removeOne(watcher)) {
|
||||
// Assume that a "result" is a fully completed image and a non-result is a cancelled ticket
|
||||
TimeRange range = watcher->property("time").value<TimeRange>();
|
||||
Node *node = copier_->get_original(
|
||||
QtUtils::value_to_ptr<Node>(watcher->property("node")));
|
||||
|
||||
if (watcher->has_result() && node) {
|
||||
if (PlaybackCache *cache = QtUtils::value_to_ptr<PlaybackCache>(
|
||||
watcher->property("cache"))) {
|
||||
AudioCacheData &d = audio_cache_data_[cache];
|
||||
|
||||
JobTime watcher_job_time =
|
||||
watcher->property("job").value<JobTime>();
|
||||
|
||||
TimeRangeList valid_ranges =
|
||||
d.job_tracker.getCurrentSubRanges(range, watcher_job_time);
|
||||
|
||||
AudioVisualWaveform waveform =
|
||||
watcher->get_ticket()
|
||||
->property("waveform")
|
||||
.value<AudioVisualWaveform>();
|
||||
|
||||
SampleBuffer buf = watcher->get().value<SampleBuffer>();
|
||||
|
||||
bool incomplete =
|
||||
watcher->get_ticket()->property("incomplete").toBool();
|
||||
|
||||
if (AudioPlaybackCache *pcm =
|
||||
dynamic_cast<AudioPlaybackCache *>(cache)) {
|
||||
// WritePCM is tolerant to its buffer being null, it will just write silence instead
|
||||
pcm->set_parameters(buf.audio_params());
|
||||
pcm->write_pcm(range, valid_ranges,
|
||||
watcher->get().value<SampleBuffer>());
|
||||
} else if (AudioWaveformCache *wave =
|
||||
dynamic_cast<AudioWaveformCache *>(cache)) {
|
||||
wave->set_parameters(buf.audio_params());
|
||||
if (!incomplete) {
|
||||
wave->write_waveform(range, valid_ranges, &waveform);
|
||||
}
|
||||
}
|
||||
|
||||
if (incomplete) {
|
||||
if (last_conform_task_ > watcher_job_time) {
|
||||
// Requeue now
|
||||
cache->invalidate(range);
|
||||
} else {
|
||||
// Wait for conform
|
||||
d.needs_conform.insert(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue rendering
|
||||
try_render();
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::video_rendered()
|
||||
{
|
||||
RenderTicketWatcher *watcher = static_cast<RenderTicketWatcher *>(sender());
|
||||
|
||||
const QStringList bad_cache_names =
|
||||
watcher->get_ticket()->property("badcache").toStringList();
|
||||
if (!bad_cache_names.empty()) {
|
||||
for (const QString &fn : bad_cache_names) {
|
||||
DiskManager::instance()->delete_specific_file(fn);
|
||||
}
|
||||
}
|
||||
|
||||
// Process passthroughs no matter what, if the viewer was switched, the passthrough map would be
|
||||
// cleared anyway
|
||||
QVector<RenderTicketPtr> tickets =
|
||||
video_immediate_passthroughs_.take(watcher);
|
||||
foreach (RenderTicketPtr t, tickets) {
|
||||
if (watcher->has_result()) {
|
||||
t->setProperty("multicam_output",
|
||||
watcher->get_ticket()->property("multicam_output"));
|
||||
t->finish(watcher->get());
|
||||
} else {
|
||||
t->finish();
|
||||
}
|
||||
}
|
||||
|
||||
// If the task list doesn't contain this watcher, presumably it was cleared as a result of a
|
||||
// viewer switch, so we'll completely ignore this watcher
|
||||
if (running_video_tasks_.removeOne(watcher)) {
|
||||
// Assume that a "result" is a fully completed image and a non-result is a cancelled ticket
|
||||
if (watcher->has_result()) {
|
||||
if (watcher->get_ticket()->property("cached").toBool()) {
|
||||
if (FrameHashCache *cache = QtUtils::value_to_ptr<FrameHashCache>(
|
||||
watcher->property("cache"))) {
|
||||
Rational time = watcher->property("time").value<Rational>();
|
||||
JobTime job = watcher->property("job").value<JobTime>();
|
||||
|
||||
if (video_cache_data_.value(cache).job_tracker.isCurrent(
|
||||
time, job)) {
|
||||
cache->validate_time(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue rendering
|
||||
try_render();
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::connect_to_node_cache(Node *node)
|
||||
{
|
||||
if (ignore_cache_requests_) {
|
||||
return;
|
||||
}
|
||||
|
||||
connect(node->video_frame_cache(), &PlaybackCache::requested, this,
|
||||
&PreviewAutoCacher::video_invalidated_from_cache);
|
||||
|
||||
connect(node->thumbnail_cache(), &PlaybackCache::requested, this,
|
||||
&PreviewAutoCacher::video_invalidated_from_cache);
|
||||
|
||||
connect(node->audio_playback_cache(), &PlaybackCache::requested, this,
|
||||
&PreviewAutoCacher::audio_invalidated_from_cache);
|
||||
|
||||
connect(node->waveform_cache(), &PlaybackCache::requested, this,
|
||||
&PreviewAutoCacher::audio_invalidated_from_cache);
|
||||
|
||||
connect(node->video_frame_cache(), &PlaybackCache::cancel_all, this,
|
||||
&PreviewAutoCacher::cancel_for_cache);
|
||||
|
||||
connect(node->audio_playback_cache(), &PlaybackCache::cancel_all, this,
|
||||
&PreviewAutoCacher::cancel_for_cache);
|
||||
|
||||
node->video_frame_cache()->resignal_requests();
|
||||
node->thumbnail_cache()->resignal_requests();
|
||||
node->audio_playback_cache()->resignal_requests();
|
||||
node->waveform_cache()->resignal_requests();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::disconnect_from_node_cache(Node *node)
|
||||
{
|
||||
disconnect(node->video_frame_cache(), &PlaybackCache::requested, this,
|
||||
&PreviewAutoCacher::video_invalidated_from_cache);
|
||||
|
||||
disconnect(node->thumbnail_cache(), &PlaybackCache::requested, this,
|
||||
&PreviewAutoCacher::video_invalidated_from_cache);
|
||||
|
||||
disconnect(node->audio_playback_cache(), &PlaybackCache::requested, this,
|
||||
&PreviewAutoCacher::audio_invalidated_from_cache);
|
||||
|
||||
disconnect(node->waveform_cache(), &PlaybackCache::requested, this,
|
||||
&PreviewAutoCacher::audio_invalidated_from_cache);
|
||||
|
||||
disconnect(node->video_frame_cache(), &PlaybackCache::cancel_all, this,
|
||||
&PreviewAutoCacher::cancel_for_cache);
|
||||
|
||||
disconnect(node->audio_playback_cache(), &PlaybackCache::cancel_all, this,
|
||||
&PreviewAutoCacher::cancel_for_cache);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::cancel_queued_single_frame_render()
|
||||
{
|
||||
if (single_frame_render_) {
|
||||
// Signal that this ticket was cancelled with no value
|
||||
single_frame_render_->finish();
|
||||
single_frame_render_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::start_caching_range(const TimeRange &range,
|
||||
TimeRangeList *range_list,
|
||||
RenderJobTracker *tracker)
|
||||
{
|
||||
range_list->insert(range);
|
||||
tracker->insert(range, copier_->get_graph_change_time());
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::start_caching_video_range(ViewerOutput *context,
|
||||
PlaybackCache *cache,
|
||||
const TimeRange &range)
|
||||
{
|
||||
Node *node = cache->parent();
|
||||
Rational using_tb;
|
||||
if (ThumbnailCache *thumbs = dynamic_cast<ThumbnailCache *>(cache)) {
|
||||
using_tb = thumbs->get_timebase();
|
||||
} else {
|
||||
using_tb = context->get_video_params().frame_rate_as_time_base();
|
||||
}
|
||||
|
||||
cache->clear_request_range(range);
|
||||
|
||||
TimeRangeListFrameIterator iterator({ range }, using_tb);
|
||||
pending_video_jobs_.push_back({ node, context, cache, range, iterator });
|
||||
video_cache_data_[cache].job_tracker.insert(
|
||||
TimeRange(iterator.snap(range.in()), range.out()),
|
||||
copier_->get_graph_change_time());
|
||||
try_render();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::start_caching_audio_range(ViewerOutput *context,
|
||||
PlaybackCache *cache,
|
||||
const TimeRange &range)
|
||||
{
|
||||
Node *node = cache->parent();
|
||||
|
||||
cache->clear_request_range(range);
|
||||
|
||||
pending_audio_jobs_.push_back({ node, context, cache, range });
|
||||
AudioCacheData &data = audio_cache_data_[cache];
|
||||
data.context = context;
|
||||
data.job_tracker.insert(range, copier_->get_graph_change_time());
|
||||
try_render();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::video_invalidated_from_node(ViewerOutput *context,
|
||||
PlaybackCache *cache,
|
||||
const TimeRange &range)
|
||||
{
|
||||
// Ignore render requests if no video is present
|
||||
if (!context || !context->get_video_params().is_valid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop any current render tasks because a) they might be out of date now anyway, and b) we
|
||||
// want to dedicate all our rendering power to realtime feedback for the user
|
||||
//CancelVideoTasks(node);
|
||||
|
||||
cache->clear_request_range(range);
|
||||
|
||||
// If auto-cache is enabled and a slider is not being dragged, queue up to hash these frames
|
||||
if (!NodeInputDragger::is_input_being_dragged()) {
|
||||
start_caching_video_range(context, cache, range);
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::audio_invalidated_from_node(ViewerOutput *context,
|
||||
PlaybackCache *cache,
|
||||
const TimeRange &range)
|
||||
{
|
||||
// Ignore render requests if no video is present
|
||||
if (!context || !context->get_audio_params().is_valid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't stop rendering audio because currently there's no system of requeuing audio if it's
|
||||
// cancelled, so some areas may end up unrendered forever
|
||||
// ClearAudioQueue();
|
||||
|
||||
cache->clear_request_range(range);
|
||||
|
||||
// If we're auto-caching audio or require realtime waveforms, we'll have to render this
|
||||
start_caching_audio_range(context, cache, range);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::set_playhead(const Rational &playhead)
|
||||
{
|
||||
cache_range_ =
|
||||
TimeRange(playhead - OAK_CONFIG("DiskCacheBehind").value<Rational>(),
|
||||
playhead + OAK_CONFIG("DiskCacheAhead").value<Rational>());
|
||||
|
||||
try_render();
|
||||
}
|
||||
|
||||
template <typename T> void cancel_tasks(const T &task_list, bool and_wait)
|
||||
{
|
||||
for (auto it = task_list.cbegin(); it != task_list.cend(); it++) {
|
||||
// Signal that the ticket should not be finished
|
||||
(*it)->cancel();
|
||||
}
|
||||
|
||||
if (and_wait) {
|
||||
// Wait for each ticket to finish
|
||||
for (auto it = task_list.cbegin(); it != task_list.cend(); it++) {
|
||||
(*it)->wait_for_finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::cancel_video_tasks(bool and_wait_for_them_to_finish)
|
||||
{
|
||||
cancel_tasks(running_video_tasks_, and_wait_for_them_to_finish);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::cancel_audio_tasks(bool and_wait_for_them_to_finish)
|
||||
{
|
||||
cancel_tasks(running_audio_tasks_, and_wait_for_them_to_finish);
|
||||
}
|
||||
|
||||
bool PreviewAutoCacher::is_rendering_custom_range() const
|
||||
{
|
||||
if (!use_custom_range_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const VideoJob &job : pending_video_jobs_) {
|
||||
if (job.range == custom_autocache_range_ && job.iterator.has_next()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::set_renders_paused(bool e)
|
||||
{
|
||||
pause_renders_ = e;
|
||||
if (!e) {
|
||||
try_render();
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::set_thumbnails_paused(bool e)
|
||||
{
|
||||
pause_thumbnails_ = e;
|
||||
if (!e) {
|
||||
try_render();
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::try_render()
|
||||
{
|
||||
delayed_requeue_timer_.stop();
|
||||
|
||||
if (copier_->has_updates_in_queue()) {
|
||||
// Check if we have jobs running in other threads that shouldn't be interrupted right now
|
||||
// NOTE: We don't check for downloads because, while they run in another thread, they don't
|
||||
// require any access to the graph and therefore don't risk race conditions.
|
||||
if (!running_audio_tasks_.isEmpty() ||
|
||||
!running_video_tasks_.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No jobs are active, we can process the update queue
|
||||
copier_->process_update_queue();
|
||||
}
|
||||
|
||||
if (single_frame_render_) {
|
||||
// Make an explicit copy of the render ticket here - it seems that on some systems it can be set
|
||||
// to NULL before we're done with it...
|
||||
RenderTicketPtr t = single_frame_render_;
|
||||
single_frame_render_ = nullptr;
|
||||
|
||||
// Check if already caching this
|
||||
Node *n = QtUtils::value_to_ptr<Node>(t->property("node"));
|
||||
Node *copy = copier_->get_copy(n);
|
||||
|
||||
if (copy) {
|
||||
RenderTicketWatcher *watcher = render_frame(
|
||||
copy, QtUtils::value_to_ptr<ViewerOutput>(t->property("viewer")),
|
||||
t->property("time").value<Rational>(), nullptr,
|
||||
t->property("dry").toBool());
|
||||
if (watcher) {
|
||||
video_immediate_passthroughs_[watcher].append(t);
|
||||
}
|
||||
} else {
|
||||
qWarning()
|
||||
<< "Failed to find copied node for SFR ticket, requeueing";
|
||||
single_frame_render_ = t;
|
||||
if (!delayed_requeue_timer_.isActive()) {
|
||||
delayed_requeue_timer_.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!pause_renders_) {
|
||||
// Completely arbitrary number. I don't know what's optimal for this yet.
|
||||
const int max_tasks = 4;
|
||||
|
||||
// Handle video tasks
|
||||
if (!pause_thumbnails_) {
|
||||
while (!pending_video_jobs_.empty()) {
|
||||
VideoJob &d = pending_video_jobs_.front();
|
||||
|
||||
if (Node *copy = copier_->get_copy(d.node)) {
|
||||
// Queue next frames
|
||||
Rational t;
|
||||
while (running_video_tasks_.size() < max_tasks &&
|
||||
d.iterator.get_next(&t)) {
|
||||
render_frame(copy, d.context, t, d.cache, false);
|
||||
|
||||
emit signal_cache_proxy_task_progress(
|
||||
double(d.iterator.frame_index()) /
|
||||
double(d.iterator.size()));
|
||||
|
||||
if (!d.iterator.has_next()) {
|
||||
emit stop_cache_proxy_tasks();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
qWarning()
|
||||
<< "Failed to find node copy for video job, retrying";
|
||||
if (!delayed_requeue_timer_.isActive()) {
|
||||
delayed_requeue_timer_.start();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (d.iterator.has_next()) {
|
||||
break;
|
||||
} else {
|
||||
pending_video_jobs_.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle audio tasks
|
||||
while (!pending_audio_jobs_.empty() &&
|
||||
running_audio_tasks_.size() < max_tasks) {
|
||||
AudioJob &d = pending_audio_jobs_.front();
|
||||
|
||||
bool pop = true;
|
||||
|
||||
// Start job
|
||||
if (Node *copy = copier_->get_copy(d.node)) {
|
||||
TimeRange &queued_range = d.range;
|
||||
TimeRange use_range = queued_range;
|
||||
|
||||
if (dynamic_cast<AudioWaveformCache *>(d.cache)) {
|
||||
Rational new_out = std::min(
|
||||
use_range.in() +
|
||||
AudioVisualWaveform::k_minimum_sample_rate.flipped(),
|
||||
use_range.out());
|
||||
|
||||
if (new_out != use_range.out()) {
|
||||
use_range.set_out(new_out);
|
||||
queued_range.set_in(new_out);
|
||||
pop = false;
|
||||
}
|
||||
}
|
||||
|
||||
render_audio(copy, d.context, use_range, d.cache);
|
||||
} else {
|
||||
qWarning()
|
||||
<< "Failed to find node copy for audio job, retrying";
|
||||
pop = false;
|
||||
if (!delayed_requeue_timer_.isActive()) {
|
||||
delayed_requeue_timer_.start();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (pop) {
|
||||
pending_audio_jobs_.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderTicketWatcher *PreviewAutoCacher::render_frame(Node *node,
|
||||
ViewerOutput *context,
|
||||
const Rational &time,
|
||||
PlaybackCache *cache,
|
||||
bool dry)
|
||||
{
|
||||
RenderTicketWatcher *watcher = new RenderTicketWatcher();
|
||||
watcher->setProperty("job",
|
||||
QVariant::fromValue(copier_->get_last_update_time()));
|
||||
watcher->setProperty("cache", QtUtils::ptr_to_value(cache));
|
||||
watcher->setProperty("time", QVariant::fromValue(time));
|
||||
connect(watcher, &RenderTicketWatcher::finished, this,
|
||||
&PreviewAutoCacher::video_rendered);
|
||||
|
||||
running_video_tasks_.append(watcher);
|
||||
|
||||
RenderManager::RenderVideoParams rvp(node, context->get_video_params(),
|
||||
context->get_audio_params(), time,
|
||||
copied_color_manager_,
|
||||
RenderMode::k_offline);
|
||||
|
||||
if (FrameHashCache *frame_cache = dynamic_cast<FrameHashCache *>(cache)) {
|
||||
if (ThumbnailCache *wave_cache =
|
||||
dynamic_cast<ThumbnailCache *>(cache)) {
|
||||
Q_UNUSED(wave_cache)
|
||||
rvp.video_params.set_divider(
|
||||
VideoParams::get_divider_for_target_resolution(
|
||||
rvp.video_params.width(), rvp.video_params.height(), 160,
|
||||
120));
|
||||
rvp.force_format = PixelFormat::f32;
|
||||
rvp.force_channel_count = VideoParams::k_rgba_channel_count;
|
||||
} else {
|
||||
frame_cache->set_timebase(
|
||||
context->get_video_params().frame_rate_as_time_base());
|
||||
}
|
||||
|
||||
rvp.add_cache(frame_cache);
|
||||
} else {
|
||||
// Preview/display frames are rendered at reduced precision to cut the
|
||||
// GPU->CPU readback and IPC transfer bandwidth. The internal render
|
||||
// pipeline stays F32/ACEScg; the final preview copy is packed 10-bit
|
||||
// RGBA (4 bytes/pixel) to preserve 10-bit panel precision while halving
|
||||
// bandwidth compared to F16.
|
||||
rvp.force_format = PixelFormat::u10;
|
||||
rvp.force_channel_count = VideoParams::k_rgba_channel_count;
|
||||
}
|
||||
|
||||
// Video playback frames are rendered out-of-process. GPU textures cannot be
|
||||
// shared across worker processes (or across independent Vulkan instances),
|
||||
// so we always request CPU frames.
|
||||
rvp.return_type = dry ? RenderManager::k_null : RenderManager::k_frame;
|
||||
|
||||
// Allow using cached images for this render job
|
||||
rvp.use_cache = true;
|
||||
|
||||
// Multicam
|
||||
rvp.multicam = copier_->get_copy(multicam_);
|
||||
|
||||
watcher->set_ticket(RenderManager::instance()->render_frame(rvp));
|
||||
|
||||
// If the ticket finished synchronously, VideoRendered has already deleted the
|
||||
// watcher. The caller must not use this pointer in that case.
|
||||
if (!running_video_tasks_.contains(watcher)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return watcher;
|
||||
}
|
||||
|
||||
RenderTicketPtr PreviewAutoCacher::render_audio(Node *node,
|
||||
ViewerOutput *context,
|
||||
const TimeRange &r,
|
||||
PlaybackCache *cache)
|
||||
{
|
||||
RenderTicketWatcher *watcher = new RenderTicketWatcher();
|
||||
watcher->setProperty("job",
|
||||
QVariant::fromValue(copier_->get_last_update_time()));
|
||||
watcher->setProperty("node", QtUtils::ptr_to_value(node));
|
||||
watcher->setProperty("cache", QtUtils::ptr_to_value(cache));
|
||||
watcher->setProperty("time", QVariant::fromValue(r));
|
||||
connect(watcher, &RenderTicketWatcher::finished, this,
|
||||
&PreviewAutoCacher::audio_rendered);
|
||||
running_audio_tasks_.append(watcher);
|
||||
|
||||
AudioParams p = context->get_audio_params();
|
||||
const bool invalid_params =
|
||||
(p.sample_rate() <= 0 || p.channel_count() <= 0);
|
||||
if (invalid_params) {
|
||||
AudioParams fallback(
|
||||
OAK_CONFIG("DefaultSequenceAudioFrequency").toInt(),
|
||||
OAK_CONFIG("DefaultSequenceAudioLayout").toULongLong(),
|
||||
ViewerOutput::k_default_sample_format);
|
||||
p = fallback;
|
||||
}
|
||||
p.set_format(ViewerOutput::k_default_sample_format);
|
||||
|
||||
RenderManager::RenderAudioParams rap(node, r, p, RenderMode::k_offline);
|
||||
|
||||
rap.generate_waveforms = dynamic_cast<AudioWaveformCache *>(cache);
|
||||
rap.clamp = false;
|
||||
|
||||
RenderTicketPtr ticket = RenderManager::instance()->render_audio(rap);
|
||||
watcher->set_ticket(ticket);
|
||||
return ticket;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::conform_finished()
|
||||
{
|
||||
// Got an audio conform, requeue all the audio currently needing a conform
|
||||
last_conform_task_.acquire();
|
||||
|
||||
for (auto it = audio_cache_data_.begin(); it != audio_cache_data_.end();
|
||||
it++) {
|
||||
if (!it.key() || !it.value().context) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const TimeRange &range : it.value().needs_conform) {
|
||||
it.key()->request(it.value().context, range);
|
||||
}
|
||||
it.value().needs_conform.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::cache_proxy_task_cancelled()
|
||||
{
|
||||
pending_video_jobs_.clear();
|
||||
|
||||
try_render();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::force_cache_range(ViewerOutput *context,
|
||||
const TimeRange &range)
|
||||
{
|
||||
use_custom_range_ = true;
|
||||
custom_autocache_range_ = range;
|
||||
|
||||
// Re-hash these frames and start rendering
|
||||
start_caching_video_range(context, context->video_frame_cache(), range);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::set_project(Project *project)
|
||||
{
|
||||
if (project_ == project) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (project_) {
|
||||
// We must wait for any jobs to finish because they'll be using our copied graph and we're
|
||||
// about to destroy it
|
||||
|
||||
// Stop requeue timer if it's running
|
||||
delayed_requeue_timer_.stop();
|
||||
|
||||
// Handle video rendering tasks
|
||||
if (!running_video_tasks_.isEmpty()) {
|
||||
// Cancel any video tasks and wait for them to finish
|
||||
cancel_video_tasks(true);
|
||||
running_video_tasks_.clear();
|
||||
}
|
||||
|
||||
// Handle audio rendering tasks
|
||||
if (!running_audio_tasks_.isEmpty()) {
|
||||
// Cancel any audio tasks and wait for them to finish
|
||||
cancel_audio_tasks(true);
|
||||
running_audio_tasks_.clear();
|
||||
}
|
||||
|
||||
// Clear any single frame render that might be queued
|
||||
cancel_queued_single_frame_render();
|
||||
|
||||
// Not interested in video passthroughs anymore
|
||||
video_immediate_passthroughs_.clear();
|
||||
|
||||
// Disconnect from all node cache's
|
||||
for (auto it = copier_->get_node_map().cbegin();
|
||||
it != copier_->get_node_map().cend(); it++) {
|
||||
disconnect_from_node_cache(it.key());
|
||||
}
|
||||
|
||||
// Delete all of our copied nodes
|
||||
copier_->set_project(nullptr);
|
||||
|
||||
// Ensure all cache data is cleared
|
||||
video_cache_data_.clear();
|
||||
audio_cache_data_.clear();
|
||||
|
||||
// Clear multicam reference
|
||||
multicam_ = nullptr;
|
||||
}
|
||||
|
||||
project_ = project;
|
||||
|
||||
if (project_) {
|
||||
// Copy graph (this should always be a Project)
|
||||
set_renders_paused(true);
|
||||
|
||||
copier_->set_project(project_);
|
||||
|
||||
for (int i = 0; i < project_->nodes().size(); i++) {
|
||||
project_->nodes().at(i)->ConnectedToPreviewEvent();
|
||||
}
|
||||
|
||||
// Find copied viewer node
|
||||
copied_color_manager_ = copier_->get_copied_project()->color_manager();
|
||||
|
||||
set_renders_paused(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_AUTOCACHER_H
|
||||
#define OAK_AUTOCACHER_H
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
#include "node/group/group.h"
|
||||
#include "node/node.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "node/project.h"
|
||||
#include "render/projectcopier.h"
|
||||
#include "render/renderjobtracker.h"
|
||||
#include "render/renderticket.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Manager for dynamically caching a sequence in the background
|
||||
*
|
||||
* Intended to be used with a Viewer to dynamically cache parts of a sequence based on the playhead.
|
||||
*/
|
||||
class PreviewAutoCacher : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
PreviewAutoCacher(QObject *parent = nullptr);
|
||||
|
||||
virtual ~PreviewAutoCacher() override;
|
||||
|
||||
RenderTicketPtr get_single_frame(ViewerOutput *viewer, const Rational &t,
|
||||
bool dry = false);
|
||||
RenderTicketPtr get_single_frame(Node *n, ViewerOutput *viewer,
|
||||
const Rational &t, bool dry = false);
|
||||
|
||||
RenderTicketPtr get_range_of_audio(ViewerOutput *viewer, TimeRange range);
|
||||
|
||||
void clear_single_frame_renders();
|
||||
void clear_single_frame_renders_that_arent_running();
|
||||
|
||||
/**
|
||||
* @brief Set the viewer node to auto-cache
|
||||
*/
|
||||
void set_project(Project *project);
|
||||
|
||||
/**
|
||||
* @brief Force a certain range to be cached
|
||||
*
|
||||
* Usually, PreviewAutoCacher caches a user-defined range around the playhead, however there are
|
||||
* times they may want certain non-playhead-related time ranges to be cached (i.e. entire sequence
|
||||
* or in/out range), so that can be set here.
|
||||
*/
|
||||
void force_cache_range(ViewerOutput *context, const TimeRange &range);
|
||||
|
||||
/**
|
||||
* @brief Updates the range of frames to auto-cache
|
||||
*/
|
||||
void set_playhead(const Rational &playhead);
|
||||
|
||||
/**
|
||||
* @brief Call cancel on all currently running video tasks
|
||||
*
|
||||
* Signalling cancel to a video task indicates that we're no longer interested in its end result.
|
||||
* This does not end all video tasks immediately, the RenderManager will do what it can to speed
|
||||
* up finishing the task. The RenderManager will also return "no result", which can be checked
|
||||
* with watcher->HasResult.
|
||||
*/
|
||||
void cancel_video_tasks(bool and_wait_for_them_to_finish = false);
|
||||
void cancel_audio_tasks(bool and_wait_for_them_to_finish = false);
|
||||
|
||||
bool is_rendering_custom_range() const;
|
||||
|
||||
void set_renders_paused(bool e);
|
||||
void set_thumbnails_paused(bool e);
|
||||
|
||||
void set_multicam_node(MultiCamNode *n)
|
||||
{
|
||||
multicam_ = n;
|
||||
}
|
||||
|
||||
void set_ignore_cache_requests(bool e)
|
||||
{
|
||||
ignore_cache_requests_ = e;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void set_display_color_processor(ColorProcessorPtr processor)
|
||||
{
|
||||
display_color_processor_ = processor;
|
||||
}
|
||||
|
||||
signals:
|
||||
void stop_cache_proxy_tasks();
|
||||
|
||||
void signal_cache_proxy_task_progress(double d);
|
||||
|
||||
private:
|
||||
void try_render();
|
||||
|
||||
RenderTicketWatcher *render_frame(Node *node, ViewerOutput *context,
|
||||
const Rational &time, PlaybackCache *cache,
|
||||
bool dry);
|
||||
|
||||
RenderTicketPtr render_audio(Node *node, ViewerOutput *context,
|
||||
const TimeRange &range, PlaybackCache *cache);
|
||||
|
||||
void connect_to_node_cache(Node *node);
|
||||
void disconnect_from_node_cache(Node *node);
|
||||
|
||||
void cancel_queued_single_frame_render();
|
||||
|
||||
void start_caching_range(const TimeRange &range, TimeRangeList *range_list,
|
||||
RenderJobTracker *tracker);
|
||||
void start_caching_video_range(ViewerOutput *context, PlaybackCache *cache,
|
||||
const TimeRange &range);
|
||||
void start_caching_audio_range(ViewerOutput *context, PlaybackCache *cache,
|
||||
const TimeRange &range);
|
||||
|
||||
void video_invalidated_from_node(ViewerOutput *context, PlaybackCache *cache,
|
||||
const olive::TimeRange &range);
|
||||
void audio_invalidated_from_node(ViewerOutput *context, PlaybackCache *cache,
|
||||
const olive::TimeRange &range);
|
||||
|
||||
Project *project_;
|
||||
|
||||
ProjectCopier *copier_;
|
||||
|
||||
TimeRange cache_range_;
|
||||
|
||||
bool use_custom_range_;
|
||||
TimeRange custom_autocache_range_;
|
||||
|
||||
bool pause_renders_;
|
||||
bool pause_thumbnails_;
|
||||
|
||||
RenderTicketPtr single_frame_render_;
|
||||
QMap<RenderTicketWatcher *, QVector<RenderTicketPtr>>
|
||||
video_immediate_passthroughs_;
|
||||
|
||||
QTimer delayed_requeue_timer_;
|
||||
|
||||
JobTime last_conform_task_;
|
||||
|
||||
QVector<RenderTicketWatcher *> running_video_tasks_;
|
||||
QVector<RenderTicketWatcher *> running_audio_tasks_;
|
||||
|
||||
ColorManager *copied_color_manager_;
|
||||
|
||||
struct VideoJob {
|
||||
Node *node;
|
||||
ViewerOutput *context;
|
||||
PlaybackCache *cache;
|
||||
TimeRange range;
|
||||
TimeRangeListFrameIterator iterator;
|
||||
};
|
||||
|
||||
struct VideoCacheData {
|
||||
RenderJobTracker job_tracker;
|
||||
};
|
||||
|
||||
struct AudioJob {
|
||||
Node *node;
|
||||
ViewerOutput *context;
|
||||
PlaybackCache *cache;
|
||||
TimeRange range;
|
||||
};
|
||||
|
||||
struct AudioCacheData {
|
||||
RenderJobTracker job_tracker;
|
||||
TimeRangeList needs_conform;
|
||||
ViewerOutput *context = nullptr;
|
||||
};
|
||||
|
||||
std::list<VideoJob> pending_video_jobs_;
|
||||
std::list<AudioJob> pending_audio_jobs_;
|
||||
|
||||
QHash<PlaybackCache *, VideoCacheData> video_cache_data_;
|
||||
QHash<PlaybackCache *, AudioCacheData> audio_cache_data_;
|
||||
|
||||
ColorProcessorPtr display_color_processor_;
|
||||
|
||||
MultiCamNode *multicam_;
|
||||
|
||||
bool ignore_cache_requests_;
|
||||
|
||||
private slots:
|
||||
/**
|
||||
* @brief Handler for when the NodeGraph reports a video change over a certain time range
|
||||
*/
|
||||
void video_invalidated_from_cache(ViewerOutput *context,
|
||||
const olive::TimeRange &range);
|
||||
|
||||
/**
|
||||
* @brief Handler for when the NodeGraph reports a audio change over a certain time range
|
||||
*/
|
||||
void audio_invalidated_from_cache(ViewerOutput *context,
|
||||
const olive::TimeRange &range);
|
||||
|
||||
void cancel_for_cache();
|
||||
|
||||
/**
|
||||
* @brief Handler for when the RenderManager has returned rendered audio
|
||||
*/
|
||||
void audio_rendered();
|
||||
|
||||
/**
|
||||
* @brief Handler for when the RenderManager has returned rendered video frames
|
||||
*/
|
||||
void video_rendered();
|
||||
|
||||
/**
|
||||
* @brief Generic function called whenever the frames to render need to be (re)queued
|
||||
*/
|
||||
//void RequeueFrames();
|
||||
|
||||
void conform_finished();
|
||||
|
||||
void cache_proxy_task_cancelled();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUTOCACHER_H
|
||||
@@ -0,0 +1,371 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "projectcopier.h"
|
||||
|
||||
#include "node/group/group.h"
|
||||
#include "node/project/footage/footage.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
ProjectCopier::ProjectCopier(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
original_ = nullptr;
|
||||
copy_ = new Project();
|
||||
copy_->setParent(this);
|
||||
}
|
||||
|
||||
void ProjectCopier::set_project(Project *project)
|
||||
{
|
||||
if (original_) {
|
||||
// Clear current project
|
||||
qDeleteAll(created_nodes_);
|
||||
created_nodes_.clear();
|
||||
copy_map_.clear();
|
||||
graph_update_queue_.clear();
|
||||
|
||||
disconnect(original_, &Project::node_added, this,
|
||||
&ProjectCopier::queue_node_add);
|
||||
disconnect(original_, &Project::node_removed, this,
|
||||
&ProjectCopier::queue_node_remove);
|
||||
disconnect(original_, &Project::input_connected, this,
|
||||
&ProjectCopier::queue_edge_add);
|
||||
disconnect(original_, &Project::input_disconnected, this,
|
||||
&ProjectCopier::queue_edge_remove);
|
||||
disconnect(original_, &Project::value_changed, this,
|
||||
&ProjectCopier::queue_value_change);
|
||||
disconnect(original_, &Project::input_value_hint_changed, this,
|
||||
&ProjectCopier::queue_value_hint_change);
|
||||
disconnect(original_, &Project::setting_changed, this,
|
||||
&ProjectCopier::queue_project_setting_change);
|
||||
}
|
||||
|
||||
original_ = project;
|
||||
|
||||
if (original_) {
|
||||
// The copied project is only used as an in-memory render proxy. Mark it so
|
||||
// downstream code (e.g. RenderWorkerPool) knows it is safe to reset its
|
||||
// modified flag after serializing a snapshot.
|
||||
copy_->setProperty("_oak_render_proxy", true);
|
||||
|
||||
// Add all nodes
|
||||
for (int i = 0; i < copy_->nodes().size(); i++) {
|
||||
insert_into_copy_map(original_->nodes().at(i), copy_->nodes().at(i));
|
||||
}
|
||||
|
||||
for (int i = copy_->nodes().size(); i < original_->nodes().size();
|
||||
i++) {
|
||||
do_node_add(original_->nodes().at(i));
|
||||
}
|
||||
|
||||
// Add all connections
|
||||
foreach (Node *node, original_->nodes()) {
|
||||
for (auto it = node->input_connections().cbegin();
|
||||
it != node->input_connections().cend(); it++) {
|
||||
do_edge_add(it->second, it->first);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy project settings
|
||||
Project::copy_settings(original_, copy_);
|
||||
|
||||
// Ensure graph change value is just before the sync value
|
||||
update_graph_change_value();
|
||||
update_last_synced_value();
|
||||
|
||||
// Connect signals for future node additions/deletions
|
||||
connect(original_, &Project::node_added, this,
|
||||
&ProjectCopier::queue_node_add, Qt::DirectConnection);
|
||||
connect(original_, &Project::node_removed, this,
|
||||
&ProjectCopier::queue_node_remove, Qt::DirectConnection);
|
||||
connect(original_, &Project::input_connected, this,
|
||||
&ProjectCopier::queue_edge_add, Qt::DirectConnection);
|
||||
connect(original_, &Project::input_disconnected, this,
|
||||
&ProjectCopier::queue_edge_remove, Qt::DirectConnection);
|
||||
connect(original_, &Project::value_changed, this,
|
||||
&ProjectCopier::queue_value_change, Qt::DirectConnection);
|
||||
connect(original_, &Project::input_value_hint_changed, this,
|
||||
&ProjectCopier::queue_value_hint_change, Qt::DirectConnection);
|
||||
connect(original_, &Project::setting_changed, this,
|
||||
&ProjectCopier::queue_project_setting_change,
|
||||
Qt::DirectConnection);
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectCopier::process_update_queue()
|
||||
{
|
||||
bool copy_changed = false;
|
||||
|
||||
// Iterate everything that happened to the graph and do the same thing on our end
|
||||
while (!graph_update_queue_.empty()) {
|
||||
QueuedJob job = graph_update_queue_.front();
|
||||
graph_update_queue_.pop_front();
|
||||
copy_changed = true;
|
||||
|
||||
switch (job.type) {
|
||||
case QueuedJob::k_node_added:
|
||||
do_node_add(job.node);
|
||||
break;
|
||||
case QueuedJob::k_node_removed:
|
||||
do_node_remove(job.node);
|
||||
break;
|
||||
case QueuedJob::k_edge_added:
|
||||
do_edge_add(job.output, job.input);
|
||||
break;
|
||||
case QueuedJob::k_edge_removed:
|
||||
do_edge_remove(job.output, job.input);
|
||||
break;
|
||||
case QueuedJob::k_value_changed:
|
||||
do_value_change(job.input);
|
||||
break;
|
||||
case QueuedJob::k_value_hint_changed:
|
||||
do_value_hint_change(job.input);
|
||||
break;
|
||||
case QueuedJob::k_project_setting_changed:
|
||||
do_project_setting_change(job.key, job.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The copied project is not saved, so its modified flag is only used by the
|
||||
// render worker pool to decide whether the serialized graph snapshot is stale.
|
||||
// Mark it modified whenever the copy has actually changed.
|
||||
if (copy_changed) {
|
||||
copy_->set_modified(true);
|
||||
}
|
||||
|
||||
// Indicate that we have synchronized to this point, which is compared with the graph change
|
||||
// time to see if our copied graph is up to date
|
||||
update_last_synced_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::do_node_add(Node *node)
|
||||
{
|
||||
if (dynamic_cast<NodeGroup *>(node)) {
|
||||
// Group nodes are just dummy nodes, no need to copy them
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy node
|
||||
Node *copy = node->copy();
|
||||
|
||||
// Add to project
|
||||
copy->setParent(copy_);
|
||||
|
||||
// Disable caches for copy
|
||||
copy->set_caches_enabled(false);
|
||||
|
||||
// Copy cache UUIDs
|
||||
copy->copy_cache_uuids_from(node);
|
||||
|
||||
// Insert into map
|
||||
insert_into_copy_map(node, copy);
|
||||
|
||||
// Keep track of our nodes
|
||||
created_nodes_.append(copy);
|
||||
}
|
||||
|
||||
void ProjectCopier::do_node_remove(Node *node)
|
||||
{
|
||||
// Find our copy and remove it
|
||||
Node *copy = copy_map_.take(node);
|
||||
|
||||
// Disconnect from node's caches
|
||||
emit removed_node(node);
|
||||
|
||||
// Remove from created list
|
||||
created_nodes_.removeOne(copy);
|
||||
|
||||
// Delete it
|
||||
delete copy;
|
||||
}
|
||||
|
||||
void ProjectCopier::do_edge_add(Node *output, const NodeInput &input)
|
||||
{
|
||||
// Create same connection with our copied graph
|
||||
Node *our_output = copy_map_.value(output);
|
||||
Node *our_input = copy_map_.value(input.node());
|
||||
|
||||
Node::connect_edge(our_output,
|
||||
NodeInput(our_input, input.input(), input.element()));
|
||||
}
|
||||
|
||||
void ProjectCopier::do_edge_remove(Node *output, const NodeInput &input)
|
||||
{
|
||||
// Remove same connection with our copied graph
|
||||
Node *our_output = copy_map_.value(output);
|
||||
Node *our_input = copy_map_.value(input.node());
|
||||
|
||||
Node::disconnect_edge(our_output,
|
||||
NodeInput(our_input, input.input(), input.element()));
|
||||
}
|
||||
|
||||
void ProjectCopier::do_value_change(const NodeInput &input)
|
||||
{
|
||||
if (dynamic_cast<NodeGroup *>(input.node())) {
|
||||
// Group nodes are just dummy nodes, no need to copy them
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy all values to our graph
|
||||
Node *our_input = copy_map_.value(input.node());
|
||||
Node::copy_values_of_element(input.node(), our_input, input.input(),
|
||||
input.element());
|
||||
}
|
||||
|
||||
void ProjectCopier::do_value_hint_change(const NodeInput &input)
|
||||
{
|
||||
if (dynamic_cast<NodeGroup *>(input.node())) {
|
||||
// Group nodes are just dummy nodes, no need to copy them
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy value hint to our graph
|
||||
Node *our_input = copy_map_.value(input.node());
|
||||
Node::ValueHint hint =
|
||||
input.node()->get_value_hint_for_input(input.input(), input.element());
|
||||
our_input->set_value_hint_for_input(input.input(), hint, input.element());
|
||||
}
|
||||
|
||||
void ProjectCopier::do_project_setting_change(const QString &key,
|
||||
const QString &value)
|
||||
{
|
||||
copy_->set_setting(key, value);
|
||||
}
|
||||
|
||||
void ProjectCopier::insert_into_copy_map(Node *node, Node *copy)
|
||||
{
|
||||
// Insert into map
|
||||
copy_map_.insert(node, copy);
|
||||
|
||||
// Copy parameters
|
||||
Node::copy_inputs(node, copy, false);
|
||||
|
||||
// Sync Footage proxy state (which is not stored as a Node input)
|
||||
if (Footage *src_footage = dynamic_cast<Footage *>(node)) {
|
||||
if (dynamic_cast<Footage *>(copy)) {
|
||||
connect(src_footage, &Footage::proxy_settings_changed, this,
|
||||
[this, src_footage]() {
|
||||
sync_footage_proxy_settings(src_footage);
|
||||
});
|
||||
sync_footage_proxy_settings(src_footage);
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to node's cache
|
||||
emit added_node(node);
|
||||
}
|
||||
|
||||
void ProjectCopier::sync_footage_proxy_settings(Footage *source)
|
||||
{
|
||||
Footage *copy = get_copy(source);
|
||||
if (!copy) {
|
||||
qWarning() << "ProjectCopier::SyncFootageProxySettings: no copy for"
|
||||
<< source->filename();
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug()
|
||||
<< "ProjectCopier::SyncFootageProxySettings:" << source->filename()
|
||||
<< "enabled=" << source->proxy_enabled() << "->"
|
||||
<< copy->proxy_enabled()
|
||||
<< "state=" << ProxyManager::proxy_state_to_string(source->proxy_state());
|
||||
|
||||
copy->set_proxy(source->proxy_path(), source->proxy_state(),
|
||||
source->proxy_video_stream_index(),
|
||||
source->proxy_preset_version(), source->proxy_enabled());
|
||||
|
||||
if (Project *cp = copy->project()) {
|
||||
cp->set_modified(true);
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_node_add(Node *node)
|
||||
{
|
||||
graph_update_queue_.push_back({ QueuedJob::k_node_added, node, NodeInput(),
|
||||
nullptr, QString(), QString() });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_node_remove(Node *node)
|
||||
{
|
||||
graph_update_queue_.push_back({ QueuedJob::k_node_removed, node, NodeInput(),
|
||||
nullptr, QString(), QString() });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_edge_add(Node *output, const NodeInput &input)
|
||||
{
|
||||
graph_update_queue_.push_back({ QueuedJob::k_edge_added, nullptr, input,
|
||||
output, QString(), QString() });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_edge_remove(Node *output, const NodeInput &input)
|
||||
{
|
||||
graph_update_queue_.push_back({ QueuedJob::k_edge_removed, nullptr, input,
|
||||
output, QString(), QString() });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_value_change(const NodeInput &input)
|
||||
{
|
||||
/*for (auto it = graph_update_queue_.begin(); it != graph_update_queue_.end(); ) {
|
||||
if (it->type == QueuedJob::kValueChanged && it->input == input) {
|
||||
it = graph_update_queue_.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}*/
|
||||
|
||||
graph_update_queue_.push_back({ QueuedJob::k_value_changed, nullptr, input,
|
||||
nullptr, QString(), QString() });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_value_hint_change(const NodeInput &input)
|
||||
{
|
||||
graph_update_queue_.push_back({ QueuedJob::k_value_hint_changed, nullptr,
|
||||
input, nullptr, QString(), QString() });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_project_setting_change(const QString &key,
|
||||
const QString &value)
|
||||
{
|
||||
graph_update_queue_.push_back({ QueuedJob::k_project_setting_changed, nullptr,
|
||||
NodeInput(), nullptr, key, value });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::update_graph_change_value()
|
||||
{
|
||||
graph_changed_time_.acquire();
|
||||
}
|
||||
|
||||
void ProjectCopier::update_last_synced_value()
|
||||
{
|
||||
last_update_time_.acquire();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_PROJECTCOPIER_H
|
||||
#define OAK_PROJECTCOPIER_H
|
||||
|
||||
#include "node/project.h"
|
||||
#include "node/project/footage/footage.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class ProjectCopier : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ProjectCopier(QObject *parent = nullptr);
|
||||
|
||||
void set_project(Project *project);
|
||||
|
||||
template <typename T> T *get_copy(T *original)
|
||||
{
|
||||
return static_cast<T *>(copy_map_.value(original));
|
||||
}
|
||||
|
||||
template <typename T> T *get_original(T *copy)
|
||||
{
|
||||
return static_cast<T *>(copy_map_.key(copy));
|
||||
}
|
||||
|
||||
Project *get_copied_project() const
|
||||
{
|
||||
return copy_;
|
||||
}
|
||||
|
||||
const QHash<Node *, Node *> &get_node_map() const
|
||||
{
|
||||
return copy_map_;
|
||||
}
|
||||
|
||||
const JobTime &get_graph_change_time() const
|
||||
{
|
||||
return graph_changed_time_;
|
||||
}
|
||||
const JobTime &get_last_update_time() const
|
||||
{
|
||||
return last_update_time_;
|
||||
}
|
||||
|
||||
bool has_updates_in_queue() const
|
||||
{
|
||||
return !graph_update_queue_.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Process all changes to internal NodeGraph copy
|
||||
*
|
||||
* PreviewAutoCacher staggers updates to its internal NodeGraph copy, only applying them when the
|
||||
* RenderManager is not reading from it. This function is called when such an opportunity arises.
|
||||
*/
|
||||
void process_update_queue();
|
||||
|
||||
signals:
|
||||
void added_node(Node *n);
|
||||
void removed_node(Node *n);
|
||||
|
||||
private:
|
||||
void do_node_add(Node *node);
|
||||
void do_node_remove(Node *node);
|
||||
void do_edge_add(Node *output, const NodeInput &input);
|
||||
void do_edge_remove(Node *output, const NodeInput &input);
|
||||
void do_value_change(const NodeInput &input);
|
||||
void do_value_hint_change(const NodeInput &input);
|
||||
void do_project_setting_change(const QString &key, const QString &value);
|
||||
|
||||
void sync_footage_proxy_settings(Footage *source);
|
||||
|
||||
void insert_into_copy_map(Node *node, Node *copy);
|
||||
|
||||
void update_graph_change_value();
|
||||
void update_last_synced_value();
|
||||
|
||||
Project *original_;
|
||||
Project *copy_;
|
||||
|
||||
class QueuedJob {
|
||||
public:
|
||||
enum Type {
|
||||
k_node_added,
|
||||
k_node_removed,
|
||||
k_edge_added,
|
||||
k_edge_removed,
|
||||
k_value_changed,
|
||||
k_value_hint_changed,
|
||||
k_project_setting_changed
|
||||
};
|
||||
|
||||
Type type;
|
||||
Node *node;
|
||||
NodeInput input;
|
||||
Node *output;
|
||||
|
||||
QString key;
|
||||
QString value;
|
||||
};
|
||||
|
||||
std::list<QueuedJob> graph_update_queue_;
|
||||
QHash<Node *, Node *> copy_map_;
|
||||
QHash<Project *, Project *> graph_map_;
|
||||
QVector<Node *> created_nodes_;
|
||||
|
||||
JobTime graph_changed_time_;
|
||||
JobTime last_update_time_;
|
||||
|
||||
private slots:
|
||||
void queue_node_add(Node *node);
|
||||
|
||||
void queue_node_remove(Node *node);
|
||||
|
||||
void queue_edge_add(Node *output, const NodeInput &input);
|
||||
|
||||
void queue_edge_remove(Node *output, const NodeInput &input);
|
||||
|
||||
void queue_value_change(const NodeInput &input);
|
||||
|
||||
void queue_value_hint_change(const NodeInput &input);
|
||||
|
||||
void queue_project_setting_change(const QString &key, const QString &value);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_PROJECTCOPIER_H
|
||||
@@ -0,0 +1,51 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_RENDERCACHE_H
|
||||
#define OAK_RENDERCACHE_H
|
||||
|
||||
#include "codec/decoder.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
template <typename K, typename V> class RenderCache : public QHash<K, V> {
|
||||
public:
|
||||
QMutex *mutex()
|
||||
{
|
||||
return &mutex_;
|
||||
}
|
||||
|
||||
private:
|
||||
QMutex mutex_;
|
||||
};
|
||||
|
||||
struct DecoderPair {
|
||||
DecoderPtr decoder = nullptr;
|
||||
qint64 last_modified = 0;
|
||||
};
|
||||
|
||||
using DecoderCache = RenderCache<Decoder::CodecStream, DecoderPair>;
|
||||
using ShaderCache = RenderCache<QString, QVariant>;
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERCACHE_H
|
||||
@@ -0,0 +1,194 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "renderer.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QVector2D>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
Renderer::Renderer(QObject *parent)
|
||||
: QObject(parent)
|
||||
, lifetime_(std::make_shared<RendererLifetime>())
|
||||
{
|
||||
}
|
||||
|
||||
Renderer::~Renderer()
|
||||
{
|
||||
destroyed_ = true;
|
||||
if (lifetime_) {
|
||||
lifetime_->alive = false;
|
||||
}
|
||||
}
|
||||
|
||||
TexturePtr Renderer::create_texture(const VideoParams ¶ms, const void *data,
|
||||
int linesize)
|
||||
{
|
||||
QVariant v;
|
||||
|
||||
if (use_texture_cache) {
|
||||
QMutexLocker locker(&texture_cache_lock_);
|
||||
for (auto it = texture_cache_.begin(); it != texture_cache_.end();
|
||||
it++) {
|
||||
if (it->width == params.effective_width() &&
|
||||
it->height == params.effective_height() &&
|
||||
it->depth == params.effective_depth() &&
|
||||
it->format == params.format() &&
|
||||
it->channel_count == params.channel_count()) {
|
||||
v = it->handle;
|
||||
texture_cache_.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (v.isNull()) {
|
||||
v = create_native_texture(params.effective_width(),
|
||||
params.effective_height(),
|
||||
params.effective_depth(), params.format(),
|
||||
params.channel_count(), data, linesize);
|
||||
} else if (data) {
|
||||
upload_to_texture(v, params, data, linesize);
|
||||
} else {
|
||||
this->flush();
|
||||
}
|
||||
|
||||
return create_texture_from_native_handle(v, params);
|
||||
}
|
||||
|
||||
void Renderer::destroy_texture(Texture *texture)
|
||||
{
|
||||
if (destroyed_) {
|
||||
return;
|
||||
}
|
||||
if (use_texture_cache) {
|
||||
// HACK: Dirty, dirty hack. OpenGL uses "contexts" to store all of its data, and each context
|
||||
// can only be used by the thread that created it. However there are also "shared contexts"
|
||||
// where assets from one context can be used in another. We use shared contexts so that
|
||||
// textures rendered in the background can be displayed on the screen, travelling from
|
||||
// a background thread to the main UI thread. However, when that texture is destroyed, it
|
||||
// comes back here to be placed in the texture cache. But that leads to a race condition
|
||||
// because it will call the background thread's renderer in the main thread. Since all
|
||||
// assets are shared, we could technically just get the texture to call "destroy" in the
|
||||
// viewer's renderer instance, but that would mean all textures would end up stranded
|
||||
// there unusable by the background renderer, negating the very advantage of the texture
|
||||
// cache in the first place. Therefore, we simply allow the thread calling to happen, and
|
||||
// use mutexes to prevent race conditions.
|
||||
//
|
||||
// Presumably Vulkan would not have this issue because it allows for application-wide
|
||||
// instances and multithreading.
|
||||
texture_cache_lock_.lock();
|
||||
texture_cache_.push_back(
|
||||
{ texture->params().effective_width(),
|
||||
texture->params().effective_height(),
|
||||
texture->params().effective_depth(), texture->params().format(),
|
||||
texture->params().channel_count(), texture->id(),
|
||||
QDateTime::currentMSecsSinceEpoch() });
|
||||
texture_cache_lock_.unlock();
|
||||
|
||||
if (QThread::currentThread() == this->thread()) {
|
||||
clear_old_textures();
|
||||
}
|
||||
} else {
|
||||
destroy_native_texture(texture->id());
|
||||
}
|
||||
}
|
||||
|
||||
QVariant Renderer::get_default_shader()
|
||||
{
|
||||
QMutexLocker locker(&color_cache_mutex_);
|
||||
|
||||
if (default_shader_.isNull()) {
|
||||
default_shader_ = create_native_shader(ShaderCode(QString(), QString()));
|
||||
}
|
||||
|
||||
return default_shader_;
|
||||
}
|
||||
|
||||
void Renderer::destroy()
|
||||
{
|
||||
if (!default_shader_.isNull()) {
|
||||
destroy_native_shader(default_shader_);
|
||||
default_shader_.clear();
|
||||
}
|
||||
|
||||
{
|
||||
QMutexLocker locker(&color_cache_mutex_);
|
||||
|
||||
// Destroy the cached native shaders explicitly. The LUT textures are
|
||||
// TexturePtrs whose destructors call DestroyTexture(), so the cache must
|
||||
// be cleared while the renderer is still alive for those to be honored.
|
||||
for (auto it = color_cache_.begin(); it != color_cache_.end(); it++) {
|
||||
if (!it->compiled_shader.isNull()) {
|
||||
destroy_native_shader(it->compiled_shader);
|
||||
}
|
||||
}
|
||||
color_cache_.clear();
|
||||
}
|
||||
|
||||
if (!interlace_texture_.isNull()) {
|
||||
destroy_native_shader(interlace_texture_);
|
||||
interlace_texture_.clear();
|
||||
}
|
||||
|
||||
for (auto it = texture_cache_.begin(); it != texture_cache_.end(); it++) {
|
||||
destroy_native_texture(it->handle);
|
||||
}
|
||||
texture_cache_.clear();
|
||||
|
||||
destroyed_ = true;
|
||||
if (lifetime_) {
|
||||
lifetime_->alive = false;
|
||||
}
|
||||
|
||||
destroy_internal();
|
||||
}
|
||||
|
||||
TexturePtr Renderer::create_texture_from_native_handle(const QVariant &v,
|
||||
const VideoParams ¶ms)
|
||||
{
|
||||
if (v.isNull()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_shared<Texture>(this, v, params, lifetime_);
|
||||
}
|
||||
|
||||
void Renderer::clear_old_textures()
|
||||
{
|
||||
QMutexLocker locker(&texture_cache_lock_);
|
||||
|
||||
for (auto it = texture_cache_.begin(); it != texture_cache_.end();) {
|
||||
if (it->accessed <
|
||||
QDateTime::currentMSecsSinceEpoch() - max_texture_life) {
|
||||
destroy_native_texture(it->handle);
|
||||
it = texture_cache_.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace olive
|
||||
@@ -0,0 +1,217 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_RENDERCONTEXT_H
|
||||
#define OAK_RENDERCONTEXT_H
|
||||
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include "common/define.h"
|
||||
#include "render/shadercode.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "texture.h"
|
||||
|
||||
// Forward declarations to keep the render core header lightweight
|
||||
namespace olive
|
||||
{
|
||||
class ColorTransformJob;
|
||||
class Node;
|
||||
}
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class ShaderJob;
|
||||
|
||||
class Renderer : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
Renderer(QObject *parent = nullptr);
|
||||
virtual ~Renderer() override;
|
||||
|
||||
virtual bool init() = 0;
|
||||
|
||||
TexturePtr create_texture(const VideoParams ¶ms,
|
||||
const void *data = nullptr, int linesize = 0);
|
||||
|
||||
void destroy_texture(Texture *texture);
|
||||
|
||||
virtual void blit_to_texture(QVariant shader, olive::AcceleratedJob &job,
|
||||
olive::Texture *destination,
|
||||
bool clear_destination = true)
|
||||
{
|
||||
blit(shader, job, destination, destination->params(),
|
||||
clear_destination);
|
||||
}
|
||||
|
||||
void blit(QVariant shader, olive::AcceleratedJob &job,
|
||||
olive::VideoParams params, bool clear_destination = true)
|
||||
{
|
||||
blit(shader, job, nullptr, params, clear_destination);
|
||||
}
|
||||
|
||||
void blit_color_managed(const ColorTransformJob &color_job,
|
||||
Texture *destination, const VideoParams ¶ms);
|
||||
void blit_color_managed(const ColorTransformJob &job, Texture *destination)
|
||||
{
|
||||
blit_color_managed(job, destination, destination->params());
|
||||
}
|
||||
void blit_color_managed(const ColorTransformJob &job,
|
||||
const VideoParams ¶ms)
|
||||
{
|
||||
blit_color_managed(job, nullptr, params);
|
||||
}
|
||||
|
||||
TexturePtr interlace_texture(TexturePtr top, TexturePtr bottom,
|
||||
const VideoParams ¶ms);
|
||||
|
||||
QVariant get_default_shader();
|
||||
|
||||
void destroy();
|
||||
|
||||
virtual void post_destroy() = 0;
|
||||
|
||||
virtual void post_init() = 0;
|
||||
|
||||
virtual void clear_destination(olive::Texture *texture = nullptr,
|
||||
double r = 0.0, double g = 0.0,
|
||||
double b = 0.0, double a = 1.0) = 0;
|
||||
|
||||
virtual QVariant create_native_shader(olive::ShaderCode code) = 0;
|
||||
|
||||
virtual void destroy_native_shader(QVariant shader) = 0;
|
||||
|
||||
virtual void upload_to_texture(const QVariant &handle,
|
||||
const VideoParams ¶ms, const void *data,
|
||||
int linesize) = 0;
|
||||
|
||||
virtual void download_from_texture(const QVariant &handle,
|
||||
const VideoParams ¶ms, void *data,
|
||||
int linesize) = 0;
|
||||
|
||||
virtual void flush() = 0;
|
||||
|
||||
virtual Color get_pixel_from_texture(olive::Texture *texture,
|
||||
const QPointF &pt) = 0;
|
||||
std::shared_ptr<RendererLifetime> get_lifetime() const
|
||||
{
|
||||
return lifetime_;
|
||||
}
|
||||
|
||||
virtual bool is_open_gl() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool is_vulkan() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Attach a texture as the current output destination for OFX plugin
|
||||
* OpenGL rendering.
|
||||
*
|
||||
* Default implementation is a no-op. OpenGL-based renderers override this
|
||||
* to bind the texture as a framebuffer render target.
|
||||
*/
|
||||
virtual void attach_output_texture(olive::Texture *texture)
|
||||
{
|
||||
(void)texture;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Detach the current OFX plugin OpenGL output texture.
|
||||
*
|
||||
* Default implementation is a no-op.
|
||||
*/
|
||||
virtual void detach_output_texture()
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void blit(QVariant shader, olive::AcceleratedJob &job,
|
||||
olive::Texture *destination,
|
||||
olive::VideoParams destination_params,
|
||||
bool clear_destination) = 0;
|
||||
virtual QVariant create_native_texture(int width, int height, int depth,
|
||||
PixelFormat format, int channel_count,
|
||||
const void *data = nullptr,
|
||||
int linesize = 0) = 0;
|
||||
|
||||
virtual void destroy_native_texture(QVariant texture) = 0;
|
||||
|
||||
virtual void destroy_internal() = 0;
|
||||
|
||||
private:
|
||||
std::atomic<bool> destroyed_{ false };
|
||||
std::shared_ptr<RendererLifetime> lifetime_;
|
||||
struct ColorContext {
|
||||
struct LUT {
|
||||
TexturePtr texture;
|
||||
Texture::Interpolation interpolation;
|
||||
QString name;
|
||||
};
|
||||
|
||||
QVariant compiled_shader;
|
||||
QVector<LUT> lut3d_textures;
|
||||
QVector<LUT> lut1d_textures;
|
||||
};
|
||||
|
||||
TexturePtr create_texture_from_native_handle(const QVariant &v,
|
||||
const VideoParams ¶ms);
|
||||
|
||||
bool get_color_context(const ColorTransformJob &color_job, ColorContext *ctx);
|
||||
|
||||
void clear_old_textures();
|
||||
|
||||
QHash<QString, ColorContext> color_cache_;
|
||||
|
||||
struct CachedTexture {
|
||||
int width;
|
||||
int height;
|
||||
int depth;
|
||||
PixelFormat format;
|
||||
int channel_count;
|
||||
QVariant handle;
|
||||
qint64 accessed;
|
||||
};
|
||||
|
||||
static const int max_texture_life = 5000;
|
||||
static const bool use_texture_cache = true;
|
||||
std::list<CachedTexture> texture_cache_;
|
||||
|
||||
QMutex color_cache_mutex_;
|
||||
|
||||
QVariant default_shader_;
|
||||
|
||||
QVariant interlace_texture_;
|
||||
|
||||
QMutex texture_cache_lock_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERCONTEXT_H
|
||||
@@ -0,0 +1,75 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 that overlap this one (code copied from TimeRangeList::remove)
|
||||
TimeRangeList::util_remove(&jobs_, range);
|
||||
|
||||
// Now append the job
|
||||
TimeRangeWithJob job(range, job_time);
|
||||
jobs_.push_back(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->get_job_time();
|
||||
}
|
||||
}
|
||||
|
||||
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->get_job_time() && it->overlaps_with(range)) {
|
||||
current_ranges.insert(it->intersected(range));
|
||||
}
|
||||
}
|
||||
|
||||
return current_ranges;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_RENDERJOBTRACKER_H
|
||||
#define OAK_RENDERJOBTRACKER_H
|
||||
|
||||
#include <olive/core/core.h>
|
||||
|
||||
#include "common/jobtime.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
using namespace core;
|
||||
|
||||
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 get_job_time() const
|
||||
{
|
||||
return job_time_;
|
||||
}
|
||||
void set_job_time(JobTime jt)
|
||||
{
|
||||
job_time_ = jt;
|
||||
}
|
||||
|
||||
private:
|
||||
JobTime job_time_;
|
||||
};
|
||||
|
||||
std::vector<TimeRangeWithJob> jobs_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERJOBTRACKER_H
|
||||
@@ -0,0 +1,407 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "rendermanager.h"
|
||||
|
||||
#include <QMatrix4x4>
|
||||
#include <QThread>
|
||||
|
||||
#include "config/config.h"
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
#include "render/backend/dynamicrenderer.h"
|
||||
#endif
|
||||
#include "render/opengl/openglrenderer.h"
|
||||
#include "renderprocessor.h"
|
||||
#include "renderworkerpool.h"
|
||||
#include "task/conform/conform.h"
|
||||
#include "task/taskmanager.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
RenderManager *RenderManager::instance_ = nullptr;
|
||||
const Rational RenderManager::k_dry_run_interval = Rational(10);
|
||||
|
||||
RenderManager::Backend RenderManager::backend_from_string(const QString &backend)
|
||||
{
|
||||
const QString lower = backend.toLower();
|
||||
if (lower == QStringLiteral("vulkan")) {
|
||||
return k_vulkan;
|
||||
}
|
||||
|
||||
if (lower == QStringLiteral("multiprocess")) {
|
||||
return k_multi_process;
|
||||
}
|
||||
|
||||
if (lower == QStringLiteral("dummy")) {
|
||||
return k_dummy;
|
||||
}
|
||||
|
||||
return k_open_gl;
|
||||
}
|
||||
|
||||
QString RenderManager::backend_to_string(Backend backend)
|
||||
{
|
||||
switch (backend) {
|
||||
case k_open_gl:
|
||||
return QStringLiteral("opengl");
|
||||
case k_vulkan:
|
||||
return QStringLiteral("vulkan");
|
||||
case k_multi_process:
|
||||
return QStringLiteral("multiprocess");
|
||||
case k_dummy:
|
||||
return QStringLiteral("dummy");
|
||||
}
|
||||
|
||||
return QStringLiteral("opengl");
|
||||
}
|
||||
|
||||
RenderManager::RenderManager(QObject *parent)
|
||||
: backend_(backend_from_string(OAK_CONFIG("GraphicsBackend").toString()))
|
||||
, requested_backend_(backend_)
|
||||
, aggressive_gc_(0)
|
||||
, worker_pool_(nullptr)
|
||||
{
|
||||
if (backend_ == k_vulkan) {
|
||||
#ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
qWarning()
|
||||
<< "Vulkan backend requested but dynamic render backend is not enabled. Falling back to OpenGL.";
|
||||
backend_ = kOpenGL;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (backend_ == k_open_gl || backend_ == k_vulkan) {
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
auto *dynamic_renderer =
|
||||
new DynamicRenderer(backend_to_string(requested_backend_));
|
||||
if (!dynamic_renderer->load()) {
|
||||
qWarning() << "Failed to load dynamic render backend"
|
||||
<< backend_to_string(requested_backend_)
|
||||
<< ", falling back to OpenGL";
|
||||
delete dynamic_renderer;
|
||||
backend_ = k_open_gl;
|
||||
context_ = new OpenGLRenderer();
|
||||
} else {
|
||||
context_ = dynamic_renderer;
|
||||
// DynamicRenderer may internally fall back (e.g. Vulkan -> OpenGL).
|
||||
// Synchronize RenderManager's view of the actual runtime backend.
|
||||
Backend actual_backend =
|
||||
backend_from_string(dynamic_renderer->backend_name());
|
||||
if (actual_backend != backend_) {
|
||||
qWarning() << "Dynamic render backend fell back from"
|
||||
<< backend_to_string(backend_) << "to"
|
||||
<< backend_to_string(actual_backend);
|
||||
backend_ = actual_backend;
|
||||
}
|
||||
}
|
||||
#else
|
||||
context_ = new OpenGLRenderer();
|
||||
#endif
|
||||
decoder_cache_ = new DecoderCache();
|
||||
shader_cache_ = new ShaderCache();
|
||||
} else {
|
||||
qCritical() << "Tried to initialize unknown graphics backend";
|
||||
context_ = nullptr;
|
||||
decoder_cache_ = nullptr;
|
||||
}
|
||||
|
||||
if (context_) {
|
||||
dry_run_thread_ = create_thread();
|
||||
audio_thread_ = create_thread();
|
||||
|
||||
waveform_threads_.resize(QThread::idealThreadCount());
|
||||
for (size_t i = 0; i < waveform_threads_.size(); i++) {
|
||||
waveform_threads_[i] = create_thread();
|
||||
}
|
||||
|
||||
auto_cacher_ = new PreviewAutoCacher(this);
|
||||
|
||||
worker_pool_ = new RenderWorkerPool(
|
||||
decoder_cache_, backend_to_string(requested_backend_), this);
|
||||
worker_pool_->start(QThread::NormalPriority);
|
||||
backend_ = k_multi_process;
|
||||
}
|
||||
|
||||
decoder_clear_timer_ = new QTimer(this);
|
||||
decoder_clear_timer_->setInterval(k_decoder_maximum_inactivity);
|
||||
connect(decoder_clear_timer_, &QTimer::timeout, this,
|
||||
&RenderManager::clear_old_decoders);
|
||||
decoder_clear_timer_->start();
|
||||
}
|
||||
|
||||
RenderManager::~RenderManager()
|
||||
{
|
||||
if (context_) {
|
||||
if (worker_pool_) {
|
||||
worker_pool_->shutdown();
|
||||
delete worker_pool_;
|
||||
worker_pool_ = nullptr;
|
||||
}
|
||||
|
||||
delete shader_cache_;
|
||||
delete decoder_cache_;
|
||||
|
||||
for (RenderThread *rt : render_threads_) {
|
||||
rt->quit();
|
||||
rt->wait();
|
||||
}
|
||||
|
||||
context_->post_destroy();
|
||||
delete context_;
|
||||
}
|
||||
}
|
||||
|
||||
RenderThread *RenderManager::create_thread(Renderer *renderer)
|
||||
{
|
||||
auto t = new RenderThread(renderer, decoder_cache_, shader_cache_, this);
|
||||
render_threads_.push_back(t);
|
||||
t->start(QThread::NormalPriority);
|
||||
return t;
|
||||
}
|
||||
|
||||
RenderTicketPtr RenderManager::render_frame(const RenderVideoParams ¶ms)
|
||||
{
|
||||
// Create ticket
|
||||
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
|
||||
|
||||
ticket->setProperty("node", QtUtils::ptr_to_value(params.node));
|
||||
ticket->setProperty("time", QVariant::fromValue(params.time));
|
||||
ticket->setProperty("size", params.force_size);
|
||||
ticket->setProperty("matrix", params.force_matrix);
|
||||
ticket->setProperty("format",
|
||||
static_cast<PixelFormat::Format>(params.force_format));
|
||||
ticket->setProperty("usecache", params.use_cache);
|
||||
ticket->setProperty("channelcount", params.force_channel_count);
|
||||
ticket->setProperty("mode", params.mode);
|
||||
ticket->setProperty("type", k_type_video);
|
||||
ticket->setProperty("colormanager",
|
||||
QtUtils::ptr_to_value(params.color_manager));
|
||||
ticket->setProperty("coloroutput",
|
||||
QVariant::fromValue(params.force_color_output));
|
||||
ticket->setProperty("colortransform",
|
||||
QVariant::fromValue(params.force_color_transform));
|
||||
Q_ASSERT(params.video_params.is_valid());
|
||||
ticket->setProperty("vparam", QVariant::fromValue(params.video_params));
|
||||
ticket->setProperty("aparam", QVariant::fromValue(params.audio_params));
|
||||
ticket->setProperty("return", params.return_type);
|
||||
ticket->setProperty("cache", params.cache_dir);
|
||||
ticket->setProperty("cachetimebase",
|
||||
QVariant::fromValue(params.cache_timebase));
|
||||
ticket->setProperty("cacheid", QVariant::fromValue(params.cache_id));
|
||||
ticket->setProperty("multicam", QtUtils::ptr_to_value(params.multicam));
|
||||
|
||||
// Video frames are always rendered by the worker pool. GPU textures cannot
|
||||
// be shared across the process boundary (or across independent Vulkan
|
||||
// instances), so texture-return requests are downgraded to CPU frames.
|
||||
RenderVideoParams worker_params = params;
|
||||
if (worker_params.return_type == ReturnType::k_texture) {
|
||||
worker_params.return_type = ReturnType::k_frame;
|
||||
}
|
||||
|
||||
if (worker_params.return_type == ReturnType::k_null) {
|
||||
if (dry_run_thread_) {
|
||||
dry_run_thread_->add_ticket(ticket);
|
||||
} else {
|
||||
// No render threads (e.g. dummy backend), finish without a result
|
||||
ticket->finish();
|
||||
}
|
||||
} else if (worker_pool_ &&
|
||||
worker_pool_->submit_frame(ticket, worker_params)) {
|
||||
return ticket;
|
||||
} else {
|
||||
qWarning()
|
||||
<< "RenderManager: worker pool unavailable, finishing ticket "
|
||||
"without result";
|
||||
ticket->finish();
|
||||
}
|
||||
|
||||
return ticket;
|
||||
}
|
||||
|
||||
RenderTicketPtr RenderManager::render_audio(const RenderAudioParams ¶ms)
|
||||
{
|
||||
// Create ticket
|
||||
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
|
||||
|
||||
ticket->setProperty("node", QtUtils::ptr_to_value(params.node));
|
||||
ticket->setProperty("time", QVariant::fromValue(params.range));
|
||||
ticket->setProperty("type", k_type_audio);
|
||||
ticket->setProperty("enablewaveforms", params.generate_waveforms);
|
||||
ticket->setProperty("clamp", params.clamp);
|
||||
ticket->setProperty("aparam", QVariant::fromValue(params.audio_params));
|
||||
ticket->setProperty("mode", params.mode);
|
||||
|
||||
if (params.generate_waveforms && !waveform_threads_.empty()) {
|
||||
size_t thread_index = last_waveform_thread_ % waveform_threads_.size();
|
||||
RenderThread *thread = waveform_threads_[thread_index];
|
||||
thread->add_ticket(ticket);
|
||||
last_waveform_thread_++;
|
||||
} else if (audio_thread_) {
|
||||
audio_thread_->add_ticket(ticket);
|
||||
} else {
|
||||
// No render threads (e.g. dummy backend), finish without a result
|
||||
ticket->finish();
|
||||
}
|
||||
|
||||
return ticket;
|
||||
}
|
||||
|
||||
bool RenderManager::remove_ticket(RenderTicketPtr ticket)
|
||||
{
|
||||
if (worker_pool_ && worker_pool_->remove_ticket(ticket)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (RenderThread *rt : render_threads_) {
|
||||
if (rt->remove_ticket(ticket)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void RenderManager::set_aggressive_garbage_collection(bool enabled)
|
||||
{
|
||||
aggressive_gc_ += enabled ? 1 : -1;
|
||||
|
||||
// Clamp at zero so unbalanced disable calls can't drive the counter negative
|
||||
if (aggressive_gc_ < 0) {
|
||||
aggressive_gc_ = 0;
|
||||
}
|
||||
|
||||
if (aggressive_gc_ > 0) {
|
||||
decoder_clear_timer_->setInterval(k_decoder_maximum_inactivity_aggressive);
|
||||
} else {
|
||||
decoder_clear_timer_->setInterval(k_decoder_maximum_inactivity);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderManager::clear_old_decoders()
|
||||
{
|
||||
if (!decoder_cache_) {
|
||||
// No decoder cache exists on backends without a renderer (e.g. dummy)
|
||||
return;
|
||||
}
|
||||
|
||||
QMutexLocker locker(decoder_cache_->mutex());
|
||||
|
||||
qint64 min_age =
|
||||
QDateTime::currentMSecsSinceEpoch() - k_decoder_maximum_inactivity;
|
||||
|
||||
for (auto it = decoder_cache_->begin(); it != decoder_cache_->end();) {
|
||||
DecoderPair decoder = it.value();
|
||||
|
||||
if (decoder.decoder->get_last_accessed_time() < min_age) {
|
||||
decoder.decoder->close();
|
||||
it = decoder_cache_->erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderThread::RenderThread(Renderer *renderer, DecoderCache *decoder_cache,
|
||||
ShaderCache *shader_cache, QObject *parent)
|
||||
: QThread(parent)
|
||||
, cancelled_(false)
|
||||
, context_(renderer)
|
||||
, decoder_cache_(decoder_cache)
|
||||
, shader_cache_(shader_cache)
|
||||
{
|
||||
if (context_) {
|
||||
context_->init();
|
||||
context_->moveToThread(this);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderThread::add_ticket(RenderTicketPtr ticket)
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
ticket->moveToThread(this);
|
||||
queue_.push_back(ticket);
|
||||
wait_.wakeOne();
|
||||
}
|
||||
|
||||
bool RenderThread::remove_ticket(RenderTicketPtr ticket)
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
|
||||
auto it = std::find(queue_.begin(), queue_.end(), ticket);
|
||||
if (it == queue_.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
queue_.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderThread::quit()
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
cancelled_ = true;
|
||||
wait_.wakeOne();
|
||||
}
|
||||
|
||||
void RenderThread::run()
|
||||
{
|
||||
if (context_) {
|
||||
context_->post_init();
|
||||
}
|
||||
|
||||
QMutexLocker locker(&mutex_);
|
||||
|
||||
while (!cancelled_) {
|
||||
if (queue_.empty()) {
|
||||
wait_.wait(&mutex_);
|
||||
}
|
||||
|
||||
if (cancelled_) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!queue_.empty()) {
|
||||
RenderTicketPtr ticket = queue_.front();
|
||||
queue_.pop_front();
|
||||
|
||||
locker.unlock();
|
||||
|
||||
// Setup the ticket for ::Process
|
||||
ticket->start();
|
||||
|
||||
if (ticket->is_cancelled()) {
|
||||
ticket->finish();
|
||||
} else {
|
||||
RenderProcessor::process(ticket, context_, decoder_cache_,
|
||||
shader_cache_);
|
||||
}
|
||||
|
||||
locker.relock();
|
||||
}
|
||||
}
|
||||
|
||||
if (context_) {
|
||||
context_->destroy();
|
||||
context_->moveToThread(this->thread());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_RENDERBACKEND_H
|
||||
#define OAK_RENDERBACKEND_H
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "colorprocessorcache.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "node/project.h"
|
||||
#include "node/traverser.h"
|
||||
#include "render/previewautocacher.h"
|
||||
#include "render/renderer.h"
|
||||
#include "render/colortransform.h"
|
||||
#include "render/renderticket.h"
|
||||
#include "rendercache.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class RenderThread : public QThread {
|
||||
Q_OBJECT
|
||||
public:
|
||||
RenderThread(Renderer *renderer, DecoderCache *decoder_cache,
|
||||
ShaderCache *shader_cache, QObject *parent = nullptr);
|
||||
|
||||
void add_ticket(RenderTicketPtr ticket);
|
||||
|
||||
bool remove_ticket(RenderTicketPtr ticket);
|
||||
|
||||
void quit();
|
||||
|
||||
protected:
|
||||
virtual void run() override;
|
||||
|
||||
private:
|
||||
QMutex mutex_;
|
||||
|
||||
QWaitCondition wait_;
|
||||
|
||||
std::list<RenderTicketPtr> queue_;
|
||||
|
||||
bool cancelled_;
|
||||
|
||||
Renderer *context_ = nullptr;
|
||||
|
||||
DecoderCache *decoder_cache_ = nullptr;
|
||||
|
||||
ShaderCache *shader_cache_ = nullptr;
|
||||
};
|
||||
|
||||
class RenderWorkerPool;
|
||||
|
||||
class RenderManager : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Backend {
|
||||
/// Graphics acceleration provided by OpenGL
|
||||
k_open_gl,
|
||||
|
||||
/// Vulkan requested by the user. Falls back to OpenGL until VulkanRenderer is implemented.
|
||||
k_vulkan,
|
||||
|
||||
/// Video frames are rendered by an external oak-render-worker process.
|
||||
k_multi_process,
|
||||
|
||||
/// No graphics rendering - used to test core threading logic
|
||||
k_dummy
|
||||
};
|
||||
|
||||
static void create_instance()
|
||||
{
|
||||
instance_ = new RenderManager();
|
||||
}
|
||||
|
||||
static void destroy_instance()
|
||||
{
|
||||
delete instance_;
|
||||
instance_ = nullptr;
|
||||
}
|
||||
|
||||
static RenderManager *instance()
|
||||
{
|
||||
return instance_;
|
||||
}
|
||||
|
||||
enum ReturnType { k_texture, k_frame, k_null };
|
||||
|
||||
struct RenderVideoParams {
|
||||
RenderVideoParams(Node *n, const VideoParams &vparam,
|
||||
const AudioParams &aparam, const Rational &t,
|
||||
ColorManager *colorman, RenderMode::Mode m)
|
||||
{
|
||||
node = n;
|
||||
video_params = vparam;
|
||||
audio_params = aparam;
|
||||
time = t;
|
||||
color_manager = colorman;
|
||||
use_cache = false;
|
||||
return_type = k_frame;
|
||||
force_format = PixelFormat::invalid;
|
||||
force_color_output = nullptr;
|
||||
force_color_transform = ColorTransform();
|
||||
force_size = QSize(0, 0);
|
||||
force_channel_count = 0;
|
||||
mode = m;
|
||||
multicam = nullptr;
|
||||
}
|
||||
|
||||
void add_cache(FrameHashCache *cache)
|
||||
{
|
||||
cache_dir = cache->get_cache_directory();
|
||||
cache_timebase = cache->get_timebase();
|
||||
cache_id = cache->get_uuid().toString();
|
||||
}
|
||||
|
||||
Node *node;
|
||||
VideoParams video_params;
|
||||
AudioParams audio_params;
|
||||
Rational time;
|
||||
ColorManager *color_manager;
|
||||
bool use_cache;
|
||||
ReturnType return_type;
|
||||
RenderMode::Mode mode;
|
||||
MultiCamNode *multicam;
|
||||
|
||||
QString cache_dir;
|
||||
Rational cache_timebase;
|
||||
QString cache_id;
|
||||
|
||||
QSize force_size;
|
||||
int force_channel_count;
|
||||
QMatrix4x4 force_matrix;
|
||||
PixelFormat force_format;
|
||||
ColorProcessorPtr force_color_output;
|
||||
ColorTransform force_color_transform;
|
||||
};
|
||||
|
||||
static const Rational k_dry_run_interval;
|
||||
|
||||
/**
|
||||
* @brief Asynchronously generate a frame at a given time
|
||||
*
|
||||
* The ticket from this function will return a FramePtr - the rendered frame in reference color
|
||||
* space.
|
||||
*
|
||||
* This function is thread-safe.
|
||||
*/
|
||||
RenderTicketPtr render_frame(const RenderVideoParams ¶ms);
|
||||
|
||||
struct RenderAudioParams {
|
||||
RenderAudioParams(Node *n, const TimeRange &time,
|
||||
const AudioParams &aparam, RenderMode::Mode m)
|
||||
{
|
||||
node = n;
|
||||
range = time;
|
||||
audio_params = aparam;
|
||||
generate_waveforms = false;
|
||||
clamp = true;
|
||||
mode = m;
|
||||
}
|
||||
|
||||
Node *node;
|
||||
TimeRange range;
|
||||
AudioParams audio_params;
|
||||
bool generate_waveforms;
|
||||
bool clamp;
|
||||
RenderMode::Mode mode;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Asynchronously generate a chunk of audio
|
||||
*
|
||||
* The ticket from this function will return a SampleBufferPtr - the rendered audio.
|
||||
*
|
||||
* This function is thread-safe.
|
||||
*/
|
||||
RenderTicketPtr render_audio(const RenderAudioParams ¶ms);
|
||||
|
||||
bool remove_ticket(RenderTicketPtr ticket);
|
||||
|
||||
enum TicketType { k_type_video, k_type_audio };
|
||||
|
||||
Backend backend() const
|
||||
{
|
||||
return backend_;
|
||||
}
|
||||
|
||||
Backend requested_backend() const
|
||||
{
|
||||
return requested_backend_;
|
||||
}
|
||||
|
||||
static Backend backend_from_string(const QString &backend);
|
||||
static QString backend_to_string(Backend backend);
|
||||
|
||||
PreviewAutoCacher *get_cacher() const
|
||||
{
|
||||
return auto_cacher_;
|
||||
}
|
||||
|
||||
void set_project(Project *p)
|
||||
{
|
||||
auto_cacher_->set_project(p);
|
||||
}
|
||||
|
||||
public slots:
|
||||
void set_aggressive_garbage_collection(bool enabled);
|
||||
|
||||
signals:
|
||||
|
||||
private:
|
||||
RenderManager(QObject *parent = nullptr);
|
||||
|
||||
virtual ~RenderManager() override;
|
||||
|
||||
RenderThread *create_thread(Renderer *renderer = nullptr);
|
||||
|
||||
static RenderManager *instance_;
|
||||
|
||||
Renderer *context_ = nullptr;
|
||||
|
||||
Backend backend_;
|
||||
Backend requested_backend_;
|
||||
|
||||
DecoderCache *decoder_cache_ = nullptr;
|
||||
|
||||
ShaderCache *shader_cache_ = nullptr;
|
||||
|
||||
static constexpr auto k_decoder_maximum_inactivity_aggressive = 1000;
|
||||
static constexpr auto k_decoder_maximum_inactivity = 5000;
|
||||
|
||||
int aggressive_gc_ = 0;
|
||||
|
||||
QTimer *decoder_clear_timer_ = nullptr;
|
||||
|
||||
RenderThread *dry_run_thread_ = nullptr;
|
||||
RenderThread *audio_thread_ = nullptr;
|
||||
|
||||
std::vector<RenderThread *> waveform_threads_;
|
||||
size_t last_waveform_thread_ = 0;
|
||||
|
||||
std::list<RenderThread *> render_threads_;
|
||||
|
||||
PreviewAutoCacher *auto_cacher_ = nullptr;
|
||||
|
||||
RenderWorkerPool *worker_pool_ = nullptr;
|
||||
|
||||
private slots:
|
||||
void clear_old_decoders();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(olive::RenderManager::TicketType)
|
||||
|
||||
#endif // OAK_RENDERBACKEND_H
|
||||
@@ -0,0 +1,52 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_RENDERMODE_H
|
||||
#define OAK_RENDERMODE_H
|
||||
|
||||
#include "common/define.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class RenderMode {
|
||||
public:
|
||||
/**
|
||||
* @brief The primary different "modes" the renderer can function in
|
||||
*/
|
||||
enum Mode {
|
||||
/**
|
||||
* This render is for realtime preview ONLY and does not need to be "perfect". Nodes can use lower-accuracy functions
|
||||
* to save performance when possible.
|
||||
*/
|
||||
k_offline,
|
||||
|
||||
/**
|
||||
* This render is some sort of export or master copy and Nodes should take time/bandwidth/system resources to produce
|
||||
* a higher accuracy version.
|
||||
*/
|
||||
k_online
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERMODE_H
|
||||
@@ -0,0 +1,892 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "renderprocessor.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QOpenGLContext>
|
||||
#include <QThreadStorage>
|
||||
#include <QVector2D>
|
||||
#include <QVector3D>
|
||||
#include <QVector4D>
|
||||
|
||||
#include "audio/audioprocessor.h"
|
||||
#include "node/block/clip/clip.h"
|
||||
#include "node/block/transition/transition.h"
|
||||
#include "node/project.h"
|
||||
#include "rendermanager.h"
|
||||
#include "render/plugin/pluginrenderer.h"
|
||||
#include "pluginSupport/oliveclip.h"
|
||||
#include "pluginSupport/olivehost.h"
|
||||
#include "render/ipc/frameslotpool.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
#define super NodeTraverser
|
||||
|
||||
RenderProcessor::RenderProcessor(RenderTicketPtr ticket, Renderer *render_ctx,
|
||||
DecoderCache *decoder_cache,
|
||||
ShaderCache *shader_cache)
|
||||
: ticket_(ticket)
|
||||
, render_ctx_(render_ctx)
|
||||
, decoder_cache_(decoder_cache)
|
||||
, shader_cache_(shader_cache)
|
||||
{
|
||||
}
|
||||
|
||||
TexturePtr RenderProcessor::generate_texture(const Rational &time,
|
||||
const Rational &frame_length)
|
||||
{
|
||||
TimeRange range = TimeRange(time, time + frame_length);
|
||||
|
||||
NodeValueTable table;
|
||||
if (Node *node = QtUtils::value_to_ptr<Node>(ticket_->property("node"))) {
|
||||
table = generate_table(node, range);
|
||||
}
|
||||
|
||||
NodeValue tex_val = table.get(NodeValue::k_texture);
|
||||
|
||||
resolve_jobs(tex_val);
|
||||
|
||||
return tex_val.to_texture();
|
||||
}
|
||||
|
||||
FramePtr RenderProcessor::generate_frame(TexturePtr texture,
|
||||
const Rational &time)
|
||||
{
|
||||
// Set up output frame parameters
|
||||
VideoParams frame_params = get_cache_video_params();
|
||||
|
||||
QSize frame_size = ticket_->property("size").value<QSize>();
|
||||
if (!frame_size.isNull()) {
|
||||
frame_params.set_width(frame_size.width());
|
||||
frame_params.set_height(frame_size.height());
|
||||
}
|
||||
|
||||
PixelFormat frame_format =
|
||||
static_cast<PixelFormat::Format>(ticket_->property("format").toInt());
|
||||
if (frame_format != PixelFormat::invalid) {
|
||||
frame_params.set_format(frame_format);
|
||||
}
|
||||
|
||||
int force_channel_count = ticket_->property("channelcount").toInt();
|
||||
if (force_channel_count != 0) {
|
||||
frame_params.set_channel_count(force_channel_count);
|
||||
} else {
|
||||
frame_params.set_channel_count(texture ?
|
||||
texture->channel_count() :
|
||||
VideoParams::k_rgba_channel_count);
|
||||
}
|
||||
|
||||
FramePtr frame = Frame::create();
|
||||
frame->set_timestamp(time);
|
||||
frame->set_video_params(frame_params);
|
||||
frame->allocate();
|
||||
|
||||
if (!texture) {
|
||||
// Blank frame out
|
||||
memset(frame->data(), 0, frame->allocated_size());
|
||||
} else {
|
||||
// Dump texture contents to frame
|
||||
ColorProcessorPtr output_color_transform =
|
||||
ticket_->property("coloroutput").value<ColorProcessorPtr>();
|
||||
const VideoParams &tex_params = texture->params();
|
||||
|
||||
if (output_color_transform) {
|
||||
TexturePtr transform_tex = render_ctx_->create_texture(tex_params);
|
||||
ColorTransformJob job;
|
||||
|
||||
job.set_color_processor(output_color_transform);
|
||||
job.set_input_texture(texture);
|
||||
job.set_input_alpha_association(
|
||||
OAK_CONFIG("ReassocLinToNonLin").toBool() ? k_alpha_associated :
|
||||
k_alpha_none);
|
||||
|
||||
render_ctx_->blit_color_managed(job, transform_tex.get());
|
||||
|
||||
texture = transform_tex;
|
||||
}
|
||||
|
||||
if (tex_params.effective_width() != frame_params.effective_width() ||
|
||||
tex_params.effective_height() != frame_params.effective_height() ||
|
||||
tex_params.format() != frame_params.format()) {
|
||||
TexturePtr blit_tex = render_ctx_->create_texture(frame_params);
|
||||
|
||||
QMatrix4x4 matrix = ticket_->property("matrix").value<QMatrix4x4>();
|
||||
|
||||
// No color transform, just blit
|
||||
ShaderJob job;
|
||||
job.insert(QStringLiteral("ove_maintex"),
|
||||
NodeValue(NodeValue::k_texture,
|
||||
QVariant::fromValue(texture)));
|
||||
job.insert(QStringLiteral("ove_mvpmat"),
|
||||
NodeValue(NodeValue::k_matrix, matrix));
|
||||
|
||||
render_ctx_->blit_to_texture(render_ctx_->get_default_shader(), job,
|
||||
blit_tex.get());
|
||||
|
||||
// Replace texture that we're going to download in the next step
|
||||
texture = blit_tex;
|
||||
}
|
||||
|
||||
render_ctx_->download_from_texture(texture->id(), texture->params(),
|
||||
frame->data(),
|
||||
frame->linesize_pixels());
|
||||
if (output_color_transform) {
|
||||
VideoParams display_params = frame->video_params();
|
||||
display_params.set_colorspace(
|
||||
QStringLiteral("display:") +
|
||||
QString::fromUtf8(output_color_transform->id()));
|
||||
frame->set_video_params(display_params);
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
void RenderProcessor::run()
|
||||
{
|
||||
// Depending on the render ticket type, start a job
|
||||
RenderManager::TicketType type =
|
||||
ticket_->property("type").value<RenderManager::TicketType>();
|
||||
|
||||
set_cancel_pointer(ticket_->get_cancel_atom());
|
||||
|
||||
VideoParams params = ticket_->property("vparam").value<VideoParams>();
|
||||
params.set_format(PixelFormat::f32);
|
||||
set_cache_video_params(params);
|
||||
set_cache_audio_params(ticket_->property("aparam").value<AudioParams>());
|
||||
|
||||
if (is_cancelled()) {
|
||||
ticket_->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// if is a plugin
|
||||
/*Node *node=ticket_->property("node").value<Node*>();
|
||||
if (node && node->getPlugin()) {
|
||||
std::shared_ptr<OFX::Host::ImageEffect::ImageEffectPlugin> plugin
|
||||
= node->getPlugin();
|
||||
std::unique_ptr<OFX::Host::ImageEffect::Instance> instance(plugin->createInstance(kOfxImageEffectContextFilter, NULL));
|
||||
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
switch (type) {
|
||||
case RenderManager::k_type_video: {
|
||||
Rational time = ticket_->property("time").value<Rational>();
|
||||
|
||||
Rational frame_length = get_cache_video_params().frame_rate_as_time_base();
|
||||
if (get_cache_video_params().interlacing() !=
|
||||
VideoParams::k_interlace_none) {
|
||||
frame_length /= 2;
|
||||
}
|
||||
|
||||
TexturePtr texture = generate_texture(time, frame_length);
|
||||
|
||||
if (!render_ctx_) {
|
||||
ticket_->finish();
|
||||
} else {
|
||||
if (get_cache_video_params().interlacing() !=
|
||||
VideoParams::k_interlace_none) {
|
||||
// Get next between frame and interlace it
|
||||
TexturePtr top = texture;
|
||||
TexturePtr bottom =
|
||||
generate_texture(time + frame_length, frame_length);
|
||||
|
||||
if (get_cache_video_params().interlacing() ==
|
||||
VideoParams::k_interlaced_bottom_first) {
|
||||
std::swap(top, bottom);
|
||||
}
|
||||
|
||||
texture = render_ctx_->interlace_texture(top, bottom,
|
||||
get_cache_video_params());
|
||||
}
|
||||
|
||||
if (heard_cancel()) {
|
||||
// Finish cancelled ticket with nothing since we can't guarantee the frame we generated
|
||||
// is actually "complete
|
||||
ticket_->finish();
|
||||
} else {
|
||||
FramePtr frame;
|
||||
QString cache = ticket_->property("cache").toString();
|
||||
RenderManager::ReturnType return_type =
|
||||
RenderManager::ReturnType(
|
||||
ticket_->property("return").toInt());
|
||||
|
||||
if (return_type == RenderManager::k_frame || !cache.isEmpty()) {
|
||||
// Convert to CPU frame
|
||||
frame = generate_frame(texture, time);
|
||||
|
||||
// Save to cache if requested
|
||||
if (!cache.isEmpty()) {
|
||||
Rational timebase =
|
||||
ticket_->property("cachetimebase").value<Rational>();
|
||||
QUuid uuid =
|
||||
ticket_->property("cacheid").value<QUuid>();
|
||||
bool cache_result = FrameHashCache::save_cache_frame(
|
||||
cache, uuid, time, timebase, frame);
|
||||
ticket_->setProperty("cached", cache_result);
|
||||
}
|
||||
}
|
||||
|
||||
if (return_type == RenderManager::k_texture) {
|
||||
// Return GPU texture
|
||||
if (!texture) {
|
||||
texture =
|
||||
render_ctx_->create_texture(get_cache_video_params());
|
||||
render_ctx_->clear_destination(texture.get());
|
||||
}
|
||||
|
||||
render_ctx_->flush();
|
||||
ticket_->finish(QVariant::fromValue(texture));
|
||||
} else {
|
||||
ticket_->finish(QVariant::fromValue(frame));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RenderManager::k_type_audio: {
|
||||
TimeRange time = ticket_->property("time").value<TimeRange>();
|
||||
|
||||
NodeValueTable table;
|
||||
if (Node *node = QtUtils::value_to_ptr<Node>(ticket_->property("node"))) {
|
||||
table = generate_table(node, time);
|
||||
}
|
||||
|
||||
NodeValue sample_val = table.get(NodeValue::k_samples);
|
||||
|
||||
resolve_jobs(sample_val);
|
||||
|
||||
SampleBuffer samples = sample_val.to_samples();
|
||||
if (samples.is_allocated()) {
|
||||
if (ticket_->property("clamp").toBool() && !is_cancelled()) {
|
||||
samples.clamp();
|
||||
}
|
||||
|
||||
if (ticket_->property("enablewaveforms").toBool() &&
|
||||
!is_cancelled()) {
|
||||
AudioVisualWaveform vis;
|
||||
vis.set_channel_count(samples.audio_params().channel_count());
|
||||
vis.overwrite_samples(samples,
|
||||
samples.audio_params().sample_rate());
|
||||
ticket_->setProperty("waveform", QVariant::fromValue(vis));
|
||||
}
|
||||
}
|
||||
|
||||
if (heard_cancel()) {
|
||||
ticket_->finish();
|
||||
} else {
|
||||
ticket_->finish(QVariant::fromValue(samples));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Fail
|
||||
ticket_->finish();
|
||||
}
|
||||
}
|
||||
|
||||
DecoderPtr
|
||||
RenderProcessor::resolve_decoder_from_input(const QString &decoder_id,
|
||||
const Decoder::CodecStream &stream)
|
||||
{
|
||||
if (!stream.is_valid()) {
|
||||
qWarning() << "Attempted to resolve the decoder of a null stream";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!decoder_cache_) {
|
||||
qWarning() << "Cannot resolve decoder for" << stream.filename()
|
||||
<< "without a decoder cache";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QMutexLocker locker(decoder_cache_->mutex());
|
||||
|
||||
DecoderPair decoder = decoder_cache_->value(stream);
|
||||
|
||||
qint64 file_last_modified =
|
||||
QFileInfo(stream.filename()).lastModified().toMSecsSinceEpoch();
|
||||
|
||||
DecoderPtr dec = nullptr;
|
||||
|
||||
if (decoder.decoder && decoder.last_modified == file_last_modified) {
|
||||
dec = decoder.decoder;
|
||||
} else {
|
||||
// No decoder
|
||||
decoder.decoder = dec = Decoder::create_from_id(decoder_id);
|
||||
decoder.last_modified = file_last_modified;
|
||||
decoder_cache_->insert(stream, decoder);
|
||||
locker.unlock();
|
||||
|
||||
if (!dec->open(stream)) {
|
||||
qWarning() << "Failed to open decoder for" << stream.filename()
|
||||
<< "::" << stream.stream();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!render_ctx_) {
|
||||
// Assume dry run and increment access time
|
||||
decoder.decoder->increment_access_time(
|
||||
RenderManager::k_dry_run_interval.to_double() * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
return dec;
|
||||
}
|
||||
|
||||
NodeValueDatabase RenderProcessor::generate_database(const Node *node,
|
||||
const TimeRange &range)
|
||||
{
|
||||
NodeValueDatabase db = super::generate_database(node, range);
|
||||
|
||||
if (const MultiCamNode *multicam =
|
||||
dynamic_cast<const MultiCamNode *>(node)) {
|
||||
if (QtUtils::value_to_ptr<MultiCamNode>(ticket_->property("multicam")) ==
|
||||
multicam) {
|
||||
int sz = multicam->get_source_count();
|
||||
QVector<TexturePtr> multicam_tex(sz);
|
||||
for (int i = 0; i < sz; i++) {
|
||||
NodeValueTable t =
|
||||
generate_table(multicam->get_connected_render_output(
|
||||
multicam->k_sources_input, i),
|
||||
range, multicam);
|
||||
NodeValue val = generate_row_value_element(
|
||||
multicam, multicam->k_sources_input, i, &t, range);
|
||||
resolve_jobs(val);
|
||||
|
||||
multicam_tex[i] = val.to_texture();
|
||||
}
|
||||
ticket_->setProperty("multicam_output",
|
||||
QVariant::fromValue(multicam_tex));
|
||||
}
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
void RenderProcessor::process(RenderTicketPtr ticket, Renderer *render_ctx,
|
||||
DecoderCache *decoder_cache,
|
||||
ShaderCache *shader_cache)
|
||||
{
|
||||
RenderProcessor p(ticket, render_ctx, decoder_cache, shader_cache);
|
||||
p.run();
|
||||
}
|
||||
|
||||
void RenderProcessor::process_video_footage(TexturePtr destination,
|
||||
const FootageJob *stream,
|
||||
const Rational &input_time)
|
||||
{
|
||||
if (ticket_->property("type").value<RenderManager::TicketType>() !=
|
||||
RenderManager::k_type_video) {
|
||||
// Video cannot contribute to audio, so we do nothing here
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the still frame cache. On large frames such as high resolution still images, uploading
|
||||
// and color managing them for every frame is a waste of time, so we implement a small cache here
|
||||
// to optimize such a situation
|
||||
VideoParams stream_data = stream->video_params();
|
||||
|
||||
ColorManager *color_manager =
|
||||
QtUtils::value_to_ptr<ColorManager>(ticket_->property("colormanager"));
|
||||
|
||||
QString using_colorspace = stream_data.colorspace();
|
||||
|
||||
if (using_colorspace.isEmpty() && color_manager) {
|
||||
using_colorspace = color_manager->get_default_input_color_space();
|
||||
}
|
||||
|
||||
if (using_colorspace.isEmpty()) {
|
||||
qWarning()
|
||||
<< "RenderProcessor ProcessVideoFootage: no input colorspace available";
|
||||
}
|
||||
|
||||
auto blit_color_managed = [&](const TexturePtr &unmanaged_texture,
|
||||
const VideoParams &texture_params) {
|
||||
if (!render_ctx_ || !unmanaged_texture || is_cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We convert to our rendering pixel format, since that will always be float-based which
|
||||
// is necessary for correct color conversion
|
||||
ColorProcessorPtr processor =
|
||||
ColorProcessor::create(color_manager, using_colorspace,
|
||||
color_manager->get_reference_color_space());
|
||||
|
||||
ColorTransformJob job;
|
||||
job.set_color_processor(processor);
|
||||
job.set_input_texture(unmanaged_texture);
|
||||
|
||||
if (texture_params.channel_count() != VideoParams::k_rgba_channel_count ||
|
||||
texture_params.colorspace() ==
|
||||
color_manager->get_reference_color_space()) {
|
||||
job.set_input_alpha_association(k_alpha_none);
|
||||
} else if (texture_params.premultiplied_alpha()) {
|
||||
job.set_input_alpha_association(k_alpha_associated);
|
||||
} else {
|
||||
job.set_input_alpha_association(k_alpha_unassociated);
|
||||
}
|
||||
|
||||
render_ctx_->blit_color_managed(job, destination.get());
|
||||
// macOS TBDR: ensure tile writeback completes before the texture
|
||||
// is read back in a potentially different shared OpenGL context.
|
||||
render_ctx_->flush();
|
||||
};
|
||||
|
||||
auto *input_pool = QtUtils::value_to_ptr<ipc::FrameSlotPool>(
|
||||
ticket_->property("ipc_input_pool"));
|
||||
int input_slot = -1;
|
||||
const QVariantList input_slots =
|
||||
ticket_->property("ipc_input_slots").toList();
|
||||
if (!input_slots.isEmpty()) {
|
||||
const QVariant cursor_value =
|
||||
ticket_->property("ipc_input_slot_cursor");
|
||||
const int cursor = cursor_value.isValid() ? cursor_value.toInt() : 0;
|
||||
if (cursor >= 0 && cursor < input_slots.size()) {
|
||||
input_slot = input_slots.at(cursor).toInt();
|
||||
ticket_->setProperty("ipc_input_slot_cursor", cursor + 1);
|
||||
}
|
||||
} else {
|
||||
const QVariant input_slot_value = ticket_->property("ipc_input_slot");
|
||||
input_slot = input_slot_value.isValid() ? input_slot_value.toInt() : -1;
|
||||
}
|
||||
if (render_ctx_ && input_pool && input_slot >= 0) {
|
||||
if (input_slot >= int(input_pool->slot_count())) {
|
||||
qWarning()
|
||||
<< "RenderProcessor received out-of-range IPC input frame slot"
|
||||
<< input_slot;
|
||||
return;
|
||||
}
|
||||
|
||||
const ipc::FrameSlotMeta *meta = input_pool->meta(uint32_t(input_slot));
|
||||
if (meta && meta->width > 0 && meta->height > 0 &&
|
||||
meta->data_size > 0 &&
|
||||
meta->data_size <= int(input_pool->slot_data_bytes())) {
|
||||
VideoParams input_params = stream_data;
|
||||
input_params.set_width(meta->width);
|
||||
input_params.set_height(meta->height);
|
||||
input_params.set_format(PixelFormat::Format(meta->format));
|
||||
input_params.set_channel_count(meta->channel_count);
|
||||
// The decoder may leave depth at 0 for 2D frames, but the renderer
|
||||
// needs depth >= 1 to compute image size and upload the texture.
|
||||
if (input_params.depth() <= 0) {
|
||||
input_params.set_depth(1);
|
||||
}
|
||||
|
||||
// Prefer the colorspace that the main process used when decoding this
|
||||
// frame. The FootageJob reconstructed in the worker may have stale or
|
||||
// empty colorspace if the project snapshot was saved before stream
|
||||
// metadata was fully resolved.
|
||||
const QString ipc_colorspace = QString::fromUtf8(meta->colorspace);
|
||||
if (!ipc_colorspace.isEmpty()) {
|
||||
input_params.set_colorspace(ipc_colorspace);
|
||||
using_colorspace = ipc_colorspace;
|
||||
}
|
||||
|
||||
const int bytes_per_pixel = input_params.get_bytes_per_pixel();
|
||||
const int linesize_pixels = bytes_per_pixel > 0 ?
|
||||
meta->linesize / bytes_per_pixel :
|
||||
input_params.effective_width();
|
||||
|
||||
const void *slot_data = input_pool->slot_data(uint32_t(input_slot));
|
||||
TexturePtr unmanaged_texture = render_ctx_->create_texture(
|
||||
input_params, slot_data, linesize_pixels);
|
||||
|
||||
blit_color_managed(unmanaged_texture, input_params);
|
||||
return;
|
||||
}
|
||||
qWarning() << "RenderProcessor received invalid IPC input frame slot"
|
||||
<< input_slot;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!decoder_cache_) {
|
||||
qWarning()
|
||||
<< "RenderProcessor has no decoder cache or IPC input frame for"
|
||||
<< stream->filename();
|
||||
return;
|
||||
}
|
||||
|
||||
const bool use_proxy = stream->should_use_proxy(
|
||||
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()));
|
||||
const QString decode_filename = use_proxy ? stream->proxy_filename() :
|
||||
stream->filename();
|
||||
const QString decoder_id = use_proxy ? stream->proxy_decoder() :
|
||||
stream->decoder();
|
||||
const int stream_index = use_proxy ? stream->proxy_stream_index() :
|
||||
stream_data.stream_index();
|
||||
|
||||
Decoder::CodecStream default_codec_stream(decode_filename, stream_index,
|
||||
get_current_block());
|
||||
|
||||
DecoderPtr decoder = nullptr;
|
||||
|
||||
switch (stream_data.video_type()) {
|
||||
case VideoParams::k_video_type_video:
|
||||
case VideoParams::k_video_type_still:
|
||||
decoder = resolve_decoder_from_input(decoder_id, default_codec_stream);
|
||||
break;
|
||||
case VideoParams::k_video_type_image_sequence: {
|
||||
if (render_ctx_) {
|
||||
// Since image sequences involve multiple files, we don't engage the decoder cache
|
||||
decoder = Decoder::create_from_id(decoder_id);
|
||||
|
||||
QString frame_filename;
|
||||
|
||||
int64_t frame_number =
|
||||
stream_data.get_time_in_timebase_units(input_time);
|
||||
frame_filename = Decoder::transform_image_sequence_file_name(
|
||||
decode_filename, frame_number);
|
||||
|
||||
// Decoder will close automatically since it's a stream_ptr
|
||||
decoder->open(Decoder::CodecStream(frame_filename, stream_index,
|
||||
get_current_block()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (decoder && render_ctx_) {
|
||||
Decoder::RetrieveVideoParams p;
|
||||
p.divider = stream->video_params().divider();
|
||||
p.maximum_format = destination->format();
|
||||
|
||||
if (!is_cancelled()) {
|
||||
VideoParams tex_params = stream->video_params();
|
||||
|
||||
if (tex_params.is_valid()) {
|
||||
TexturePtr unmanaged_texture;
|
||||
|
||||
p.renderer = render_ctx_;
|
||||
p.time =
|
||||
(stream_data.video_type() == VideoParams::k_video_type_video) ?
|
||||
input_time :
|
||||
Decoder::k_any_timecode;
|
||||
p.cancelled = get_cancel_pointer();
|
||||
p.force_range = stream_data.color_range();
|
||||
p.src_interlacing = stream_data.interlacing();
|
||||
|
||||
unmanaged_texture = decoder->retrieve_video(p);
|
||||
|
||||
if (!is_cancelled() && unmanaged_texture) {
|
||||
blit_color_managed(unmanaged_texture, stream_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RenderProcessor::process_audio_footage(SampleBuffer &destination,
|
||||
const FootageJob *stream,
|
||||
const TimeRange &input_time)
|
||||
{
|
||||
// The worker process has no decoder cache and does not decode audio. Bail
|
||||
// out gracefully rather than letting ResolveDecoderFromInput crash.
|
||||
if (!decoder_cache_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mirror the video path: use the proxy (when enabled, ready, and containing
|
||||
// audio) for offline renders only, never for export
|
||||
const bool use_proxy = stream->should_use_proxy(
|
||||
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()));
|
||||
const QString decode_filename = use_proxy ? stream->proxy_filename() :
|
||||
stream->filename();
|
||||
const QString decoder_id = use_proxy ? stream->proxy_decoder() :
|
||||
stream->decoder();
|
||||
const int stream_index = use_proxy ?
|
||||
stream->proxy_stream_index() :
|
||||
stream->audio_params().stream_index();
|
||||
|
||||
DecoderPtr decoder = resolve_decoder_from_input(
|
||||
decoder_id,
|
||||
Decoder::CodecStream(decode_filename, stream_index, nullptr));
|
||||
|
||||
if (decoder) {
|
||||
const AudioParams &audio_params = get_cache_audio_params();
|
||||
|
||||
Decoder::RetrieveAudioStatus status = decoder->retrieve_audio(
|
||||
destination, input_time, audio_params, stream->cache_path(),
|
||||
loop_mode(),
|
||||
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()));
|
||||
|
||||
if (status == Decoder::k_waiting_for_conform) {
|
||||
ticket_->setProperty("incomplete", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RenderProcessor::process_shader(TexturePtr destination, const Node *node,
|
||||
const ShaderJob *job)
|
||||
{
|
||||
if (!render_ctx_) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString full_shader_id =
|
||||
QStringLiteral("%1:%2").arg(node->id(), job->get_shader_id());
|
||||
|
||||
QMutexLocker locker(shader_cache_->mutex());
|
||||
|
||||
QVariant shader = shader_cache_->value(full_shader_id);
|
||||
|
||||
if (shader.isNull()) {
|
||||
// Since we have shader code, compile it now
|
||||
shader = render_ctx_->create_native_shader(
|
||||
node->get_shader_code(job->get_shader_id()));
|
||||
|
||||
if (shader.isNull()) {
|
||||
// Couldn't find or build the shader required
|
||||
return;
|
||||
}
|
||||
|
||||
shader_cache_->insert(full_shader_id, shader);
|
||||
}
|
||||
|
||||
locker.unlock();
|
||||
|
||||
// Run shader
|
||||
render_ctx_->blit_to_texture(shader, const_cast<ShaderJob &>(*job),
|
||||
destination.get());
|
||||
}
|
||||
|
||||
void RenderProcessor::process_samples(SampleBuffer &destination,
|
||||
const Node *node, const TimeRange &range,
|
||||
const SampleJob &job)
|
||||
{
|
||||
if (!job.samples().is_allocated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
NodeValueRow value_db;
|
||||
|
||||
const AudioParams &audio_params = get_cache_audio_params();
|
||||
|
||||
for (size_t i = 0; i < job.samples().sample_count(); i++) {
|
||||
// Calculate the exact Rational time at this sample
|
||||
double sample_to_second =
|
||||
static_cast<double>(i) /
|
||||
static_cast<double>(audio_params.sample_rate());
|
||||
|
||||
Rational this_sample_time =
|
||||
Rational::from_double(range.in().to_double() + sample_to_second);
|
||||
|
||||
// Update all non-sample and non-footage inputs
|
||||
for (auto j = job.get_values().constBegin();
|
||||
j != job.get_values().constEnd(); j++) {
|
||||
TimeRange r = TimeRange(this_sample_time, this_sample_time);
|
||||
NodeValueTable value = process_input(node, j.key(), r);
|
||||
|
||||
value_db.insert(j.key(),
|
||||
generate_row_value(node, j.key(), &value, r));
|
||||
}
|
||||
|
||||
node->process_samples(value_db, job.samples(), destination, i);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderProcessor::process_color_transform(TexturePtr destination,
|
||||
const Node *node,
|
||||
const ColorTransformJob *job)
|
||||
{
|
||||
if (!render_ctx_) {
|
||||
return;
|
||||
}
|
||||
|
||||
render_ctx_->blit_color_managed(*job, destination.get());
|
||||
}
|
||||
|
||||
void RenderProcessor::process_frame_generation(TexturePtr destination,
|
||||
const Node *node,
|
||||
const GenerateJob *job)
|
||||
{
|
||||
if (!render_ctx_) {
|
||||
return;
|
||||
}
|
||||
|
||||
FramePtr frame = Frame::create();
|
||||
|
||||
frame->set_video_params(destination->params());
|
||||
frame->allocate();
|
||||
|
||||
node->generate_frame(frame, *job);
|
||||
|
||||
destination->upload(frame->data(), frame->linesize_pixels());
|
||||
}
|
||||
|
||||
TexturePtr RenderProcessor::process_plugin_job(TexturePtr texture,
|
||||
TexturePtr destination,
|
||||
const Node *node)
|
||||
{
|
||||
(void)node;
|
||||
|
||||
if (!render_ctx_ || !texture || !destination) {
|
||||
return destination;
|
||||
}
|
||||
|
||||
auto *plugin_job = dynamic_cast<plugin::PluginJob *>(texture->job());
|
||||
if (!plugin_job) {
|
||||
return destination;
|
||||
}
|
||||
|
||||
plugin::PluginRenderer plugin_renderer(render_ctx_);
|
||||
if (!plugin_renderer.renderer()) {
|
||||
return destination;
|
||||
}
|
||||
|
||||
NodeValueRow &values = plugin_job->get_values();
|
||||
|
||||
auto is_usable_texture = [](const TexturePtr &tex) {
|
||||
if (!tex) {
|
||||
return false;
|
||||
}
|
||||
if (!tex->is_dummy() && tex->renderer()) {
|
||||
return true;
|
||||
}
|
||||
AVFramePtr frame = tex->frame();
|
||||
return frame && frame->data(0);
|
||||
};
|
||||
|
||||
TexturePtr src = nullptr;
|
||||
QString effect_input_id;
|
||||
if (plugin_job->node()) {
|
||||
effect_input_id = plugin_job->node()->get_effect_input_id();
|
||||
}
|
||||
if (!effect_input_id.isEmpty()) {
|
||||
if (TexturePtr effect_tex = values.value(effect_input_id).to_texture();
|
||||
is_usable_texture(effect_tex)) {
|
||||
src = effect_tex;
|
||||
}
|
||||
}
|
||||
if (!src) {
|
||||
const QString source_key =
|
||||
QString::fromUtf8(kOfxImageEffectSimpleSourceClipName);
|
||||
if (TexturePtr source_tex = values.value(source_key).to_texture();
|
||||
is_usable_texture(source_tex)) {
|
||||
src = source_tex;
|
||||
} else if (TexturePtr effect_tex =
|
||||
values.value(plugin::k_texture_input).to_texture();
|
||||
is_usable_texture(effect_tex)) {
|
||||
src = effect_tex;
|
||||
}
|
||||
}
|
||||
if (!src) {
|
||||
for (auto it = values.cbegin(); it != values.cend(); ++it) {
|
||||
if (it.value().type() == NodeValue::k_texture) {
|
||||
if (TexturePtr any_tex = it.value().to_texture();
|
||||
is_usable_texture(any_tex)) {
|
||||
src = any_tex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugin_renderer.render_plugin(src, *plugin_job, destination,
|
||||
destination->params(), true, false);
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
TexturePtr RenderProcessor::process_video_cache_job(const CacheJob *val)
|
||||
{
|
||||
FramePtr frame = FrameHashCache::load_cache_frame(val->get_filename());
|
||||
if (frame) {
|
||||
// Auto-detect and discard black/empty cached frames (macOS TBDR artifact)
|
||||
bool all_black = true;
|
||||
if (frame->data() && frame->allocated_size() > 0) {
|
||||
const uint8_t *pixels =
|
||||
reinterpret_cast<const uint8_t *>(frame->data());
|
||||
size_t alloc_size = static_cast<size_t>(frame->allocated_size());
|
||||
size_t check_bytes = std::min(alloc_size, size_t(4096));
|
||||
for (size_t i = 0; i < check_bytes; ++i) {
|
||||
if (pixels[i] != 0) {
|
||||
all_black = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (all_black) {
|
||||
qWarning() << "[CACHE] Discarding black cached frame:"
|
||||
<< val->get_filename()
|
||||
<< "time=" << frame->timestamp().to_double()
|
||||
<< "size=" << frame->allocated_size();
|
||||
QFile::remove(val->get_filename());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TexturePtr tex = create_texture(frame->video_params());
|
||||
if (tex) {
|
||||
tex->upload(frame->data(), frame->linesize_pixels());
|
||||
return tex;
|
||||
}
|
||||
} else {
|
||||
QStringList s = ticket_->property("badcache").toStringList();
|
||||
s.append(val->get_filename());
|
||||
ticket_->setProperty("badcache", s);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TexturePtr RenderProcessor::create_texture(const VideoParams &p)
|
||||
{
|
||||
if (render_ctx_) {
|
||||
return render_ctx_->create_texture(p);
|
||||
} else {
|
||||
return super::create_texture(p);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderProcessor::convert_to_reference_space(TexturePtr destination,
|
||||
TexturePtr source,
|
||||
const QString &input_cs)
|
||||
{
|
||||
if (!render_ctx_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ColorManager *color_manager =
|
||||
QtUtils::value_to_ptr<ColorManager>(ticket_->property("colormanager"));
|
||||
ColorProcessorPtr cp = ColorProcessor::create(
|
||||
color_manager, input_cs, color_manager->get_reference_color_space());
|
||||
|
||||
ColorTransformJob ctj;
|
||||
|
||||
ctj.set_color_processor(cp);
|
||||
ctj.set_input_texture(source);
|
||||
ctj.set_input_alpha_association(k_alpha_associated);
|
||||
|
||||
render_ctx_->blit_color_managed(ctj, destination.get());
|
||||
}
|
||||
|
||||
bool RenderProcessor::use_cache() const
|
||||
{
|
||||
return static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()) ==
|
||||
RenderMode::k_offline;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_RENDERPROCESSOR_H
|
||||
#define OAK_RENDERPROCESSOR_H
|
||||
|
||||
#include "node/block/clip/clip.h"
|
||||
#include <memory>
|
||||
#include "node/traverser.h"
|
||||
#include "render/renderer.h"
|
||||
#include "rendercache.h"
|
||||
#include "renderticket.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
namespace plugin
|
||||
{
|
||||
class PluginRenderer;
|
||||
}
|
||||
|
||||
class RenderProcessor : public NodeTraverser {
|
||||
public:
|
||||
virtual NodeValueDatabase generate_database(const Node *node,
|
||||
const TimeRange &range) override;
|
||||
|
||||
static void process(RenderTicketPtr ticket, Renderer *render_ctx,
|
||||
DecoderCache *decoder_cache, ShaderCache *shader_cache);
|
||||
|
||||
struct RenderedWaveform {
|
||||
const ClipBlock *block;
|
||||
AudioVisualWaveform waveform;
|
||||
TimeRange range;
|
||||
bool silence;
|
||||
};
|
||||
|
||||
protected:
|
||||
virtual void process_video_footage(TexturePtr destination,
|
||||
const FootageJob *stream,
|
||||
const Rational &input_time) override;
|
||||
|
||||
virtual void process_audio_footage(SampleBuffer &destination,
|
||||
const FootageJob *stream,
|
||||
const TimeRange &input_time) override;
|
||||
|
||||
virtual void process_shader(TexturePtr destination, const Node *node,
|
||||
const ShaderJob *job) override;
|
||||
|
||||
virtual void process_samples(SampleBuffer &destination, const Node *node,
|
||||
const TimeRange &range,
|
||||
const SampleJob &job) override;
|
||||
|
||||
virtual void process_color_transform(TexturePtr destination, const Node *node,
|
||||
const ColorTransformJob *job) override;
|
||||
|
||||
virtual void process_frame_generation(TexturePtr destination,
|
||||
const Node *node,
|
||||
const GenerateJob *job) override;
|
||||
|
||||
virtual TexturePtr process_plugin_job(TexturePtr texture,
|
||||
TexturePtr destination,
|
||||
const Node *node) override;
|
||||
|
||||
virtual TexturePtr process_video_cache_job(const CacheJob *val) override;
|
||||
|
||||
virtual TexturePtr create_texture(const VideoParams &p) override;
|
||||
|
||||
virtual SampleBuffer create_sample_buffer(const AudioParams ¶ms,
|
||||
int sample_count) override
|
||||
{
|
||||
return SampleBuffer(params, sample_count);
|
||||
}
|
||||
|
||||
virtual void convert_to_reference_space(TexturePtr destination,
|
||||
TexturePtr source,
|
||||
const QString &input_cs) override;
|
||||
|
||||
virtual bool use_cache() const override;
|
||||
|
||||
private:
|
||||
RenderProcessor(RenderTicketPtr ticket, Renderer *render_ctx,
|
||||
DecoderCache *decoder_cache, ShaderCache *shader_cache);
|
||||
|
||||
TexturePtr generate_texture(const Rational &time,
|
||||
const Rational &frame_length);
|
||||
|
||||
FramePtr generate_frame(TexturePtr texture, const Rational &time);
|
||||
|
||||
void run();
|
||||
|
||||
DecoderPtr resolve_decoder_from_input(const QString &decoder_id,
|
||||
const Decoder::CodecStream &stream);
|
||||
|
||||
RenderTicketPtr ticket_;
|
||||
|
||||
Renderer *render_ctx_;
|
||||
|
||||
std::unique_ptr<olive::plugin::PluginRenderer> plugin_renderer_;
|
||||
|
||||
DecoderCache *decoder_cache_;
|
||||
|
||||
ShaderCache *shader_cache_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(olive::RenderProcessor::RenderedWaveform)
|
||||
|
||||
#endif // OAK_RENDERPROCESSOR_H
|
||||
@@ -0,0 +1,214 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "renderticket.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
RenderTicket::RenderTicket()
|
||||
: is_running_(false)
|
||||
, has_result_(false)
|
||||
, finish_count_(0)
|
||||
{
|
||||
}
|
||||
|
||||
void RenderTicket::wait_for_finished(QMutex *mutex)
|
||||
{
|
||||
if (is_running_) {
|
||||
wait_.wait(mutex);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderTicket::start()
|
||||
{
|
||||
QMutexLocker locker(&lock_);
|
||||
|
||||
is_running_ = true;
|
||||
has_result_ = false;
|
||||
result_.clear();
|
||||
}
|
||||
|
||||
void RenderTicket::finish()
|
||||
{
|
||||
finish_internal(false, QVariant());
|
||||
}
|
||||
|
||||
void RenderTicket::finish(QVariant result)
|
||||
{
|
||||
finish_internal(true, result);
|
||||
}
|
||||
|
||||
QVariant RenderTicket::get()
|
||||
{
|
||||
wait_for_finished();
|
||||
|
||||
// We don't have to mutex around this because there is no way to write to `result_` after
|
||||
// the ticket has finished and the above function blocks the calling thread until it is finished
|
||||
return result_;
|
||||
}
|
||||
|
||||
void RenderTicket::wait_for_finished()
|
||||
{
|
||||
QMutexLocker locker(&lock_);
|
||||
|
||||
wait_for_finished(&lock_);
|
||||
}
|
||||
|
||||
bool RenderTicket::is_running(bool lock)
|
||||
{
|
||||
if (lock) {
|
||||
lock_.lock();
|
||||
}
|
||||
|
||||
bool running = is_running_;
|
||||
|
||||
if (lock) {
|
||||
lock_.unlock();
|
||||
}
|
||||
|
||||
return running;
|
||||
}
|
||||
|
||||
int RenderTicket::get_finish_count(bool lock)
|
||||
{
|
||||
if (lock) {
|
||||
lock_.lock();
|
||||
}
|
||||
|
||||
int count = finish_count_;
|
||||
|
||||
if (lock) {
|
||||
lock_.unlock();
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
bool RenderTicket::has_result()
|
||||
{
|
||||
QMutexLocker locker(&lock_);
|
||||
|
||||
return has_result_;
|
||||
}
|
||||
|
||||
void RenderTicket::finish_internal(bool has_result, QVariant result)
|
||||
{
|
||||
QMutexLocker locker(&lock_);
|
||||
|
||||
if (!is_running_) {
|
||||
qWarning() << "Tried to finish ticket that wasn't running";
|
||||
} else {
|
||||
is_running_ = false;
|
||||
has_result_ = has_result;
|
||||
result_ = result;
|
||||
finish_count_++;
|
||||
|
||||
wait_.wakeAll();
|
||||
|
||||
locker.unlock();
|
||||
|
||||
emit finished();
|
||||
}
|
||||
}
|
||||
|
||||
RenderTicketWatcher::RenderTicketWatcher(QObject *parent)
|
||||
: QObject(parent)
|
||||
, ticket_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
void RenderTicketWatcher::set_ticket(RenderTicketPtr ticket)
|
||||
{
|
||||
if (ticket_) {
|
||||
qCritical() << "Tried to set a ticket on a RenderTicketWatcher twice";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ticket) {
|
||||
qCritical() << "Tried to set a null ticket on a RenderTicketWatcher";
|
||||
return;
|
||||
}
|
||||
|
||||
ticket_ = ticket;
|
||||
|
||||
// Lock ticket so we can query if it's already finished by the time this code runs
|
||||
QMutexLocker locker(ticket->lock());
|
||||
|
||||
connect(ticket_.get(), &RenderTicket::finished, this,
|
||||
&RenderTicketWatcher::ticket_finished);
|
||||
|
||||
if (!ticket_->is_running(false) && ticket_->get_finish_count(false) > 0) {
|
||||
// Ticket has already finished before, so we emit a signal asynchronously
|
||||
// to avoid deleting this watcher before the caller has a chance to use
|
||||
// the returned pointer.
|
||||
QMetaObject::invokeMethod(this, &RenderTicketWatcher::ticket_finished,
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderTicketWatcher::is_running()
|
||||
{
|
||||
if (ticket_) {
|
||||
return ticket_->is_running();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderTicketWatcher::wait_for_finished()
|
||||
{
|
||||
if (ticket_) {
|
||||
ticket_->wait_for_finished();
|
||||
}
|
||||
}
|
||||
|
||||
QVariant RenderTicketWatcher::get()
|
||||
{
|
||||
if (ticket_) {
|
||||
return ticket_->get();
|
||||
} else {
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderTicketWatcher::has_result()
|
||||
{
|
||||
if (ticket_) {
|
||||
return ticket_->has_result();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderTicketWatcher::cancel()
|
||||
{
|
||||
if (ticket_) {
|
||||
ticket_->cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void RenderTicketWatcher::ticket_finished()
|
||||
{
|
||||
emit finished(this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_RENDERTICKET_H
|
||||
#define OAK_RENDERTICKET_H
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QMutex>
|
||||
#include <QWaitCondition>
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "common/cancelableobject.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class RenderTicket : public QObject, public CancelableObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
RenderTicket();
|
||||
|
||||
/**
|
||||
* @brief Get the ticket's current state
|
||||
*
|
||||
* This function is thread safe, unless `lock` is set to false. Then the caller has responsibility
|
||||
* of locking the mutex before and unlocking after this function is called.
|
||||
*/
|
||||
bool is_running(bool lock = true);
|
||||
|
||||
/**
|
||||
* @brief Determine how many times ticket has been finished
|
||||
*
|
||||
* This function is thread safe, unless `lock` is set to false. Then the caller has responsibility
|
||||
* of locking the mutex before and unlocking after this function is called.
|
||||
*/
|
||||
int get_finish_count(bool lock = true);
|
||||
|
||||
/**
|
||||
* @brief Check if this ticket has a result
|
||||
*
|
||||
* If this ticket is running, this will always return false.
|
||||
*/
|
||||
bool has_result();
|
||||
|
||||
/**
|
||||
* @brief Get value, if any
|
||||
*/
|
||||
QVariant get();
|
||||
|
||||
/**
|
||||
* @brief Wait for ticket to be finished
|
||||
*
|
||||
* If this ticket is not running, this function returns immediately.
|
||||
*/
|
||||
void wait_for_finished();
|
||||
void wait_for_finished(QMutex *mutex);
|
||||
|
||||
/**
|
||||
* @brief Access this ticket's mutex
|
||||
*
|
||||
* Use if you're doing several operations on a ticket and need to ensure thread safety while
|
||||
* doing so. Most of the time this isn't necessary since all functions are thread safe by default.
|
||||
*/
|
||||
QMutex *lock()
|
||||
{
|
||||
return &lock_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Signal to the ticket that it is running
|
||||
*
|
||||
* If any value is set, it is cleared.
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* @brief Finish ticket with no value
|
||||
*
|
||||
* Sets ticket to no longer running and assume it has received no result.
|
||||
*/
|
||||
void finish();
|
||||
|
||||
/**
|
||||
* @brief Finish ticket with value
|
||||
*
|
||||
* Sets ticket to no longer running and provide a value generated by the operation requested.
|
||||
*/
|
||||
void finish(QVariant result);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief Emitted when finish has been called by any means (either cancelled or with a result)
|
||||
*/
|
||||
void finished();
|
||||
|
||||
private:
|
||||
void finish_internal(bool has_result, QVariant result);
|
||||
|
||||
bool is_running_;
|
||||
|
||||
QVariant result_;
|
||||
|
||||
bool has_result_;
|
||||
|
||||
int finish_count_;
|
||||
|
||||
QMutex lock_;
|
||||
|
||||
QWaitCondition wait_;
|
||||
};
|
||||
|
||||
using RenderTicketPtr = std::shared_ptr<RenderTicket>;
|
||||
|
||||
class RenderTicketWatcher : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
RenderTicketWatcher(QObject *parent = nullptr);
|
||||
|
||||
RenderTicketPtr get_ticket() const
|
||||
{
|
||||
return ticket_;
|
||||
}
|
||||
|
||||
void set_ticket(RenderTicketPtr ticket);
|
||||
|
||||
bool is_running();
|
||||
|
||||
void wait_for_finished();
|
||||
|
||||
QVariant get();
|
||||
|
||||
bool has_result();
|
||||
|
||||
void cancel();
|
||||
|
||||
signals:
|
||||
void finished(RenderTicketWatcher *watcher);
|
||||
|
||||
private:
|
||||
RenderTicketPtr ticket_;
|
||||
|
||||
private slots:
|
||||
void ticket_finished();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(olive::RenderTicketPtr)
|
||||
|
||||
#endif // OAK_RENDERTICKET_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,173 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_RENDERWORKERPOOL_H
|
||||
#define OAK_RENDERWORKERPOOL_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QMutex>
|
||||
#include <QSet>
|
||||
#include <QThread>
|
||||
#include <QVector>
|
||||
#include <QWaitCondition>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "node/project/serializer/serializer.h"
|
||||
#include "render/ipc/frameslotpool.h"
|
||||
#include "render/ipc/ipcmessage.h"
|
||||
#include "render/ipc/sharedmemoryregion.h"
|
||||
#include "render/rendermanager.h"
|
||||
|
||||
class QProcess;
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class Project;
|
||||
|
||||
class RenderWorkerPool : public QThread {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit RenderWorkerPool(DecoderCache *decoder_cache,
|
||||
const QString &gpu_backend,
|
||||
QObject *parent = nullptr);
|
||||
~RenderWorkerPool() override;
|
||||
|
||||
bool submit_frame(RenderTicketPtr ticket,
|
||||
const RenderManager::RenderVideoParams ¶ms);
|
||||
|
||||
bool remove_ticket(RenderTicketPtr ticket);
|
||||
|
||||
void shutdown();
|
||||
|
||||
protected:
|
||||
void run() override;
|
||||
|
||||
private:
|
||||
struct Job {
|
||||
Job(RenderTicketPtr t, const RenderManager::RenderVideoParams &p)
|
||||
: ticket(t)
|
||||
, params(p)
|
||||
{
|
||||
}
|
||||
|
||||
RenderTicketPtr ticket;
|
||||
RenderManager::RenderVideoParams params;
|
||||
QString graph_path;
|
||||
QString node_token;
|
||||
QVector<FramePtr> input_frames;
|
||||
};
|
||||
|
||||
enum class JobResult {
|
||||
k_finished,
|
||||
k_retryable_failure,
|
||||
k_fatal_failure,
|
||||
k_cancelled
|
||||
};
|
||||
|
||||
struct ActiveJob {
|
||||
RenderTicketPtr ticket;
|
||||
qint64 process_id = 0;
|
||||
qint64 ticket_id = 0;
|
||||
};
|
||||
|
||||
struct PooledWorker {
|
||||
QProcess *process = nullptr;
|
||||
QString loaded_graph_path;
|
||||
qint64 last_used_ms = 0;
|
||||
int use_count = 0;
|
||||
|
||||
// Persistent shared memory for this worker. Reusing regions across frames
|
||||
// avoids the cost of creating/destroying large shm segments every render.
|
||||
ipc::SharedMemoryRegion output_region;
|
||||
ipc::FrameSlotPool output_pool;
|
||||
size_t output_slot_bytes = 0;
|
||||
QString output_shm_key;
|
||||
|
||||
ipc::SharedMemoryRegion input_region;
|
||||
ipc::FrameSlotPool input_pool;
|
||||
size_t input_slot_bytes = 0;
|
||||
QString input_shm_key;
|
||||
};
|
||||
|
||||
struct CachedGraph {
|
||||
QString path;
|
||||
};
|
||||
|
||||
bool prepare_job(RenderTicketPtr ticket,
|
||||
const RenderManager::RenderVideoParams ¶ms, Job *job);
|
||||
bool write_graph_snapshot(Project *project, QString *path);
|
||||
bool is_supported(const RenderManager::RenderVideoParams ¶ms) const;
|
||||
|
||||
void worker_loop(int worker_index,
|
||||
std::vector<std::unique_ptr<PooledWorker>> *local_pool);
|
||||
void process_job(const Job &job, int worker_index,
|
||||
std::vector<std::unique_ptr<PooledWorker>> *local_pool);
|
||||
JobResult process_job_attempt(const Job &job, int worker_index,
|
||||
int attempt_index, PooledWorker *worker);
|
||||
void finish_with_frame(RenderTicketPtr ticket, const ipc::FrameSlotPool &pool,
|
||||
uint32_t slot);
|
||||
void cleanup_graph_file(const QString &path);
|
||||
void add_graph_path_ref(const QString &path);
|
||||
void add_graph_path_ref_locked(const QString &path);
|
||||
void release_graph_path_ref(const QString &path);
|
||||
void release_graph_path_ref_locked(const QString &path);
|
||||
void set_graph_path_cached(const QString &path, bool cached);
|
||||
void set_graph_path_cached_locked(const QString &path, bool cached);
|
||||
void cancel_active_process(qint64 process_id);
|
||||
void set_active_worker(int worker_index, RenderTicketPtr ticket,
|
||||
QProcess *worker, qint64 ticket_id);
|
||||
void clear_active_worker(int worker_index, qint64 process_id);
|
||||
int worker_count() const;
|
||||
|
||||
std::unique_ptr<PooledWorker>
|
||||
acquire_worker(std::vector<std::unique_ptr<PooledWorker>> *local_pool,
|
||||
const QString &graph_path);
|
||||
void return_worker(std::vector<std::unique_ptr<PooledWorker>> *local_pool,
|
||||
std::unique_ptr<PooledWorker> worker, bool keep_alive);
|
||||
void shutdown_worker(PooledWorker *worker);
|
||||
void
|
||||
shutdown_local_pool(std::vector<std::unique_ptr<PooledWorker>> *local_pool);
|
||||
void clear_graph_cache();
|
||||
|
||||
DecoderCache *decoder_cache_;
|
||||
QString gpu_backend_;
|
||||
QMutex mutex_;
|
||||
QWaitCondition wait_;
|
||||
std::deque<Job> queue_;
|
||||
bool stopping_ = false;
|
||||
QVector<ActiveJob> active_jobs_;
|
||||
QHash<QUuid, CachedGraph> graph_cache_;
|
||||
QHash<QString, int> graph_path_ref_count_;
|
||||
QSet<QString> cached_graph_paths_;
|
||||
|
||||
static constexpr uint32_t k_output_slots = 2;
|
||||
static constexpr int k_max_attempts = 2;
|
||||
static constexpr int k_max_width = 4096;
|
||||
static constexpr int k_max_height = 2160;
|
||||
static constexpr int k_worker_idle_timeout_ms = 30000;
|
||||
static constexpr int k_worker_max_uses = 100;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERWORKERPOOL_H
|
||||
@@ -0,0 +1,65 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_SHADERCODE_H
|
||||
#define OAK_SHADERCODE_H
|
||||
|
||||
#include "common/filefunctions.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class ShaderCode {
|
||||
public:
|
||||
ShaderCode(const QString &frag_code = QString(),
|
||||
const QString &vert_code = QString())
|
||||
: frag_code_(frag_code)
|
||||
, vert_code_(vert_code)
|
||||
{
|
||||
}
|
||||
|
||||
const QString &frag_code() const
|
||||
{
|
||||
return frag_code_;
|
||||
}
|
||||
void set_frag_code(const QString &f)
|
||||
{
|
||||
frag_code_ = f;
|
||||
}
|
||||
|
||||
const QString &vert_code() const
|
||||
{
|
||||
return vert_code_;
|
||||
}
|
||||
void set_vert_code(const QString &v)
|
||||
{
|
||||
vert_code_ = v;
|
||||
}
|
||||
|
||||
private:
|
||||
QString frag_code_;
|
||||
|
||||
QString vert_code_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_SHADERCODE_H
|
||||
@@ -0,0 +1,184 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "subtitleparams.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include "common/xmlutils.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
QString SubtitleParams::generate_ass_header()
|
||||
{
|
||||
// NOTE: We'll probably implement more customization as we support ASS better. Right now, we only
|
||||
// natively support SRT and only make this header because FFmpeg requires it.
|
||||
static const int k_ass_default_play_res_x = 384;
|
||||
static const int k_ass_default_play_res_y = 288;
|
||||
static const QString k_ass_default_font = QStringLiteral("Arial");
|
||||
static const int k_ass_default_font_size = 16;
|
||||
static const int k_ass_default_primary_color = 0xFFFFFF; // White
|
||||
static const int k_ass_default_secondary_color = 0xFFFFFF; // White
|
||||
static const int k_ass_default_outline_color = 0x000000; // Black
|
||||
static const int k_ass_default_back_color = 0x000000; // Black
|
||||
static const int k_ass_bold = 0;
|
||||
static const int k_ass_italic = 0;
|
||||
static const int k_ass_underline = 0;
|
||||
static const int k_ass_strike = 0;
|
||||
static const int k_ass_border_style = 1;
|
||||
static const int k_ass_alignment = 2;
|
||||
|
||||
QString ass_code;
|
||||
|
||||
// Header info
|
||||
ass_code.append(QStringLiteral("[Script Info]\r\n"));
|
||||
ass_code.append(QStringLiteral("; Script generated by %1 %2\r\n")
|
||||
.arg(QCoreApplication::applicationName(),
|
||||
QCoreApplication::applicationVersion()));
|
||||
ass_code.append(QStringLiteral("ScriptType: v4.00+\r\n"));
|
||||
ass_code.append(QStringLiteral("PlayResX: %1\r\n")
|
||||
.arg(QString::number(k_ass_default_play_res_x)));
|
||||
ass_code.append(QStringLiteral("PlayResY: %1\r\n")
|
||||
.arg(QString::number(k_ass_default_play_res_y)));
|
||||
ass_code.append(QStringLiteral("ScaledBorderAndShadow: yes\r\n"));
|
||||
ass_code.append(QStringLiteral("\r\n"));
|
||||
|
||||
// ASSv4 header
|
||||
ass_code.append(QStringLiteral("[V4+ Styles]\r\n"));
|
||||
ass_code.append(QStringLiteral("Format: Name, "));
|
||||
ass_code.append(QStringLiteral("Fontname, Fontsize, "));
|
||||
ass_code.append(QStringLiteral(
|
||||
"PrimaryColour, SecondaryColour, OutlineColour, BackColour, "));
|
||||
ass_code.append(QStringLiteral("Bold, Italic, Underline, StrikeOut, "));
|
||||
ass_code.append(QStringLiteral("ScaleX, ScaleY, "));
|
||||
ass_code.append(QStringLiteral("Spacing, Angle, "));
|
||||
ass_code.append(QStringLiteral("BorderStyle, Outline, Shadow, "));
|
||||
ass_code.append(QStringLiteral("Alignment, MarginL, MarginR, MarginV, "));
|
||||
ass_code.append(QStringLiteral("Encoding\r\n"));
|
||||
ass_code.append(QStringLiteral("Style: "));
|
||||
|
||||
// Name
|
||||
ass_code.append(QStringLiteral("Default,"));
|
||||
|
||||
// Font{name,size}
|
||||
ass_code.append(QStringLiteral("%1,%2,").arg(
|
||||
k_ass_default_font, QString::number(k_ass_default_font_size)));
|
||||
|
||||
// {Primary,Secondary,Outline,Back}Colour
|
||||
ass_code.append(QStringLiteral("&H%1,&H%2,&H%3,&H%4,")
|
||||
.arg(QString::number(k_ass_default_primary_color, 16),
|
||||
QString::number(k_ass_default_secondary_color, 16),
|
||||
QString::number(k_ass_default_outline_color, 16),
|
||||
QString::number(k_ass_default_back_color, 16)));
|
||||
|
||||
// Bold, Italic, Underline, StrikeOut
|
||||
ass_code.append(
|
||||
QStringLiteral("%1,%2,%3,%4,")
|
||||
.arg(QString::number(k_ass_bold), QString::number(k_ass_italic),
|
||||
QString::number(k_ass_underline), QString::number(k_ass_strike)));
|
||||
|
||||
// Scale{X,Y}
|
||||
ass_code.append(QStringLiteral("100,100,"));
|
||||
|
||||
// Spacing, Angle
|
||||
ass_code.append(QStringLiteral("0,0,"));
|
||||
|
||||
// BorderStyle, Outline, Shadow
|
||||
ass_code.append(
|
||||
QStringLiteral("%1,1,0,").arg(QString::number(k_ass_border_style)));
|
||||
|
||||
// Alignment, Margin[LRV]
|
||||
ass_code.append(
|
||||
QStringLiteral("%1,10,10,10,").arg(QString::number(k_ass_alignment)));
|
||||
|
||||
// Encoding
|
||||
ass_code.append(QStringLiteral("0\r\n"));
|
||||
ass_code.append(QStringLiteral("\r\n"));
|
||||
ass_code.append(QStringLiteral("[Events]\r\n"));
|
||||
ass_code.append(QStringLiteral(
|
||||
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\r\n"));
|
||||
|
||||
return ass_code;
|
||||
}
|
||||
|
||||
void SubtitleParams::load(QXmlStreamReader *reader)
|
||||
{
|
||||
this->clear();
|
||||
|
||||
while (xml_read_next_start_element(reader)) {
|
||||
if (reader->name() == QStringLiteral("streamindex")) {
|
||||
set_stream_index(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("enabled")) {
|
||||
set_enabled(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("subtitles")) {
|
||||
while (xml_read_next_start_element(reader)) {
|
||||
if (reader->name() == QStringLiteral("subtitle")) {
|
||||
Rational in, out;
|
||||
QString text;
|
||||
|
||||
XMLAttributeLoop(reader, attr)
|
||||
{
|
||||
if (attr.name() == QStringLiteral("in")) {
|
||||
in = Rational::from_string(
|
||||
attr.value().toString().toStdString());
|
||||
} else if (attr.name() == QStringLiteral("out")) {
|
||||
out = Rational::from_string(
|
||||
attr.value().toString().toStdString());
|
||||
}
|
||||
}
|
||||
|
||||
text = reader->readElementText();
|
||||
|
||||
this->push_back(Subtitle(TimeRange(in, out), text));
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SubtitleParams::save(QXmlStreamWriter *writer) const
|
||||
{
|
||||
writer->writeTextElement(QStringLiteral("streamindex"),
|
||||
QString::number(stream_index_));
|
||||
writer->writeTextElement(QStringLiteral("enabled"),
|
||||
QString::number(enabled_));
|
||||
|
||||
writer->writeStartElement(QStringLiteral("subtitles"));
|
||||
for (auto it = this->cbegin(); it != this->cend(); it++) {
|
||||
writer->writeStartElement(QStringLiteral("subtitle"));
|
||||
writer->writeAttribute(
|
||||
QStringLiteral("in"),
|
||||
QString::fromStdString(it->time().in().to_string()));
|
||||
writer->writeAttribute(
|
||||
QStringLiteral("out"),
|
||||
QString::fromStdString(it->time().out().to_string()));
|
||||
writer->writeCharacters(it->text());
|
||||
writer->writeEndElement(); // subtitle
|
||||
}
|
||||
writer->writeEndElement(); // subtitles
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_SUBTITLEPARAMS_H
|
||||
#define OAK_SUBTITLEPARAMS_H
|
||||
|
||||
#include <olive/core/core.h>
|
||||
#include <QRect>
|
||||
#include <QString>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
using namespace olive::core;
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class Subtitle {
|
||||
public:
|
||||
Subtitle() = default;
|
||||
|
||||
Subtitle(const TimeRange &time, const QString &text)
|
||||
: range_(time)
|
||||
, text_(text)
|
||||
{
|
||||
}
|
||||
|
||||
const TimeRange &time() const
|
||||
{
|
||||
return range_;
|
||||
}
|
||||
void set_time(const TimeRange &t)
|
||||
{
|
||||
range_ = t;
|
||||
}
|
||||
|
||||
const QString &text() const
|
||||
{
|
||||
return text_;
|
||||
}
|
||||
void set_text(const QString &t)
|
||||
{
|
||||
text_ = t;
|
||||
}
|
||||
|
||||
private:
|
||||
TimeRange range_;
|
||||
|
||||
QString text_;
|
||||
};
|
||||
|
||||
class SubtitleParams : public std::vector<Subtitle> {
|
||||
public:
|
||||
SubtitleParams()
|
||||
{
|
||||
stream_index_ = 0;
|
||||
enabled_ = true;
|
||||
}
|
||||
|
||||
static QString generate_ass_header();
|
||||
|
||||
void load(QXmlStreamReader *reader);
|
||||
|
||||
void save(QXmlStreamWriter *writer) const;
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return !this->empty();
|
||||
}
|
||||
|
||||
Rational duration() const
|
||||
{
|
||||
if (this->empty()) {
|
||||
return 0;
|
||||
} else {
|
||||
return back().time().out();
|
||||
}
|
||||
}
|
||||
|
||||
int stream_index() const
|
||||
{
|
||||
return stream_index_;
|
||||
}
|
||||
void set_stream_index(int i)
|
||||
{
|
||||
stream_index_ = i;
|
||||
}
|
||||
|
||||
bool enabled() const
|
||||
{
|
||||
return enabled_;
|
||||
}
|
||||
void set_enabled(bool e)
|
||||
{
|
||||
enabled_ = e;
|
||||
}
|
||||
|
||||
private:
|
||||
int stream_index_;
|
||||
|
||||
bool enabled_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(olive::Subtitle)
|
||||
Q_DECLARE_METATYPE(olive::SubtitleParams)
|
||||
|
||||
#endif // OAK_SUBTITLEPARAMS_H
|
||||
@@ -0,0 +1,59 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 "texture.h"
|
||||
|
||||
#include "render/job/acceleratedjob.h"
|
||||
#include "renderer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
const Texture::Interpolation Texture::k_default_interpolation =
|
||||
Texture::k_mipmapped_linear;
|
||||
|
||||
Texture::~Texture()
|
||||
{
|
||||
if (is_renderer_alive()) {
|
||||
renderer_->destroy_texture(this);
|
||||
}
|
||||
|
||||
if (job_) {
|
||||
delete job_;
|
||||
}
|
||||
}
|
||||
|
||||
void Texture::upload(void *data, int linesize)
|
||||
{
|
||||
if (is_renderer_alive()) {
|
||||
renderer_->upload_to_texture(this->id(), this->params(), data, linesize);
|
||||
}
|
||||
}
|
||||
|
||||
void Texture::download(void *data, int linesize)
|
||||
{
|
||||
if (is_renderer_alive()) {
|
||||
renderer_->download_from_texture(this->id(), this->params(), data,
|
||||
linesize);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 OAK_RENDERTEXTURE_H
|
||||
#define OAK_RENDERTEXTURE_H
|
||||
|
||||
#include "common/avframeptr.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <QVariant>
|
||||
|
||||
#include "render/videoparams.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class AcceleratedJob;
|
||||
class Renderer;
|
||||
struct RendererLifetime {
|
||||
std::atomic<bool> alive{ true };
|
||||
};
|
||||
|
||||
class Texture;
|
||||
using TexturePtr = std::shared_ptr<Texture>;
|
||||
|
||||
class Texture {
|
||||
public:
|
||||
enum Interpolation { k_nearest, k_linear, k_mipmapped_linear };
|
||||
|
||||
static const Interpolation k_default_interpolation;
|
||||
|
||||
/**
|
||||
* @brief Construct a dummy texture with no renderer backend
|
||||
*/
|
||||
Texture(const VideoParams ¶m)
|
||||
: renderer_(nullptr)
|
||||
, renderer_lifetime_(nullptr)
|
||||
, params_(param)
|
||||
, job_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
Texture(const VideoParams &p, const T &j)
|
||||
: Texture(p)
|
||||
{
|
||||
job_ = new T(j);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a real texture linked to a renderer backend
|
||||
*/
|
||||
Texture(Renderer *renderer, const QVariant &native,
|
||||
const VideoParams ¶m,
|
||||
std::shared_ptr<RendererLifetime> lifetime = nullptr)
|
||||
: renderer_(renderer)
|
||||
, renderer_lifetime_(lifetime)
|
||||
, params_(param)
|
||||
, id_(native)
|
||||
, job_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
~Texture();
|
||||
|
||||
QVariant id() const
|
||||
{
|
||||
return id_;
|
||||
}
|
||||
|
||||
const VideoParams ¶ms() const
|
||||
{
|
||||
return params_;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static TexturePtr job(const VideoParams &p, const T &j)
|
||||
{
|
||||
return std::make_shared<Texture>(p, j);
|
||||
}
|
||||
|
||||
template <typename T> TexturePtr to_job(const T &job)
|
||||
{
|
||||
return Texture::job(params_, job);
|
||||
}
|
||||
|
||||
void upload(void *data, int linesize);
|
||||
|
||||
void download(void *data, int linesize);
|
||||
|
||||
bool is_dummy() const
|
||||
{
|
||||
return !renderer_;
|
||||
}
|
||||
|
||||
int width() const
|
||||
{
|
||||
return params_.effective_width();
|
||||
}
|
||||
|
||||
int height() const
|
||||
{
|
||||
return params_.effective_height();
|
||||
}
|
||||
|
||||
QVector2D virtual_resolution() const
|
||||
{
|
||||
return QVector2D(params_.square_pixel_width(), params_.height());
|
||||
}
|
||||
|
||||
PixelFormat format() const
|
||||
{
|
||||
return params_.format();
|
||||
}
|
||||
|
||||
int channel_count() const
|
||||
{
|
||||
return params_.channel_count();
|
||||
}
|
||||
|
||||
int divider() const
|
||||
{
|
||||
return params_.divider();
|
||||
}
|
||||
|
||||
const Rational &pixel_aspect_ratio() const
|
||||
{
|
||||
return params_.pixel_aspect_ratio();
|
||||
}
|
||||
|
||||
Renderer *renderer() const
|
||||
{
|
||||
return renderer_;
|
||||
}
|
||||
|
||||
bool is_job() const
|
||||
{
|
||||
return job_;
|
||||
}
|
||||
AcceleratedJob *job() const
|
||||
{
|
||||
return job_;
|
||||
}
|
||||
void handle_frame(AVFramePtr ptr)
|
||||
{
|
||||
frame_ = ptr;
|
||||
}
|
||||
AVFramePtr frame()
|
||||
{
|
||||
return frame_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool is_renderer_alive() const
|
||||
{
|
||||
return renderer_ &&
|
||||
(!renderer_lifetime_ || renderer_lifetime_->alive.load());
|
||||
}
|
||||
|
||||
Renderer *renderer_;
|
||||
std::shared_ptr<RendererLifetime> renderer_lifetime_;
|
||||
|
||||
VideoParams params_;
|
||||
|
||||
QVariant id_;
|
||||
|
||||
AcceleratedJob *job_;
|
||||
|
||||
AVFramePtr frame_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(olive::TexturePtr)
|
||||
|
||||
#endif // OAK_RENDERTEXTURE_H
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user