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:
2026-07-20 03:23:28 +08:00
parent 026ff94b5e
commit 28c4426236
604 changed files with 243 additions and 172 deletions
+84
View File
@@ -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
)
+32
View File
@@ -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
+157
View File
@@ -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 &params)
{
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)));
}
}
+88
View File
@@ -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 &params);
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
+130
View File
@@ -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);
}
}
+85
View File
@@ -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
+397
View File
@@ -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 &params,
const void *data, int linesize)
{
upload_to_texture_(handle_, &handle, &params, data, linesize);
}
// Downloads backend texture data into a caller-provided CPU buffer.
void DynamicRenderer::download_from_texture(const QVariant &handle,
const VideoParams &params, void *data,
int linesize)
{
download_from_texture_(handle_, &handle, &params, 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_);
}
}
}
+134
View File
@@ -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 &params, const void *data,
int linesize) override;
// Downloads backend texture pixels to CPU memory.
virtual void download_from_texture(const QVariant &handle,
const VideoParams &params, 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
+121
View File
@@ -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
+66
View File
@@ -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
+230
View File
@@ -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 &params)
{
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());
}
}
}
+174
View File
@@ -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());
}
}
+77
View File
@@ -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
+34
View File
@@ -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
+92
View File
@@ -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
+434
View File
@@ -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_;
}
}
}
+190
View File
@@ -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
+462
View File
@@ -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");
}
}
}
+109
View File
@@ -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
+132
View File
@@ -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();
}
}
+91
View File
@@ -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
+59
View File
@@ -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 &params)
{
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;
}
}
+27
View File
@@ -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
)
+179
View File
@@ -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
+183
View File
@@ -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
+230
View File
@@ -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
+161
View File
@@ -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
+231
View File
@@ -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
+123
View File
@@ -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
+185
View File
@@ -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
+29
View File
@@ -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
)
+27
View File
@@ -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
{
}
+80
View File
@@ -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
+68
View File
@@ -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
+184
View File
@@ -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
+196
View File
@@ -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
+43
View File
@@ -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
+27
View File
@@ -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
+81
View File
@@ -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
+74
View File
@@ -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
+123
View File
@@ -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
+29
View File
@@ -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
+96
View File
@@ -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;
}
}
+75
View File
@@ -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
+68
View File
@@ -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;
}
}
+55
View File
@@ -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
+29
View File
@@ -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
)
+401
View File
@@ -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}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/ocioconf">
@QRC_BODY@
</qresource>
</RCC>
+22
View File
@@ -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
)
+237
View File
@@ -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
+164
View File
@@ -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 &params, const void *data,
int linesize) override;
virtual void download_from_texture(const QVariant &handle,
const VideoParams &params, 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
+318
View File
@@ -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();
}
}
}
+187
View File
@@ -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
+6
View File
@@ -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
+85
View File
@@ -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 &params);
}
// 作用: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
+93
View File
@@ -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 &params)
{
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);
}
}
+111
View File
@@ -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 &params);
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
+874
View File
@@ -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);
}
}
}
+244
View File
@@ -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
+371
View File
@@ -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();
}
}
+150
View File
@@ -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
+51
View File
@@ -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
+194
View File
@@ -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 &params, 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 &params)
{
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
+217
View File
@@ -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 &params,
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 &params);
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 &params)
{
blit_color_managed(job, nullptr, params);
}
TexturePtr interlace_texture(TexturePtr top, TexturePtr bottom,
const VideoParams &params);
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 &params, const void *data,
int linesize) = 0;
virtual void download_from_texture(const QVariant &handle,
const VideoParams &params, 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 &params);
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
+75
View File
@@ -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;
}
}
+76
View File
@@ -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
+407
View File
@@ -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 &params)
{
// 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 &params)
{
// 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());
}
}
}
+277
View File
@@ -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 &params);
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 &params);
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
+52
View File
@@ -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
+892
View File
@@ -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;
}
}
+127
View File
@@ -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 &params,
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
+214
View File
@@ -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);
}
}
+169
View File
@@ -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
+173
View File
@@ -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 &params);
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 &params, Job *job);
bool write_graph_snapshot(Project *project, QString *path);
bool is_supported(const RenderManager::RenderVideoParams &params) 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
+65
View File
@@ -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
+184
View File
@@ -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
}
}
+127
View File
@@ -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
+59
View File
@@ -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);
}
}
}
+195
View File
@@ -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 &param)
: 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 &param,
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 &params() 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