Files
oak-editor/app/render/renderprocessor.cpp
T
Mike-Solar ff0eee3a88 perf(plugin): cache PluginRenderer per-thread and skip redundant clip prefs
Fix three performance traps in the OFX plugin preview pipeline that
  caused slideshow-like performance when any plugin was active.

  1. Eliminate per-frame PluginRenderer creation (GL FBO alloc/free)

     RenderProcessor is stack-allocated per ticket, so its
     plugin_renderer_ member was constructed and destroyed every frame.
     PluginRenderer inherits OpenGLRenderer, whose PostInit() calls
     glGenFramebuffers() and whose destructor calls glDeleteFramebuffers().
     On Apple Silicon's TBDR this is pathologically expensive.

     Fix: use a thread_local cached PluginRenderer so each render thread
     creates it only once and reuses it forever.

  2. Call getClipPreferences() conditionally

     The code unconditionally called instance->getClipPreferences() on
     every single frame. This dispatches kOfxImageEffectActionGetClipPreferences
     into the plugin even when no inputs or parameters have changed.

     Fix: check areClipPrefsDirty() first. The OpenFX Host Support library
     already tracks this flag and sets it to true when slave params or clip
     connections change.

  3. Call ApplyParamOverrides() before renderAction

     ApplyParamOverrides() was defined but never invoked, so animated
     plugin parameters were never pushed into the OFX instance before
     rendering.

     Fix: call it after beginRenderAction() and before renderAction(),
     injecting the current NodeValueRow values at the correct OfxTime.
2026-05-16 17:34:29 +08:00

766 lines
21 KiB
C++

/***
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 <QOpenGLContext>
#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/opengl/openglrenderer.h"
#include "render/plugin/pluginrenderer.h"
#include "pluginSupport/OliveClip.h"
#include "pluginSupport/OliveHost.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::GenerateTexture(const rational &time,
const rational &frame_length)
{
TimeRange range = TimeRange(time, time + frame_length);
NodeValueTable table;
if (Node *node = QtUtils::ValueToPtr<Node>(ticket_->property("node"))) {
table = GenerateTable(node, range);
}
NodeValue tex_val = table.Get(NodeValue::kTexture);
ResolveJobs(tex_val);
return tex_val.toTexture();
}
FramePtr RenderProcessor::GenerateFrame(TexturePtr texture,
const rational &time)
{
// Set up output frame parameters
VideoParams frame_params = GetCacheVideoParams();
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::kRGBAChannelCount);
}
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_->CreateTexture(tex_params);
ColorTransformJob job;
job.SetColorProcessor(output_color_transform);
job.SetInputTexture(texture);
job.SetInputAlphaAssociation(
OLIVE_CONFIG("ReassocLinToNonLin").toBool() ? kAlphaAssociated :
kAlphaNone);
render_ctx_->BlitColorManaged(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_->CreateTexture(frame_params);
QMatrix4x4 matrix = ticket_->property("matrix").value<QMatrix4x4>();
// No color transform, just blit
ShaderJob job;
job.Insert(QStringLiteral("ove_maintex"),
NodeValue(NodeValue::kTexture,
QVariant::fromValue(texture)));
job.Insert(QStringLiteral("ove_mvpmat"),
NodeValue(NodeValue::kMatrix, matrix));
render_ctx_->BlitToTexture(render_ctx_->GetDefaultShader(), job,
blit_tex.get());
// Replace texture that we're going to download in the next step
texture = blit_tex;
}
render_ctx_->Flush();
render_ctx_->DownloadFromTexture(texture->id(), texture->params(),
frame->data(),
frame->linesize_pixels());
}
return frame;
}
void RenderProcessor::Run()
{
// Depending on the render ticket type, start a job
RenderManager::TicketType type =
ticket_->property("type").value<RenderManager::TicketType>();
SetCancelPointer(ticket_->GetCancelAtom());
VideoParams params=ticket_->property("vparam").value<VideoParams>();
params.set_format(PixelFormat::F32);
SetCacheVideoParams(params);
SetCacheAudioParams(ticket_->property("aparam").value<AudioParams>());
if (IsCancelled()) {
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::kTypeVideo: {
rational time = ticket_->property("time").value<rational>();
rational frame_length = GetCacheVideoParams().frame_rate_as_time_base();
if (GetCacheVideoParams().interlacing() !=
VideoParams::kInterlaceNone) {
frame_length /= 2;
}
TexturePtr texture = GenerateTexture(time, frame_length);
if (!render_ctx_) {
ticket_->Finish();
} else {
if (GetCacheVideoParams().interlacing() !=
VideoParams::kInterlaceNone) {
// Get next between frame and interlace it
TexturePtr top = texture;
TexturePtr bottom =
GenerateTexture(time + frame_length, frame_length);
if (GetCacheVideoParams().interlacing() ==
VideoParams::kInterlacedBottomFirst) {
std::swap(top, bottom);
}
texture = render_ctx_->InterlaceTexture(top, bottom,
GetCacheVideoParams());
}
if (HeardCancel()) {
// 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::kFrame || !cache.isEmpty()) {
// Convert to CPU frame
frame = GenerateFrame(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::SaveCacheFrame(
cache, uuid, time, timebase, frame);
ticket_->setProperty("cached", cache_result);
}
}
if (return_type == RenderManager::kTexture) {
// Return GPU texture
if (!texture) {
texture =
render_ctx_->CreateTexture(GetCacheVideoParams());
render_ctx_->ClearDestination(texture.get());
}
render_ctx_->Flush();
ticket_->Finish(QVariant::fromValue(texture));
} else {
ticket_->Finish(QVariant::fromValue(frame));
}
}
}
break;
}
case RenderManager::kTypeAudio: {
TimeRange time = ticket_->property("time").value<TimeRange>();
NodeValueTable table;
if (Node *node = QtUtils::ValueToPtr<Node>(ticket_->property("node"))) {
table = GenerateTable(node, time);
}
NodeValue sample_val = table.Get(NodeValue::kSamples);
ResolveJobs(sample_val);
SampleBuffer samples = sample_val.toSamples();
if (samples.is_allocated()) {
if (ticket_->property("clamp").toBool() && !IsCancelled()) {
samples.clamp();
}
if (ticket_->property("enablewaveforms").toBool() &&
!IsCancelled()) {
AudioVisualWaveform vis;
vis.set_channel_count(samples.audio_params().channel_count());
vis.OverwriteSamples(samples,
samples.audio_params().sample_rate());
ticket_->setProperty("waveform", QVariant::fromValue(vis));
}
}
if (HeardCancel()) {
ticket_->Finish();
} else {
ticket_->Finish(QVariant::fromValue(samples));
}
break;
}
default:
// Fail
ticket_->Finish();
}
}
DecoderPtr
RenderProcessor::ResolveDecoderFromInput(const QString &decoder_id,
const Decoder::CodecStream &stream)
{
if (!stream.IsValid()) {
qWarning() << "Attempted to resolve the decoder of a null stream";
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::CreateFromID(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->IncrementAccessTime(
RenderManager::kDryRunInterval.toDouble() * 1000);
}
}
return dec;
}
NodeValueDatabase RenderProcessor::GenerateDatabase(const Node *node,
const TimeRange &range)
{
NodeValueDatabase db = super::GenerateDatabase(node, range);
if (const MultiCamNode *multicam =
dynamic_cast<const MultiCamNode *>(node)) {
if (QtUtils::ValueToPtr<MultiCamNode>(ticket_->property("multicam")) ==
multicam) {
int sz = multicam->GetSourceCount();
QVector<TexturePtr> multicam_tex(sz);
for (int i = 0; i < sz; i++) {
NodeValueTable t =
GenerateTable(multicam->GetConnectedRenderOutput(
multicam->kSourcesInput, i),
range, multicam);
NodeValue val = GenerateRowValueElement(
multicam, multicam->kSourcesInput, i, &t, range);
ResolveJobs(val);
multicam_tex[i] = val.toTexture();
}
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::ProcessVideoFootage(TexturePtr destination,
const FootageJob *stream,
const rational &input_time)
{
if (ticket_->property("type").value<RenderManager::TicketType>() !=
RenderManager::kTypeVideo) {
// 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::ValueToPtr<ColorManager>(ticket_->property("colormanager"));
QString using_colorspace = stream_data.colorspace();
if (using_colorspace.isEmpty()) {
// FIXME:
qWarning() << "HAVEN'T GOTTEN DEFAULT INPUT COLORSPACE";
}
Decoder::CodecStream default_codec_stream(
stream->filename(), stream_data.stream_index(), GetCurrentBlock());
QString decoder_id = stream->decoder();
DecoderPtr decoder = nullptr;
switch (stream_data.video_type()) {
case VideoParams::kVideoTypeVideo:
case VideoParams::kVideoTypeStill:
decoder = ResolveDecoderFromInput(decoder_id, default_codec_stream);
break;
case VideoParams::kVideoTypeImageSequence: {
if (render_ctx_) {
// Since image sequences involve multiple files, we don't engage the decoder cache
decoder = Decoder::CreateFromID(decoder_id);
QString frame_filename;
int64_t frame_number =
stream_data.get_time_in_timebase_units(input_time);
frame_filename = Decoder::TransformImageSequenceFileName(
stream->filename(), frame_number);
// Decoder will close automatically since it's a stream_ptr
decoder->Open(Decoder::CodecStream(
frame_filename, stream_data.stream_index(), GetCurrentBlock()));
}
break;
}
}
if (decoder && render_ctx_) {
Decoder::RetrieveVideoParams p;
p.divider = stream->video_params().divider();
p.maximum_format = destination->format();
if (!IsCancelled()) {
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::kVideoTypeVideo) ?
input_time :
Decoder::kAnyTimecode;
p.cancelled = GetCancelPointer();
p.force_range = stream_data.color_range();
p.src_interlacing = stream_data.interlacing();
unmanaged_texture = decoder->RetrieveVideo(p);
if (!IsCancelled() && unmanaged_texture) {
// 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->GetReferenceColorSpace());
ColorTransformJob job;
job.SetColorProcessor(processor);
job.SetInputTexture(unmanaged_texture);
if (stream_data.channel_count() !=
VideoParams::kRGBAChannelCount ||
stream_data.colorspace() ==
color_manager->GetReferenceColorSpace()) {
job.SetInputAlphaAssociation(kAlphaNone);
} else if (stream_data.premultiplied_alpha()) {
job.SetInputAlphaAssociation(kAlphaAssociated);
} else {
job.SetInputAlphaAssociation(kAlphaUnassociated);
}
render_ctx_->BlitColorManaged(job, destination.get());
}
}
}
}
}
void RenderProcessor::ProcessAudioFootage(SampleBuffer &destination,
const FootageJob *stream,
const TimeRange &input_time)
{
DecoderPtr decoder = ResolveDecoderFromInput(
stream->decoder(),
Decoder::CodecStream(stream->filename(),
stream->audio_params().stream_index(), nullptr));
if (decoder) {
const AudioParams &audio_params = GetCacheAudioParams();
Decoder::RetrieveAudioStatus status = decoder->RetrieveAudio(
destination, input_time, audio_params, stream->cache_path(),
loop_mode(),
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()));
if (status == Decoder::kWaitingForConform) {
ticket_->setProperty("incomplete", true);
}
}
}
void RenderProcessor::ProcessShader(TexturePtr destination, const Node *node,
const ShaderJob *job)
{
if (!render_ctx_) {
return;
}
QString full_shader_id =
QStringLiteral("%1:%2").arg(node->id(), job->GetShaderID());
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_->CreateNativeShader(
node->GetShaderCode(job->GetShaderID()));
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_->BlitToTexture(shader, const_cast<ShaderJob&>(*job), destination.get());
}
void RenderProcessor::ProcessSamples(SampleBuffer &destination,
const Node *node, const TimeRange &range,
const SampleJob &job)
{
if (!job.samples().is_allocated()) {
return;
}
NodeValueRow value_db;
const AudioParams &audio_params = GetCacheAudioParams();
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::fromDouble(range.in().toDouble() + sample_to_second);
// Update all non-sample and non-footage inputs
for (auto j = job.GetValues().constBegin();
j != job.GetValues().constEnd(); j++) {
TimeRange r = TimeRange(this_sample_time, this_sample_time);
NodeValueTable value = ProcessInput(node, j.key(), r);
value_db.insert(j.key(),
GenerateRowValue(node, j.key(), &value, r));
}
node->ProcessSamples(value_db, job.samples(), destination, i);
}
}
void RenderProcessor::ProcessColorTransform(TexturePtr destination,
const Node *node,
const ColorTransformJob *job)
{
if (!render_ctx_) {
return;
}
render_ctx_->BlitColorManaged(*job, destination.get());
}
void RenderProcessor::ProcessFrameGeneration(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->GenerateFrame(frame, *job);
destination->Upload(frame->data(), frame->linesize_pixels());
}
TexturePtr RenderProcessor::ProcessPluginJob(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 = nullptr;
{
thread_local static std::shared_ptr<plugin::PluginRenderer>
cached_plugin_renderer;
if (!cached_plugin_renderer) {
auto *gl = dynamic_cast<OpenGLRenderer *>(render_ctx_);
if (gl && gl->context()) {
cached_plugin_renderer =
std::make_shared<plugin::PluginRenderer>();
cached_plugin_renderer->Init(gl->context());
cached_plugin_renderer->PostInit();
}
}
plugin_renderer = cached_plugin_renderer.get();
}
if (!plugin_renderer) {
return destination;
}
NodeValueRow &values = plugin_job->GetValues();
auto is_usable_texture = [](const TexturePtr &tex) {
if (!tex) {
return false;
}
if (!tex->IsDummy() && 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()->GetEffectInputID();
}
if (!effect_input_id.isEmpty()) {
if (TexturePtr effect_tex = values.value(effect_input_id).toTexture();
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).toTexture();
is_usable_texture(source_tex)) {
src = source_tex;
} else if (TexturePtr effect_tex =
values.value(plugin::kTextureInput).toTexture();
is_usable_texture(effect_tex)) {
src = effect_tex;
}
}
if (!src) {
for (auto it = values.cbegin(); it != values.cend(); ++it) {
if (it.value().type() == NodeValue::kTexture) {
if (TexturePtr any_tex = it.value().toTexture();
is_usable_texture(any_tex)) {
src = any_tex;
break;
}
}
}
}
plugin_renderer->RenderPlugin(
src,
*plugin_job,
destination,
destination->params(),
true,
false);
return destination;
}
TexturePtr RenderProcessor::ProcessVideoCacheJob(const CacheJob *val)
{
FramePtr frame = FrameHashCache::LoadCacheFrame(val->GetFilename());
if (frame) {
TexturePtr tex = CreateTexture(frame->video_params());
if (tex) {
tex->Upload(frame->data(), frame->linesize_pixels());
return tex;
}
} else {
QStringList s = ticket_->property("badcache").toStringList();
s.append(val->GetFilename());
ticket_->setProperty("badcache", s);
}
return nullptr;
}
TexturePtr RenderProcessor::CreateTexture(const VideoParams &p)
{
if (render_ctx_) {
return render_ctx_->CreateTexture(p);
} else {
return super::CreateTexture(p);
}
}
void RenderProcessor::ConvertToReferenceSpace(TexturePtr destination,
TexturePtr source,
const QString &input_cs)
{
if (!render_ctx_) {
return;
}
ColorManager *color_manager =
QtUtils::ValueToPtr<ColorManager>(ticket_->property("colormanager"));
ColorProcessorPtr cp = ColorProcessor::Create(
color_manager, input_cs, color_manager->GetReferenceColorSpace());
ColorTransformJob ctj;
ctj.SetColorProcessor(cp);
ctj.SetInputTexture(source);
ctj.SetInputAlphaAssociation(kAlphaAssociated);
render_ctx_->BlitColorManaged(ctj, destination.get());
}
bool RenderProcessor::UseCache() const
{
return static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()) ==
RenderMode::kOffline;
}
}