build: split the engine into liboakengine.so; worker drops the UI entirely
Physical split: app/{audio,cli,codec,common,config,node,pluginSupport,
render,task,timeline,undo,tool,shaders} plus coreengine, version and
ui/icons+colorcoding move to a new top-level engine/ tree, built as
liboakengine.so (shared). The render backends (oakgl/oakvulkan) move
with it and link the engine library instead of embedding a static
render-core subset (libolive-rendercore is gone).
- oak-render-worker now links liboakengine instead of the whole
libolive-editor object set: 336MB -> 2.9MB, no Qt Widgets UI
- the editor links liboakengine for the engine and keeps only UI
objects in libolive-editor
- install/packaging: GNUInstallDirs libdir on Linux, bundle copy on
macOS, oakengine.dll staged for NSIS, AppImage validation entry
- fix backend lookup for the new layout: DynamicRenderer searched
../app but backends now live in engine/; a stale pre-split liboakgl
in the build tree got dlopened instead, re-initialized and later
destroyed the interposed engine statics (full-suite segfault at
DialogSequenceParameterTab, found via gdb watchpoint)
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
target_sources(oakengine PRIVATE
|
||||
olivehost.h
|
||||
olivehost.cpp
|
||||
oliveplugininstance.h
|
||||
oliveplugininstance.cpp
|
||||
pluginprogressreporter.h
|
||||
pluginprogressreporter.cpp
|
||||
oliveclip.cpp
|
||||
oliveclip.h
|
||||
paraminstance.cpp
|
||||
paraminstance.h
|
||||
image.cpp
|
||||
image.h
|
||||
)
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Oak Video Editor - Non-Linear Video Editor
|
||||
* Copyright (C) 2026 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 "image.h"
|
||||
|
||||
#include "ofxImageEffect.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
|
||||
static const char *pixel_depth_to_ofx(core::PixelFormat format)
|
||||
{
|
||||
switch (format) {
|
||||
case core::PixelFormat::u8:
|
||||
return kOfxBitDepthByte;
|
||||
case core::PixelFormat::u16:
|
||||
return kOfxBitDepthShort;
|
||||
case core::PixelFormat::f16:
|
||||
return kOfxBitDepthHalf;
|
||||
case core::PixelFormat::f32:
|
||||
return kOfxBitDepthFloat;
|
||||
case core::PixelFormat::invalid:
|
||||
case core::PixelFormat::count:
|
||||
break;
|
||||
}
|
||||
|
||||
return kOfxBitDepthNone;
|
||||
}
|
||||
|
||||
static const char *components_to_ofx(int channel_count)
|
||||
{
|
||||
switch (channel_count) {
|
||||
case 1:
|
||||
return kOfxImageComponentAlpha;
|
||||
case 3:
|
||||
return kOfxImageComponentRGB;
|
||||
case 4:
|
||||
return kOfxImageComponentRGBA;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return kOfxImageComponentNone;
|
||||
}
|
||||
|
||||
Image::Image(OFX::Host::ImageEffect::ClipInstance &clip_instance)
|
||||
: OFX::Host::ImageEffect::Image(clip_instance)
|
||||
, width_(0)
|
||||
, height_(0)
|
||||
, format_(core::PixelFormat::invalid)
|
||||
, premultiplied_alpha_(false)
|
||||
, channel_count_(0)
|
||||
, row_bytes_(0)
|
||||
, bounds_{ 0, 0, 0, 0 }
|
||||
, rod_{ 0, 0, 0, 0 }
|
||||
{
|
||||
}
|
||||
|
||||
Image::Image(OFX::Host::ImageEffect::ClipInstance &clip_instance,
|
||||
const VideoParams ¶ms, const OfxRectI &bounds,
|
||||
const OfxRectI &rod, bool clear)
|
||||
: OFX::Host::ImageEffect::Image(clip_instance)
|
||||
, width_(0)
|
||||
, height_(0)
|
||||
, format_(core::PixelFormat::invalid)
|
||||
, premultiplied_alpha_(false)
|
||||
, channel_count_(0)
|
||||
, row_bytes_(0)
|
||||
, bounds_{ 0, 0, 0, 0 }
|
||||
, rod_{ 0, 0, 0, 0 }
|
||||
{
|
||||
allocate_from_params(params, bounds, rod, clear);
|
||||
}
|
||||
|
||||
Image::~Image()
|
||||
{
|
||||
}
|
||||
|
||||
void Image::allocate_from_params(const VideoParams ¶ms,
|
||||
const OfxRectI &bounds, const OfxRectI &rod,
|
||||
bool clear)
|
||||
{
|
||||
allocate(bounds.x2 - bounds.x1, bounds.y2 - bounds.y1, params.format(),
|
||||
params.channel_count(), params.premultiplied_alpha(), bounds, rod,
|
||||
clear);
|
||||
}
|
||||
|
||||
void Image::ensure_allocated_from_params(const VideoParams ¶ms,
|
||||
const OfxRectI &bounds,
|
||||
const OfxRectI &rod, bool clear)
|
||||
{
|
||||
bool same = (width_ == bounds.x2 - bounds.x1) &&
|
||||
(height_ == bounds.y2 - bounds.y1) &&
|
||||
(format_ == params.format()) &&
|
||||
(channel_count_ == params.channel_count()) &&
|
||||
(premultiplied_alpha_ == params.premultiplied_alpha()) &&
|
||||
(bounds_.x1 == bounds.x1) && (bounds_.y1 == bounds.y1) &&
|
||||
(bounds_.x2 == bounds.x2) && (bounds_.y2 == bounds.y2) &&
|
||||
(rod_.x1 == rod.x1) && (rod_.y1 == rod.y1) &&
|
||||
(rod_.x2 == rod.x2) && (rod_.y2 == rod.y2);
|
||||
|
||||
if (!same) {
|
||||
allocate_from_params(params, bounds, rod, clear);
|
||||
} else if (clear && !image_.empty()) {
|
||||
std::fill(image_.begin(), image_.end(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Image::allocate(int width, int height, core::PixelFormat format,
|
||||
int channel_count, bool premultiplied_alpha,
|
||||
const OfxRectI &bounds, const OfxRectI &rod, bool clear)
|
||||
{
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
format_ = format;
|
||||
channel_count_ = channel_count;
|
||||
premultiplied_alpha_ = premultiplied_alpha;
|
||||
bounds_ = bounds;
|
||||
rod_ = rod;
|
||||
|
||||
int bytes_per_component = format_.byte_count();
|
||||
row_bytes_ = width_ * channel_count_ * bytes_per_component;
|
||||
int buffer_size = row_bytes_ * height_;
|
||||
if (buffer_size < 0) {
|
||||
buffer_size = 0;
|
||||
}
|
||||
|
||||
image_.resize(static_cast<size_t>(buffer_size));
|
||||
if (clear && !image_.empty()) {
|
||||
std::fill(image_.begin(), image_.end(), 0);
|
||||
}
|
||||
|
||||
setPointerProperty(kOfxImagePropData, image_.data());
|
||||
setIntProperty(kOfxImagePropRowBytes, row_bytes_);
|
||||
setIntProperty(kOfxImagePropBounds, bounds.x1, 0);
|
||||
setIntProperty(kOfxImagePropBounds, bounds.y1, 1);
|
||||
setIntProperty(kOfxImagePropBounds, bounds.x2, 2);
|
||||
setIntProperty(kOfxImagePropBounds, bounds.y2, 3);
|
||||
setIntProperty(kOfxImagePropRegionOfDefinition, rod.x1, 0);
|
||||
setIntProperty(kOfxImagePropRegionOfDefinition, rod.y1, 1);
|
||||
setIntProperty(kOfxImagePropRegionOfDefinition, rod.x2, 2);
|
||||
setIntProperty(kOfxImagePropRegionOfDefinition, rod.y2, 3);
|
||||
setStringProperty(kOfxImageEffectPropComponents,
|
||||
components_to_ofx(channel_count_));
|
||||
setStringProperty(kOfxImageEffectPropPixelDepth, pixel_depth_to_ofx(format_));
|
||||
setStringProperty(kOfxImageEffectPropPreMultiplication,
|
||||
premultiplied_alpha_ ? kOfxImagePreMultiplied :
|
||||
kOfxImageUnPreMultiplied);
|
||||
}
|
||||
|
||||
core::PixelFormat Image::pixel_format()
|
||||
{
|
||||
if (format_ != core::PixelFormat::invalid) {
|
||||
return format_;
|
||||
}
|
||||
|
||||
std::string type = getStringProperty(kOfxImageEffectPropPixelDepth);
|
||||
if (type == kOfxBitDepthByte) {
|
||||
format_ = core::PixelFormat::u8;
|
||||
} else if (type == kOfxBitDepthShort) {
|
||||
format_ = core::PixelFormat::u16;
|
||||
} else if (type == kOfxBitDepthHalf) {
|
||||
format_ = core::PixelFormat::f16;
|
||||
} else if (type == kOfxBitDepthFloat) {
|
||||
format_ = core::PixelFormat::f32;
|
||||
} else {
|
||||
format_ = core::PixelFormat::invalid;
|
||||
}
|
||||
return format_;
|
||||
}
|
||||
|
||||
bool Image::premultiplied_alpha()
|
||||
{
|
||||
std::string premultiplied =
|
||||
getStringProperty(kOfxImageEffectPropPreMultiplication);
|
||||
premultiplied_alpha_ = (premultiplied == kOfxImagePreMultiplied);
|
||||
return premultiplied_alpha_;
|
||||
}
|
||||
|
||||
int Image::width()
|
||||
{
|
||||
int bounds[4] = { 0 };
|
||||
getIntPropertyN(kOfxImagePropBounds, bounds, 4);
|
||||
width_ = bounds[2] - bounds[0];
|
||||
return width_;
|
||||
}
|
||||
|
||||
int Image::height()
|
||||
{
|
||||
int bounds[4] = { 0 };
|
||||
getIntPropertyN(kOfxImagePropBounds, bounds, 4);
|
||||
height_ = bounds[3] - bounds[1];
|
||||
return height_;
|
||||
}
|
||||
|
||||
int Image::channel_count()
|
||||
{
|
||||
std::string type = getStringProperty(kOfxImageEffectPropComponents);
|
||||
if (type == kOfxImageComponentAlpha) {
|
||||
channel_count_ = 1;
|
||||
} else if (type == kOfxImageComponentRGBA) {
|
||||
channel_count_ = 4;
|
||||
} else if (type == kOfxImageComponentRGB) {
|
||||
channel_count_ = 3;
|
||||
} else {
|
||||
channel_count_ = 0;
|
||||
}
|
||||
return channel_count_;
|
||||
}
|
||||
|
||||
} // namespace plugin
|
||||
} // namespace olive
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Oak Video Editor - Non-Linear Video Editor
|
||||
* Copyright (C) 2026 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_OLIVE_EDITOR_PLUGIN_IMAGE_H
|
||||
#define OAK_OLIVE_EDITOR_PLUGIN_IMAGE_H
|
||||
|
||||
#include "ofxCore.h"
|
||||
#include "ofxImageEffect.h"
|
||||
#include "ofxhClip.h"
|
||||
#include "olive/core/render/pixelformat.h"
|
||||
#include "render/loopmode.h"
|
||||
#include "render/videoparams.h"
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
class Image : public OFX::Host::ImageEffect::Image {
|
||||
public:
|
||||
Image(OFX::Host::ImageEffect::ClipInstance &clip_instance);
|
||||
Image(OFX::Host::ImageEffect::ClipInstance &clip_instance,
|
||||
const VideoParams ¶ms, const OfxRectI &bounds,
|
||||
const OfxRectI &rod, bool clear = true);
|
||||
~Image();
|
||||
uint8_t *data()
|
||||
{
|
||||
return (uint8_t *)getPointerProperty(kOfxImagePropData);
|
||||
}
|
||||
int width();
|
||||
int height();
|
||||
core::PixelFormat pixel_format();
|
||||
bool premultiplied_alpha();
|
||||
int channel_count();
|
||||
|
||||
void allocate_from_params(const VideoParams ¶ms, const OfxRectI &bounds,
|
||||
const OfxRectI &rod, bool clear = true);
|
||||
void ensure_allocated_from_params(const VideoParams ¶ms,
|
||||
const OfxRectI &bounds, const OfxRectI &rod,
|
||||
bool clear = false);
|
||||
void allocate(int width, int height, core::PixelFormat format,
|
||||
int channel_count, bool premultiplied_alpha,
|
||||
const OfxRectI &bounds, const OfxRectI &rod,
|
||||
bool clear = true);
|
||||
int row_bytes() const
|
||||
{
|
||||
return row_bytes_;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::vector<uint8_t> image_;
|
||||
int width_;
|
||||
int height_;
|
||||
core::PixelFormat format_;
|
||||
bool premultiplied_alpha_;
|
||||
int channel_count_;
|
||||
int row_bytes_;
|
||||
OfxRectI bounds_;
|
||||
OfxRectI rod_;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif //OAK_OLIVE_EDITOR_PLUGIN_IMAGE_H
|
||||
@@ -0,0 +1,857 @@
|
||||
/*
|
||||
* 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-1.
|
||||
//
|
||||
|
||||
#include "oliveclip.h"
|
||||
|
||||
#include "common/current.h"
|
||||
#include "common/ffmpegutils.h"
|
||||
#include "ofxCore.h"
|
||||
#include "ofxhClip.h"
|
||||
#include "pluginSupport/image.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
#include <QOpenGLFunctions>
|
||||
#endif
|
||||
#include "common/ffmpegutils.h"
|
||||
#include "render/renderer.h"
|
||||
#include <ffmpeg_bridge/ffmpeg_bridge.h>
|
||||
namespace
|
||||
{
|
||||
// The bridge header only defines the little-endian pixel formats. FFmpeg
|
||||
// numbers each big-endian variant immediately before its little-endian
|
||||
// counterpart (BE == LE - 1), so derive the BE constants used below.
|
||||
constexpr int fb_pix_fmt_gray_f32_be = fb_pix_fmt_gray_f32_le - 1;
|
||||
constexpr int fb_pix_fmt_rgb_f32_be = fb_pix_fmt_rgb_f32_le - 1;
|
||||
constexpr int fb_pix_fmt_rgba_f32_be = fb_pix_fmt_rgba_f32_le - 1;
|
||||
|
||||
const std::string k_bit_depth_none_str(kOfxBitDepthNone);
|
||||
const std::string k_bit_depth_byte_str(kOfxBitDepthByte);
|
||||
const std::string k_bit_depth_short_str(kOfxBitDepthShort);
|
||||
const std::string k_bit_depth_half_str(kOfxBitDepthHalf);
|
||||
const std::string k_bit_depth_float_str(kOfxBitDepthFloat);
|
||||
const std::string k_image_component_none_str(kOfxImageComponentNone);
|
||||
const std::string k_image_component_alpha_str(kOfxImageComponentAlpha);
|
||||
const std::string k_image_component_rgb_str(kOfxImageComponentRGB);
|
||||
const std::string k_image_component_rgba_str(kOfxImageComponentRGBA);
|
||||
const std::string k_image_premult_str(kOfxImagePreMultiplied);
|
||||
const std::string k_image_un_premult_str(kOfxImageUnPreMultiplied);
|
||||
const std::string k_image_field_none_str(kOfxImageFieldNone);
|
||||
const std::string k_image_field_upper_str(kOfxImageFieldUpper);
|
||||
const std::string k_image_field_lower_str(kOfxImageFieldLower);
|
||||
|
||||
static int bytes_to_pixels(int byte_linesize, const olive::VideoParams ¶ms)
|
||||
{
|
||||
const int bytes_per_pixel =
|
||||
params.channel_count() * params.format().byte_count();
|
||||
if (bytes_per_pixel <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return byte_linesize / bytes_per_pixel;
|
||||
}
|
||||
|
||||
static int packed_float_channels(int fmt)
|
||||
{
|
||||
switch (fmt) {
|
||||
case fb_pix_fmt_gray_f32_le:
|
||||
case fb_pix_fmt_gray_f32_be:
|
||||
return 1;
|
||||
case fb_pix_fmt_rgb_f32_le:
|
||||
case fb_pix_fmt_rgb_f32_be:
|
||||
return 3;
|
||||
case fb_pix_fmt_rgba_f32_le:
|
||||
case fb_pix_fmt_rgba_f32_be:
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static bool packed_dst_info(int fmt, int *channels,
|
||||
int *bytes_per_component)
|
||||
{
|
||||
switch (fmt) {
|
||||
case fb_pix_fmt_gra_y8:
|
||||
*channels = 1;
|
||||
*bytes_per_component = 1;
|
||||
return true;
|
||||
case fb_pix_fmt_rg_b24:
|
||||
*channels = 3;
|
||||
*bytes_per_component = 1;
|
||||
return true;
|
||||
case fb_pix_fmt_rgba:
|
||||
*channels = 4;
|
||||
*bytes_per_component = 1;
|
||||
return true;
|
||||
case fb_pix_fmt_gra_y16_le:
|
||||
*channels = 1;
|
||||
*bytes_per_component = 2;
|
||||
return true;
|
||||
case fb_pix_fmt_rg_b48_le:
|
||||
*channels = 3;
|
||||
*bytes_per_component = 2;
|
||||
return true;
|
||||
case fb_pix_fmt_rgb_a64_le:
|
||||
*channels = 4;
|
||||
*bytes_per_component = 2;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static olive::AVFramePtr
|
||||
readback_texture_to_frame(olive::TexturePtr texture,
|
||||
const olive::VideoParams ¶ms)
|
||||
{
|
||||
if (!texture || texture->is_dummy() || !texture->renderer()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int pix_fmt = olive::FFmpegUtils::get_f_fmpeg_pixel_format(
|
||||
params.format(), params.channel_count());
|
||||
if (pix_fmt == fb_pix_fmt_none) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!fb_pix_fmt_is_planar(pix_fmt)) {
|
||||
olive::AVFramePtr frame = olive::create_av_frame_ptr();
|
||||
frame->set_format(pix_fmt);
|
||||
frame->set_width(params.width());
|
||||
frame->set_height(params.height());
|
||||
if (frame->get_buffer(0) < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
const int linesize_pixels = bytes_to_pixels(frame->linesize(0), params);
|
||||
texture->renderer()->download_from_texture(
|
||||
texture->id(), params, frame->data(0), linesize_pixels);
|
||||
return frame;
|
||||
}
|
||||
|
||||
olive::VideoParams rgba_params(params.width(), params.height(),
|
||||
olive::core::PixelFormat::u8, 4,
|
||||
params.pixel_aspect_ratio(),
|
||||
params.interlacing(), params.divider());
|
||||
|
||||
olive::AVFramePtr rgba_frame = olive::create_av_frame_ptr();
|
||||
rgba_frame->set_format(fb_pix_fmt_rgba);
|
||||
rgba_frame->set_width(params.width());
|
||||
rgba_frame->set_height(params.height());
|
||||
if (rgba_frame->get_buffer(0) < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const int linesize_pixels =
|
||||
bytes_to_pixels(rgba_frame->linesize(0), rgba_params);
|
||||
texture->renderer()->download_from_texture(
|
||||
texture->id(), rgba_params, rgba_frame->data(0), linesize_pixels);
|
||||
|
||||
olive::AVFramePtr dst = olive::create_av_frame_ptr();
|
||||
dst->set_format(pix_fmt);
|
||||
dst->set_width(params.width());
|
||||
dst->set_height(params.height());
|
||||
if (dst->get_buffer(0) < 0) {
|
||||
return rgba_frame;
|
||||
}
|
||||
|
||||
FBScaler *scaler = fb_scaler_create(
|
||||
rgba_frame->width(), rgba_frame->height(), rgba_frame->format(),
|
||||
dst->width(), dst->height(), pix_fmt, FB_SCALER_POINT);
|
||||
if (!scaler) {
|
||||
return rgba_frame;
|
||||
}
|
||||
|
||||
uint8_t *src_data[4];
|
||||
int src_linesize[4];
|
||||
uint8_t *dst_data[4];
|
||||
int dst_linesize[4];
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
src_data[i] = rgba_frame->data(i);
|
||||
src_linesize[i] = rgba_frame->linesize(i);
|
||||
dst_data[i] = dst->data(i);
|
||||
dst_linesize[i] = dst->linesize(i);
|
||||
}
|
||||
fb_scaler_scale_slices(scaler, src_data, src_linesize,
|
||||
rgba_frame->height(), dst_data, dst_linesize);
|
||||
fb_scaler_free(&scaler);
|
||||
return dst;
|
||||
}
|
||||
|
||||
static olive::AVFramePtr convert_packed_float_frame(olive::AVFramePtr src,
|
||||
int dst_fmt)
|
||||
{
|
||||
if (!src || !src->data(0)) {
|
||||
return nullptr;
|
||||
}
|
||||
const int src_channels =
|
||||
packed_float_channels(src->format());
|
||||
if (src_channels == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int dst_channels = 0;
|
||||
int bytes_per_component = 0;
|
||||
if (!packed_dst_info(dst_fmt, &dst_channels, &bytes_per_component)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
olive::AVFramePtr dst = olive::create_av_frame_ptr();
|
||||
dst->set_format(dst_fmt);
|
||||
dst->set_width(src->width());
|
||||
dst->set_height(src->height());
|
||||
if (dst->get_buffer(0) < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto clamp01 = [](float v) -> float { return std::clamp(v, 0.0f, 1.0f); };
|
||||
|
||||
for (int y = 0; y < src->height(); ++y) {
|
||||
const float *src_row = reinterpret_cast<const float *>(
|
||||
src->data(0) + y * src->linesize(0));
|
||||
uint8_t *dst_row = dst->data(0) + y * dst->linesize(0);
|
||||
|
||||
if (bytes_per_component == 2) {
|
||||
auto *dst_row_u16 = reinterpret_cast<uint16_t *>(dst_row);
|
||||
for (int x = 0; x < src->width(); ++x) {
|
||||
const float *pix = src_row + x * src_channels;
|
||||
float r = pix[0];
|
||||
float g = (src_channels > 1) ? pix[1] : r;
|
||||
float b = (src_channels > 2) ? pix[2] : r;
|
||||
float a = (src_channels > 3) ? pix[3] : 1.0f;
|
||||
if (dst_channels == 1) {
|
||||
float luma = 0.2126f * r + 0.7152f * g + 0.0722f * b;
|
||||
dst_row_u16[x] = static_cast<uint16_t>(
|
||||
std::lround(clamp01(luma) * 65535.0f));
|
||||
continue;
|
||||
}
|
||||
dst_row_u16[x * dst_channels + 0] =
|
||||
static_cast<uint16_t>(std::lround(clamp01(r) * 65535.0f));
|
||||
dst_row_u16[x * dst_channels + 1] =
|
||||
static_cast<uint16_t>(std::lround(clamp01(g) * 65535.0f));
|
||||
dst_row_u16[x * dst_channels + 2] =
|
||||
static_cast<uint16_t>(std::lround(clamp01(b) * 65535.0f));
|
||||
if (dst_channels == 4) {
|
||||
dst_row_u16[x * dst_channels + 3] = static_cast<uint16_t>(
|
||||
std::lround(clamp01(a) * 65535.0f));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int x = 0; x < src->width(); ++x) {
|
||||
const float *pix = src_row + x * src_channels;
|
||||
float r = pix[0];
|
||||
float g = (src_channels > 1) ? pix[1] : r;
|
||||
float b = (src_channels > 2) ? pix[2] : r;
|
||||
float a = (src_channels > 3) ? pix[3] : 1.0f;
|
||||
if (dst_channels == 1) {
|
||||
float luma = 0.2126f * r + 0.7152f * g + 0.0722f * b;
|
||||
dst_row[x] = static_cast<uint8_t>(
|
||||
std::lround(clamp01(luma) * 255.0f));
|
||||
continue;
|
||||
}
|
||||
dst_row[x * dst_channels + 0] =
|
||||
static_cast<uint8_t>(std::lround(clamp01(r) * 255.0f));
|
||||
dst_row[x * dst_channels + 1] =
|
||||
static_cast<uint8_t>(std::lround(clamp01(g) * 255.0f));
|
||||
dst_row[x * dst_channels + 2] =
|
||||
static_cast<uint8_t>(std::lround(clamp01(b) * 255.0f));
|
||||
if (dst_channels == 4) {
|
||||
dst_row[x * dst_channels + 3] =
|
||||
static_cast<uint8_t>(std::lround(clamp01(a) * 255.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dst;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string &olive::plugin::OliveClipInstance::getUnmappedBitDepth() const
|
||||
{
|
||||
// Return the plugin's preferred pixel depth from base class
|
||||
// This is set during getClipPreferences action via setPixelDepth()
|
||||
const std::string &depth = getPixelDepth();
|
||||
if (!depth.empty() && depth != k_bit_depth_none_str) {
|
||||
return depth;
|
||||
}
|
||||
// Fallback to params_ if base class value is not set
|
||||
switch (params_.format()) {
|
||||
case PixelFormat::invalid:
|
||||
return k_bit_depth_none_str;
|
||||
case PixelFormat::u8:
|
||||
return k_bit_depth_byte_str;
|
||||
case PixelFormat::u10:
|
||||
return k_bit_depth_none_str;
|
||||
case PixelFormat::u16:
|
||||
return k_bit_depth_short_str;
|
||||
case PixelFormat::f16:
|
||||
return k_bit_depth_half_str;
|
||||
case PixelFormat::f32:
|
||||
return k_bit_depth_float_str;
|
||||
default:
|
||||
return k_bit_depth_none_str;
|
||||
}
|
||||
}
|
||||
const std::string &
|
||||
olive::plugin::OliveClipInstance::getUnmappedComponents() const
|
||||
{
|
||||
// Return the plugin's preferred components from base class
|
||||
// This is set during getClipPreferences action via setComponents()
|
||||
const std::string &comp = getComponents();
|
||||
if (!comp.empty() && comp != k_image_component_none_str) {
|
||||
return comp;
|
||||
}
|
||||
// Fallback to params_ if base class value is not set
|
||||
switch (params_.channel_count()) {
|
||||
case 1:
|
||||
return k_image_component_alpha_str;
|
||||
case 3:
|
||||
return k_image_component_rgb_str;
|
||||
case 4:
|
||||
return k_image_component_rgba_str;
|
||||
default:
|
||||
return k_image_component_none_str;
|
||||
}
|
||||
}
|
||||
const std::string &olive::plugin::OliveClipInstance::getPremult() const
|
||||
{
|
||||
if (params_.premultiplied_alpha()) {
|
||||
return k_image_premult_str;
|
||||
} else {
|
||||
return k_image_un_premult_str;
|
||||
}
|
||||
}
|
||||
double olive::plugin::OliveClipInstance::getAspectRatio() const
|
||||
{
|
||||
double par = params_.pixel_aspect_ratio().to_double();
|
||||
if (par == 0.0) {
|
||||
return 1.0; // default PAR when not explicitly set
|
||||
}
|
||||
return par;
|
||||
}
|
||||
double olive::plugin::OliveClipInstance::getFrameRate() const
|
||||
{
|
||||
return params_.frame_rate().to_double();
|
||||
}
|
||||
void olive::plugin::OliveClipInstance::getFrameRange(double &start_frame,
|
||||
double &end_frame) const
|
||||
{
|
||||
start_frame = params_.frame_rate().to_double() * params_.start_time();
|
||||
end_frame =
|
||||
start_frame + params_.frame_rate().to_double() * params_.duration();
|
||||
}
|
||||
const std::string &olive::plugin::OliveClipInstance::getFieldOrder() const
|
||||
{
|
||||
switch (params_.interlacing()) {
|
||||
case VideoParams::k_interlace_none:
|
||||
return k_image_field_none_str;
|
||||
case VideoParams::k_interlaced_top_first:
|
||||
return k_image_field_upper_str;
|
||||
case VideoParams::k_interlaced_bottom_first:
|
||||
return k_image_field_lower_str;
|
||||
}
|
||||
return k_image_field_none_str;
|
||||
}
|
||||
bool olive::plugin::OliveClipInstance::getConnected() const
|
||||
{
|
||||
if (name_ == kOfxImageEffectOutputClipName) {
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
if (!output_textures_.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (images_.empty())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
if (!input_textures_.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (images_.empty())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
double olive::plugin::OliveClipInstance::getUnmappedFrameRate() const
|
||||
{
|
||||
return getFrameRate();
|
||||
}
|
||||
void olive::plugin::OliveClipInstance::getUnmappedFrameRange(
|
||||
double &start_frame, double &end_frame) const
|
||||
{
|
||||
getFrameRange(start_frame, end_frame);
|
||||
}
|
||||
bool olive::plugin::OliveClipInstance::getContinuousSamples() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OFX::Host::ImageEffect::Image *
|
||||
olive::plugin::OliveClipInstance::getImage(OfxTime time,
|
||||
const OfxRectD *optional_bounds)
|
||||
{
|
||||
OfxRectD rod_d = getRegionOfDefinition(time);
|
||||
OfxRectI rod = { static_cast<int>(std::floor(rod_d.x1)),
|
||||
static_cast<int>(std::floor(rod_d.y1)),
|
||||
static_cast<int>(std::ceil(rod_d.x2)),
|
||||
static_cast<int>(std::ceil(rod_d.y2)) };
|
||||
(void)optional_bounds;
|
||||
// Always return full-frame images to keep input data consistent.
|
||||
OfxRectI bounds = rod;
|
||||
|
||||
if (name_ == "Output") {
|
||||
if (!images_.contains(time)) {
|
||||
// make a new ref counted image
|
||||
images_.insert(time,
|
||||
new Image(*const_cast<OliveClipInstance *>(this),
|
||||
params_, bounds, rod, true));
|
||||
}
|
||||
|
||||
// add another reference to the member image for this fetch
|
||||
// as we have a ref count of 1 due to construction, this will
|
||||
// cause the output image never to delete by the plugin
|
||||
// when it releases the image
|
||||
images_[time]->addReference();
|
||||
|
||||
images_[time]->ensure_allocated_from_params(params_, bounds, rod, true);
|
||||
|
||||
// return it
|
||||
return images_[time];
|
||||
} else {
|
||||
if (images_.contains(time)) {
|
||||
Image *image = images_.value(time);
|
||||
image->ensure_allocated_from_params(params_, bounds, rod, false);
|
||||
image->addReference();
|
||||
return image;
|
||||
}
|
||||
|
||||
// Fetch on demand for the input clip.
|
||||
// Use plugin-preferred params to ensure the image format matches
|
||||
// what the plugin expects (may differ from input texture format)
|
||||
VideoParams preferred_params = getPluginPreferredParams();
|
||||
if (preferred_params.format() == core::PixelFormat::invalid) {
|
||||
preferred_params = params_;
|
||||
}
|
||||
// Keep dimensions and other settings from params_
|
||||
preferred_params.set_width(params_.width());
|
||||
preferred_params.set_height(params_.height());
|
||||
preferred_params.set_pixel_aspect_ratio(params_.pixel_aspect_ratio());
|
||||
|
||||
// Guard against zero-size or invalid-format images that would
|
||||
// cause EXC_BAD_ACCESS when the plugin accesses pixel data.
|
||||
if (preferred_params.width() <= 0 || preferred_params.height() <= 0 ||
|
||||
preferred_params.format() == core::PixelFormat::invalid ||
|
||||
preferred_params.channel_count() <= 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Cache the on-demand image like the output path does, so repeated
|
||||
// fetches at the same time reuse it and getConnected() reflects it.
|
||||
// The extra reference keeps the cached image alive when the plugin
|
||||
// releases its own.
|
||||
prune_images_cache();
|
||||
Image *image = new Image(*this, preferred_params, bounds, rod, true);
|
||||
images_.insert(time, image);
|
||||
image->addReference();
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
OFX::Host::ImageEffect::Image *
|
||||
olive::plugin::OliveClipInstance::getOutputImage(OfxTime time)
|
||||
{
|
||||
if (images_.contains(time)) {
|
||||
return images_.value(time);
|
||||
}
|
||||
|
||||
OfxRectD rod_d = getRegionOfDefinition(time);
|
||||
OfxRectI rod = { static_cast<int>(std::floor(rod_d.x1)),
|
||||
static_cast<int>(std::floor(rod_d.y1)),
|
||||
static_cast<int>(std::ceil(rod_d.x2)),
|
||||
static_cast<int>(std::ceil(rod_d.y2)) };
|
||||
OfxRectI bounds = rod;
|
||||
|
||||
// Use plugin-preferred params instead of params_ to ensure the image
|
||||
// is created with the format the plugin expects
|
||||
VideoParams preferred_params = getPluginPreferredParams();
|
||||
if (preferred_params.format() == core::PixelFormat::invalid) {
|
||||
preferred_params = params_;
|
||||
}
|
||||
// Keep the dimensions and other settings from params_
|
||||
preferred_params.set_width(params_.width());
|
||||
preferred_params.set_height(params_.height());
|
||||
preferred_params.set_pixel_aspect_ratio(params_.pixel_aspect_ratio());
|
||||
|
||||
auto image = new Image(*this, preferred_params, bounds, rod, true);
|
||||
images_.insert(time, image);
|
||||
return image;
|
||||
}
|
||||
|
||||
olive::VideoParams
|
||||
olive::plugin::OliveClipInstance::getPluginPreferredParams() const
|
||||
{
|
||||
VideoParams result = params_;
|
||||
|
||||
// Get format from base class _pixelDepth (set by getClipPreferences)
|
||||
const std::string &depth = getPixelDepth();
|
||||
if (!depth.empty()) {
|
||||
if (depth == kOfxBitDepthByte) {
|
||||
result.set_format(core::PixelFormat::u8);
|
||||
} else if (depth == kOfxBitDepthShort) {
|
||||
result.set_format(core::PixelFormat::u16);
|
||||
} else if (depth == kOfxBitDepthHalf) {
|
||||
result.set_format(core::PixelFormat::f16);
|
||||
} else if (depth == kOfxBitDepthFloat) {
|
||||
result.set_format(core::PixelFormat::f32);
|
||||
}
|
||||
}
|
||||
|
||||
// Get channel count from base class _components (set by getClipPreferences)
|
||||
const std::string &comp = getComponents();
|
||||
if (!comp.empty()) {
|
||||
if (comp == kOfxImageComponentRGBA) {
|
||||
result.set_channel_count(4);
|
||||
} else if (comp == kOfxImageComponentRGB) {
|
||||
result.set_channel_count(3);
|
||||
} else if (comp == kOfxImageComponentAlpha) {
|
||||
result.set_channel_count(1);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
OfxRectD
|
||||
olive::plugin::OliveClipInstance::getRegionOfDefinition(OfxTime time) const
|
||||
{
|
||||
if (regionOfDefinitions_.contains(time)) {
|
||||
return regionOfDefinitions_.value(time);
|
||||
}
|
||||
OfxRectD region_of_definition;
|
||||
region_of_definition.x1 = region_of_definition.y1 = 0;
|
||||
double par = params_.pixel_aspect_ratio().to_double();
|
||||
region_of_definition.x2 = params_.width() * par;
|
||||
region_of_definition.y2 = params_.height();
|
||||
if (region_of_definition.x2 <= 0 || region_of_definition.y2 <= 0) {
|
||||
// The params provide no usable region; fall back to the default set
|
||||
// via setDefaultRegionOfDefinition().
|
||||
return defaultRegionOfDefinitions_;
|
||||
}
|
||||
return region_of_definition;
|
||||
}
|
||||
void olive::plugin::OliveClipInstance::setRegionOfDefinition(
|
||||
OfxRectD region_of_definition, OfxTime time)
|
||||
{
|
||||
regionOfDefinitions_[time] = region_of_definition;
|
||||
}
|
||||
|
||||
void olive::plugin::OliveClipInstance::setDefaultRegionOfDefinition(
|
||||
OfxRectD region_of_definition)
|
||||
{
|
||||
defaultRegionOfDefinitions_ = region_of_definition;
|
||||
}
|
||||
|
||||
void olive::plugin::OliveClipInstance::prune_images_cache()
|
||||
{
|
||||
// Do not prune output clip images; they may have external references
|
||||
// added by getImage()/addReference() and are typically single-frame.
|
||||
if (name_ == kOfxImageEffectOutputClipName) {
|
||||
return;
|
||||
}
|
||||
while (images_.size() > k_max_input_image_cache) {
|
||||
auto it = images_.begin();
|
||||
Image *img = it.value();
|
||||
images_.erase(it);
|
||||
delete img;
|
||||
}
|
||||
}
|
||||
|
||||
void olive::plugin::OliveClipInstance::setParams(const VideoParams ¶ms)
|
||||
{
|
||||
params_ = params;
|
||||
// Sync with OpenFX Host Support's _pixelDepth and _components
|
||||
setPixelDepth(getUnmappedBitDepth());
|
||||
setComponents(getUnmappedComponents());
|
||||
}
|
||||
|
||||
void olive::plugin::OliveClipInstance::setInputTexture(TexturePtr texture,
|
||||
OfxTime time,
|
||||
bool readback_cpu)
|
||||
{
|
||||
if (!texture) {
|
||||
return;
|
||||
}
|
||||
VideoParams incoming = texture->params();
|
||||
|
||||
// Preserve time-related properties from the host/project.
|
||||
// The frame rate of an OFX clip should reflect the project's frame rate,
|
||||
// not the individual input texture's frame rate. If different inputs
|
||||
// have different frame rates, setupClipPreferencesArgs throws an exception.
|
||||
Rational saved_frame_rate = params_.frame_rate();
|
||||
Rational saved_time_base = params_.time_base();
|
||||
|
||||
this->params_ = incoming;
|
||||
|
||||
params_.set_frame_rate(saved_frame_rate);
|
||||
params_.set_time_base(saved_time_base);
|
||||
// Note: We do NOT call setPixelDepth/setComponents here because
|
||||
// those should be set by getClipPreferences to reflect the PLUGIN's
|
||||
// preferred format, not the input texture's format.
|
||||
// The base class values are used by getUnmappedBitDepth/Components
|
||||
// to report plugin capabilities to the plugin itself.
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
input_textures_.insert(time, texture);
|
||||
#endif
|
||||
|
||||
// In OpenGL render path, skip CPU readback entirely.
|
||||
// The plugin will fetch input via loadTexture() using GPU texture IDs.
|
||||
// If the plugin falls back to getImage(), it will be created on-demand
|
||||
// in getImage() with zero-initialized data.
|
||||
if (!readback_cpu) {
|
||||
return;
|
||||
}
|
||||
|
||||
AVFramePtr frame = texture->frame();
|
||||
if (!frame || !frame->data(0)) {
|
||||
frame = readback_texture_to_frame(texture, params_);
|
||||
}
|
||||
int expected_fmt = FFmpegUtils::get_f_fmpeg_pixel_format(
|
||||
params_.format(), params_.channel_count());
|
||||
if (expected_fmt == fb_pix_fmt_none) {
|
||||
return;
|
||||
}
|
||||
OfxRectI bounds = { 0, 0, params_.width(), params_.height() };
|
||||
OfxRectD rod_d = getRegionOfDefinition(time);
|
||||
OfxRectI region_of_definition = { static_cast<int>(std::floor(rod_d.x1)),
|
||||
static_cast<int>(std::floor(rod_d.y1)),
|
||||
static_cast<int>(std::ceil(rod_d.x2)),
|
||||
static_cast<int>(std::ceil(rod_d.y2)) };
|
||||
|
||||
Image *image;
|
||||
if (images_.contains(time)) {
|
||||
image = images_.value(time);
|
||||
image->ensure_allocated_from_params(params_, bounds, region_of_definition,
|
||||
false);
|
||||
} else {
|
||||
prune_images_cache();
|
||||
image = new Image(*this, params_, bounds, region_of_definition, false);
|
||||
image->ensure_allocated_from_params(params_, bounds, region_of_definition,
|
||||
false);
|
||||
images_.insert(time, image);
|
||||
}
|
||||
|
||||
uint8_t *dst = (uint8_t *)image->data();
|
||||
if (!dst) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frame || !frame->data(0)) {
|
||||
std::memset(dst, 0, image->row_bytes() * image->height());
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect NaN/Inf in float input data before passing to CImg.
|
||||
// CImg::blur_bilateral computes (int)round(val / sigma) which becomes
|
||||
// undefined behaviour when val is NaN, leading to out-of-bounds indexing
|
||||
// and SIGSEGV on Apple Silicon (where (int)NaN often evaluates to 0 or
|
||||
// INT_MIN, causing huge offsets into bgrid._data).
|
||||
if (params_.format() == core::PixelFormat::f32) {
|
||||
const float *fptr = reinterpret_cast<const float *>(frame->data(0));
|
||||
int row_floats = frame->linesize(0) / static_cast<int>(sizeof(float));
|
||||
bool has_nan = false;
|
||||
for (int y = 0; y < params_.height() && !has_nan; ++y) {
|
||||
for (int x = 0; x < params_.width() * params_.channel_count();
|
||||
++x) {
|
||||
float v = fptr[y * row_floats + x];
|
||||
if (std::isnan(v) || std::isinf(v)) {
|
||||
qWarning()
|
||||
<< "[PLUGIN] NaN/Inf detected in input frame at pixel ("
|
||||
<< x / params_.channel_count() << "," << y
|
||||
<< ") channel=" << (x % params_.channel_count())
|
||||
<< " value=" << v;
|
||||
has_nan = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (has_nan) {
|
||||
qWarning()
|
||||
<< "[PLUGIN] Filling corrupted input frame with black to avoid CImg crash";
|
||||
std::memset(dst, 0, image->row_bytes() * image->height());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AVFramePtr src_frame = frame;
|
||||
if (frame->format() != expected_fmt || frame->width() != params_.width() ||
|
||||
frame->height() != params_.height()) {
|
||||
if (packed_float_channels(frame->format()) >
|
||||
0) {
|
||||
AVFramePtr converted = convert_packed_float_frame(frame, expected_fmt);
|
||||
if (converted) {
|
||||
src_frame = converted;
|
||||
goto copy_pixels;
|
||||
}
|
||||
}
|
||||
AVFramePtr converted = create_av_frame_ptr();
|
||||
converted->set_format(expected_fmt);
|
||||
converted->set_width(params_.width());
|
||||
converted->set_height(params_.height());
|
||||
if (converted->get_buffer(0) < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
FBScaler *scaler = fb_scaler_create(
|
||||
frame->width(), frame->height(), frame->format(),
|
||||
converted->width(), converted->height(), converted->format(),
|
||||
FB_SCALER_POINT);
|
||||
if (!scaler) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t *src_data[4];
|
||||
int src_linesize[4];
|
||||
uint8_t *dst_data[4];
|
||||
int dst_linesize[4];
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
src_data[i] = frame->data(i);
|
||||
src_linesize[i] = frame->linesize(i);
|
||||
dst_data[i] = converted->data(i);
|
||||
dst_linesize[i] = converted->linesize(i);
|
||||
}
|
||||
fb_scaler_scale_slices(scaler, src_data, src_linesize, frame->height(),
|
||||
dst_data, dst_linesize);
|
||||
fb_scaler_free(&scaler);
|
||||
|
||||
src_frame = converted;
|
||||
}
|
||||
|
||||
copy_pixels:
|
||||
int bytes_per_component = params_.format().byte_count();
|
||||
int bytes_per_row =
|
||||
params_.width() * params_.channel_count() * bytes_per_component;
|
||||
int src_row_bytes = src_frame->linesize(0);
|
||||
int dst_row_bytes = image->row_bytes();
|
||||
int copy_bytes =
|
||||
std::min(bytes_per_row, std::min(src_row_bytes, dst_row_bytes));
|
||||
int copy_height = std::min(image->height(), src_frame->height());
|
||||
|
||||
const uint8_t *src = src_frame->data(0);
|
||||
if (params_.format() == core::PixelFormat::f32) {
|
||||
const float *src_f = reinterpret_cast<const float *>(src);
|
||||
float *dst_f = reinterpret_cast<float *>(dst);
|
||||
int src_stride = src_row_bytes / static_cast<int>(sizeof(float));
|
||||
int dst_stride = dst_row_bytes / static_cast<int>(sizeof(float));
|
||||
int floats_per_row = copy_bytes / static_cast<int>(sizeof(float));
|
||||
bool has_nan = false;
|
||||
for (int y = 0; y < copy_height; ++y) {
|
||||
for (int i = 0; i < floats_per_row; ++i) {
|
||||
float v = src_f[y * src_stride + i];
|
||||
if (std::isnan(v) || std::isinf(v)) {
|
||||
v = 0.0f;
|
||||
has_nan = true;
|
||||
}
|
||||
dst_f[y * dst_stride + i] = v;
|
||||
}
|
||||
}
|
||||
if (has_nan) {
|
||||
qWarning()
|
||||
<< "[PLUGIN] NaN/Inf scrubbed from input frame data during copy";
|
||||
}
|
||||
} else if (dst_row_bytes == src_row_bytes && src_row_bytes == copy_bytes) {
|
||||
std::memcpy(dst, src, copy_bytes * copy_height);
|
||||
} else {
|
||||
for (int y = 0; y < copy_height; ++y) {
|
||||
std::memcpy(dst + y * dst_row_bytes, src + y * src_row_bytes,
|
||||
copy_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void olive::plugin::OliveClipInstance::setOutputTexture(TexturePtr texture,
|
||||
OfxTime time)
|
||||
{
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
if (!texture) {
|
||||
return;
|
||||
}
|
||||
output_textures_.insert(time, texture);
|
||||
#else
|
||||
(void)texture;
|
||||
(void)time;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
OFX::Host::ImageEffect::Texture *
|
||||
olive::plugin::OliveClipInstance::loadTexture(OfxTime time, const char *format,
|
||||
const OfxRectD *optional_bounds)
|
||||
{
|
||||
(void)format;
|
||||
|
||||
TexturePtr gl_texture = nullptr;
|
||||
if (isOutput()) {
|
||||
gl_texture = output_textures_.value(time, nullptr);
|
||||
} else {
|
||||
TexturePtr input = input_textures_.value(time);
|
||||
gl_texture = input ? input : nullptr;
|
||||
}
|
||||
|
||||
if (!gl_texture || gl_texture->is_dummy() || !gl_texture->id().isValid()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
OfxRectD rod_d = getRegionOfDefinition(time);
|
||||
OfxRectI rod = { static_cast<int>(std::floor(rod_d.x1)),
|
||||
static_cast<int>(std::floor(rod_d.y1)),
|
||||
static_cast<int>(std::ceil(rod_d.x2)),
|
||||
static_cast<int>(std::ceil(rod_d.y2)) };
|
||||
OfxRectI bounds = rod;
|
||||
if (optional_bounds) {
|
||||
bounds.x1 = static_cast<int>(std::floor(optional_bounds->x1));
|
||||
bounds.y1 = static_cast<int>(std::floor(optional_bounds->y1));
|
||||
bounds.x2 = static_cast<int>(std::ceil(optional_bounds->x2));
|
||||
bounds.y2 = static_cast<int>(std::ceil(optional_bounds->y2));
|
||||
}
|
||||
bounds.x1 = std::max(bounds.x1, rod.x1);
|
||||
bounds.y1 = std::max(bounds.y1, rod.y1);
|
||||
bounds.x2 = std::min(bounds.x2, rod.x2);
|
||||
bounds.y2 = std::min(bounds.y2, rod.y2);
|
||||
|
||||
const int bytes_per_row = params_.width() * params_.channel_count() *
|
||||
params_.format().byte_count();
|
||||
const std::string &field = getFieldOrder();
|
||||
const std::string unique_id =
|
||||
std::to_string(reinterpret_cast<uintptr_t>(gl_texture.get())) + "_" +
|
||||
std::to_string(static_cast<long long>(time));
|
||||
|
||||
const int texture_id = gl_texture->id().value<GLuint>();
|
||||
OFX::Host::ImageEffect::Texture *texture =
|
||||
new OFX::Host::ImageEffect::Texture(*this, 1.0, 1.0, texture_id,
|
||||
GL_TEXTURE_2D, bounds, rod,
|
||||
bytes_per_row, field, unique_id);
|
||||
texture->addReference();
|
||||
return texture;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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-1.
|
||||
//
|
||||
|
||||
#ifndef OAK_OLIVECLIP_H
|
||||
#define OAK_OLIVECLIP_H
|
||||
#include "image.h"
|
||||
#include "ofxCore.h"
|
||||
#include "ofxhClip.h"
|
||||
#include "render/texture.h"
|
||||
#include "render/videoparams.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <memory>
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
class OliveClipInstance : public OFX::Host::ImageEffect::ClipInstance {
|
||||
public:
|
||||
OliveClipInstance(OFX::Host::ImageEffect::Instance *effect_instance,
|
||||
OFX::Host::ImageEffect::ClipDescriptor &desc,
|
||||
VideoParams ¶ms)
|
||||
: ClipInstance(effect_instance, desc)
|
||||
, params_(params)
|
||||
, defaultRegionOfDefinitions_{ 0, 0, 0, 0 }
|
||||
, name_(desc.getName())
|
||||
{
|
||||
}
|
||||
OFX::Host::ImageEffect::Image *getOutputImage(OfxTime time);
|
||||
|
||||
const std::string &getUnmappedBitDepth() const override;
|
||||
const std::string &getUnmappedComponents() const override;
|
||||
const std::string &getPremult() const override;
|
||||
double getAspectRatio() const override;
|
||||
double getFrameRate() const override;
|
||||
void getFrameRange(double &start_frame, double &end_frame) const override;
|
||||
const std::string &getFieldOrder() const override;
|
||||
bool getConnected() const override;
|
||||
double getUnmappedFrameRate() const override;
|
||||
void getUnmappedFrameRange(double &start_frame,
|
||||
double &end_frame) const override;
|
||||
bool getContinuousSamples() const override;
|
||||
OFX::Host::ImageEffect::Image *
|
||||
getImage(OfxTime time, const OfxRectD *optional_bounds) override;
|
||||
OfxRectD getRegionOfDefinition(OfxTime time) const override;
|
||||
|
||||
void setRegionOfDefinition(OfxRectD region_of_definition, OfxTime time);
|
||||
void setDefaultRegionOfDefinition(OfxRectD region_of_definition);
|
||||
void setParams(const VideoParams ¶ms);
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
OFX::Host::ImageEffect::Texture *
|
||||
loadTexture(OfxTime time, const char *format,
|
||||
const OfxRectD *optional_bounds) override;
|
||||
#endif
|
||||
|
||||
void setInputTexture(TexturePtr texture, OfxTime time,
|
||||
bool readback_cpu = true);
|
||||
void setOutputTexture(TexturePtr texture, OfxTime time);
|
||||
|
||||
// Get the plugin-preferred VideoParams based on base class _pixelDepth/_components
|
||||
VideoParams getPluginPreferredParams() const;
|
||||
|
||||
// Prune old entries from the images_ cache to prevent unbounded growth.
|
||||
// Output clip images are not pruned (they are typically single-frame).
|
||||
void prune_images_cache();
|
||||
|
||||
static constexpr int k_max_input_image_cache = 8;
|
||||
|
||||
private:
|
||||
VideoParams params_;
|
||||
|
||||
QMap<OfxTime, OfxRectD> regionOfDefinitions_;
|
||||
|
||||
OfxRectD defaultRegionOfDefinitions_;
|
||||
|
||||
std::string name_;
|
||||
QMap<OfxTime, Image *> images_;
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
QMap<OfxTime, TexturePtr> input_textures_;
|
||||
QMap<OfxTime, TexturePtr> output_textures_;
|
||||
#endif
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif //OAK_OLIVECLIP_H
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* 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 "node/project.h"
|
||||
#include "ofxhImageEffect.h"
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <ofxhPluginCache.h>
|
||||
#include <ofxhBinary.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include "olivehost.h"
|
||||
|
||||
#include "oliveplugininstance.h"
|
||||
#include "common/current.h"
|
||||
#include "ofxMessage.h"
|
||||
#include "version.h"
|
||||
#include <QMessageBox>
|
||||
using namespace OFX::Host;
|
||||
using namespace olive::plugin;
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
class PluginNode;
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
void add_plugin_path(OFX::Host::PluginCache *cache, const QString &path,
|
||||
bool recurse = true)
|
||||
{
|
||||
if (!cache || path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
QDir dir(path);
|
||||
if (!dir.exists()) {
|
||||
return;
|
||||
}
|
||||
cache->addFileToPath(dir.canonicalPath().toStdString(), recurse);
|
||||
}
|
||||
|
||||
void add_plugin_paths_from_env(OFX::Host::PluginCache *cache, const char *env_var)
|
||||
{
|
||||
QString raw = qEnvironmentVariable(env_var);
|
||||
if (raw.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const QChar separator = QDir::listSeparator();
|
||||
const QStringList paths = raw.split(separator, Qt::SkipEmptyParts);
|
||||
for (const QString &path : paths) {
|
||||
add_plugin_path(cache, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void olive::plugin::load_plugins(QString path)
|
||||
{
|
||||
std::shared_ptr<OliveHost> host = Current::getInstance().plugin_host();
|
||||
std::shared_ptr<ImageEffect::PluginCache> image_effect_plugin_cache =
|
||||
Current::getInstance().plugin_cache();
|
||||
|
||||
if (!host || !image_effect_plugin_cache) {
|
||||
host = std::make_shared<OliveHost>();
|
||||
Current::getInstance().setPluginHost(host);
|
||||
|
||||
image_effect_plugin_cache =
|
||||
std::make_shared<ImageEffect::PluginCache>(*host);
|
||||
Current::getInstance().setPluginCache(image_effect_plugin_cache);
|
||||
|
||||
image_effect_plugin_cache->registerInCache(
|
||||
*OFX::Host::PluginCache::getPluginCache());
|
||||
}
|
||||
OFX::Host::PluginCache *cache = OFX::Host::PluginCache::getPluginCache();
|
||||
cache->setPluginHostPath("Olive");
|
||||
|
||||
const QString home_path = QDir::homePath();
|
||||
add_plugin_path(cache, QDir(home_path).filePath(".OFX/Plugins"));
|
||||
add_plugin_path(cache, QDir(home_path).filePath(".local/share/OFX/Plugins"));
|
||||
add_plugin_path(cache,
|
||||
QDir(home_path).filePath(".local/share/olive/ofx/Plugins"));
|
||||
|
||||
const QString app_dir = QCoreApplication::applicationDirPath();
|
||||
add_plugin_path(cache, QDir(app_dir).filePath("../OFX/Plugins"));
|
||||
add_plugin_path(cache, QDir(app_dir).filePath("../share/olive/ofx/Plugins"));
|
||||
add_plugin_path(cache, QDir(app_dir).filePath("../lib/olive/ofx/Plugins"));
|
||||
|
||||
add_plugin_paths_from_env(cache, "OLIVE_OFX_PLUGIN_PATH");
|
||||
add_plugin_paths_from_env(cache, "OLIVE_PLUGIN_PATH");
|
||||
|
||||
if (!path.isEmpty()) {
|
||||
add_plugin_path(cache, path, true);
|
||||
}
|
||||
cache->scanPluginFiles();
|
||||
}
|
||||
OliveHost::OliveHost()
|
||||
{
|
||||
// Identify the host to plugins; HostSupport seeds these with "UNKNOWN".
|
||||
_properties.setStringProperty(kOfxPropName, "Oak Video Editor");
|
||||
_properties.setStringProperty(kOfxPropLabel, "Oak Video Editor");
|
||||
_properties.setStringProperty(kOfxPropVersionLabel,
|
||||
olive::k_app_version.toStdString());
|
||||
|
||||
// Numeric version for plugins that query kOfxPropVersion directly.
|
||||
const QStringList version_parts =
|
||||
olive::k_app_version.section(QLatin1Char('-'), 0, 0)
|
||||
.split(QLatin1Char('.'));
|
||||
_properties.setIntProperty(kOfxPropVersion,
|
||||
version_parts.value(0).toInt(), 0);
|
||||
_properties.setIntProperty(kOfxPropVersion,
|
||||
version_parts.value(1).toInt(), 1);
|
||||
_properties.setIntProperty(kOfxPropVersion,
|
||||
version_parts.value(2).toInt(), 2);
|
||||
}
|
||||
|
||||
OliveHost::~OliveHost()
|
||||
{
|
||||
}
|
||||
|
||||
void OliveHost::destroy_instance(OFX::Host::ImageEffect::Instance *instance)
|
||||
{
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
for (auto it = instances_.begin(); it != instances_.end(); ++it) {
|
||||
if (it->get() == instance) {
|
||||
instances_.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::shared_ptr<OFX::Host::ImageEffect::Descriptor>
|
||||
OliveHost::makeDescriptor(ImageEffect::ImageEffectPlugin *plugin)
|
||||
{
|
||||
std::shared_ptr<OFX::Host::ImageEffect::Descriptor> desc =
|
||||
std::make_shared<ImageEffect::Descriptor>(plugin);
|
||||
descriptors_.append(std::shared_ptr<ImageEffect::Descriptor>(desc));
|
||||
return desc;
|
||||
}
|
||||
std::shared_ptr<OFX::Host::ImageEffect::Descriptor>
|
||||
OliveHost::makeDescriptor(const ImageEffect::Descriptor &root_context,
|
||||
ImageEffect::ImageEffectPlugin *plugin)
|
||||
{
|
||||
std::shared_ptr<OFX::Host::ImageEffect::Descriptor> desc =
|
||||
std::make_shared<ImageEffect::Descriptor>(root_context, plugin);
|
||||
descriptors_.append(std::shared_ptr<ImageEffect::Descriptor>(desc));
|
||||
return desc;
|
||||
}
|
||||
std::shared_ptr<OFX::Host::ImageEffect::Descriptor>
|
||||
OliveHost::makeDescriptor(const std::string &bundle_path,
|
||||
ImageEffect::ImageEffectPlugin *plugin)
|
||||
{
|
||||
std::shared_ptr<OFX::Host::ImageEffect::Descriptor> desc =
|
||||
std::make_shared<ImageEffect::Descriptor>(bundle_path, plugin);
|
||||
descriptors_.append(std::shared_ptr<ImageEffect::Descriptor>(desc));
|
||||
return desc;
|
||||
}
|
||||
|
||||
ImageEffect::Instance *
|
||||
OliveHost::newInstance(void *client_data, ImageEffect::ImageEffectPlugin *plugin,
|
||||
ImageEffect::Descriptor &desc,
|
||||
const std::string &context)
|
||||
{
|
||||
auto *instance = new OlivePluginInstance(
|
||||
plugin, desc, context, Current::getInstance().interactive());
|
||||
if (client_data) {
|
||||
auto *node = static_cast<PluginNode *>(client_data);
|
||||
instance->setNode(
|
||||
std::shared_ptr<PluginNode>(node, [](PluginNode *) {}));
|
||||
}
|
||||
instances_.append(std::shared_ptr<OlivePluginInstance>(instance));
|
||||
return instance;
|
||||
};
|
||||
OfxStatus olive::plugin::OliveHost::vmessage(const char *type, const char *id,
|
||||
const char *format, va_list args)
|
||||
{
|
||||
if (!type || !format) {
|
||||
return kOfxStatFailed;
|
||||
}
|
||||
|
||||
char buffer[1024];
|
||||
buffer[0] = '\0';
|
||||
vsnprintf(buffer, sizeof(buffer), format, args);
|
||||
QString message(buffer);
|
||||
|
||||
auto *app = qobject_cast<QApplication *>(QCoreApplication::instance());
|
||||
// A modal dialog would hang a headless (offscreen) session, so log to
|
||||
// stderr instead of showing one.
|
||||
if (!app || QGuiApplication::platformName() == QLatin1String("offscreen")) {
|
||||
qWarning().noquote() << "OFX message:" << type << message;
|
||||
if (strcmp(type, kOfxMessageQuestion) == 0) {
|
||||
return kOfxStatReplyNo;
|
||||
}
|
||||
return kOfxStatOK;
|
||||
}
|
||||
|
||||
if (strcmp(type, kOfxMessageQuestion) == 0) {
|
||||
auto ret = QMessageBox::question(nullptr, "", message, QMessageBox::Ok,
|
||||
QMessageBox::Cancel);
|
||||
return (ret == QMessageBox::Ok) ? kOfxStatReplyYes : kOfxStatReplyNo;
|
||||
}
|
||||
|
||||
if (strcmp(type, kOfxMessageError) == 0) {
|
||||
QMessageBox::critical(nullptr, "", message);
|
||||
} else if (strcmp(type, kOfxMessageWarning) == 0) {
|
||||
QMessageBox::warning(nullptr, "", message);
|
||||
} else {
|
||||
QMessageBox::information(nullptr, "", message);
|
||||
}
|
||||
|
||||
return kOfxStatOK;
|
||||
}
|
||||
// TODO: Persistent messages shouldn't use pop-up window.
|
||||
OfxStatus olive::plugin::OliveHost::setPersistentMessage(const char *type,
|
||||
const char *id,
|
||||
const char *format,
|
||||
va_list args)
|
||||
{
|
||||
if (!type || !format) {
|
||||
return kOfxStatFailed;
|
||||
}
|
||||
|
||||
char buffer[1024];
|
||||
buffer[0] = '\0';
|
||||
vsnprintf(buffer, sizeof(buffer), format, args);
|
||||
QString message(buffer);
|
||||
|
||||
// A modal dialog would hang a headless (offscreen) session, so log to
|
||||
// stderr instead of showing one.
|
||||
const bool headless =
|
||||
QGuiApplication::platformName() == QLatin1String("offscreen");
|
||||
|
||||
if (strcmp(type, kOfxMessageError) == 0) {
|
||||
persistent_messages_.append({ HostMessageType::error, message });
|
||||
if (headless) {
|
||||
qWarning().noquote() << "OFX error:" << message;
|
||||
} else {
|
||||
QMessageBox::critical(nullptr, "", message);
|
||||
}
|
||||
} else if (strcmp(type, kOfxMessageWarning) == 0) {
|
||||
persistent_messages_.append({ HostMessageType::warning, message });
|
||||
if (headless) {
|
||||
qWarning().noquote() << "OFX warning:" << message;
|
||||
} else {
|
||||
QMessageBox::warning(nullptr, "", message);
|
||||
}
|
||||
} else if (strcmp(type, kOfxMessageMessage) == 0) {
|
||||
persistent_messages_.append({ HostMessageType::message, message });
|
||||
if (headless) {
|
||||
qWarning().noquote() << "OFX message:" << message;
|
||||
} else {
|
||||
QMessageBox::information(nullptr, "", message);
|
||||
}
|
||||
} else {
|
||||
return kOfxStatFailed;
|
||||
}
|
||||
|
||||
return kOfxStatOK;
|
||||
}
|
||||
|
||||
OfxStatus olive::plugin::OliveHost::clearPersistentMessage()
|
||||
{
|
||||
persistent_messages_.clear();
|
||||
return kOfxStatOK;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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_OLIVE_HOST_H
|
||||
#define OAK_OLIVE_HOST_H
|
||||
#include "node/plugins/plugin.h"
|
||||
#include "ofxhHost.h"
|
||||
#include "ofxhImageEffectAPI.h"
|
||||
#include "ofxCore.h"
|
||||
#include "ofxhImageEffect.h"
|
||||
|
||||
#include <QVariant>
|
||||
#include <cstdint>
|
||||
#include <QString>
|
||||
#include <QMap>
|
||||
#include <any>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <qlist.h>
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
enum class HostMessageType { error, warning, message };
|
||||
struct HostPersistentMessage {
|
||||
HostMessageType type;
|
||||
QString message;
|
||||
};
|
||||
|
||||
void load_plugins(QString path);
|
||||
class OliveHost : public OFX::Host::ImageEffect::Host {
|
||||
public:
|
||||
OliveHost();
|
||||
~OliveHost() override;
|
||||
void destroy_instance(OFX::Host::ImageEffect::Instance *instance);
|
||||
|
||||
bool pluginSupported(OFX::Host::ImageEffect::ImageEffectPlugin *plugin,
|
||||
std::string &reason) const override
|
||||
{
|
||||
if (!plugin) {
|
||||
reason = "null plugin";
|
||||
return false;
|
||||
}
|
||||
if (plugin->getContexts().empty()) {
|
||||
reason = "no supported contexts (describe failed)";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
OFX::Host::ImageEffect::Instance *
|
||||
newInstance(void *client_data,
|
||||
OFX::Host::ImageEffect::ImageEffectPlugin *plugin,
|
||||
OFX::Host::ImageEffect::Descriptor &desc,
|
||||
const std::string &context) override;
|
||||
|
||||
std::shared_ptr<OFX::Host::ImageEffect::Descriptor>
|
||||
makeDescriptor(OFX::Host::ImageEffect::ImageEffectPlugin *plugin) override;
|
||||
|
||||
std::shared_ptr<OFX::Host::ImageEffect::Descriptor>
|
||||
makeDescriptor(const OFX::Host::ImageEffect::Descriptor &root_context,
|
||||
OFX::Host::ImageEffect::ImageEffectPlugin *plugin) override;
|
||||
|
||||
std::shared_ptr<OFX::Host::ImageEffect::Descriptor>
|
||||
makeDescriptor(const std::string &bundle_path,
|
||||
OFX::Host::ImageEffect::ImageEffectPlugin *plugin) override;
|
||||
/// vmessage
|
||||
virtual OfxStatus vmessage(const char *type, const char *id,
|
||||
const char *format, va_list args);
|
||||
|
||||
/// vmessage
|
||||
virtual OfxStatus setPersistentMessage(const char *type, const char *id,
|
||||
const char *format, va_list args);
|
||||
/// vmessage
|
||||
virtual OfxStatus clearPersistentMessage();
|
||||
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
/// @see OfxImageEffectOpenGLRenderSuiteV1.flushResources()
|
||||
virtual OfxStatus flushOpenGLResources() const
|
||||
{
|
||||
return kOfxStatFailed;
|
||||
};
|
||||
#endif
|
||||
private:
|
||||
QList<std::shared_ptr<OFX::Host::ImageEffect::Descriptor>> descriptors_;
|
||||
QList<std::shared_ptr<OFX::Host::ImageEffect::Instance>> instances_;
|
||||
QList<HostPersistentMessage> persistent_messages_;
|
||||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,599 @@
|
||||
/*
|
||||
* 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 "oliveplugininstance.h"
|
||||
|
||||
#include "oliveclip.h"
|
||||
#include "ofxGPURender.h"
|
||||
#include "ofxCore.h"
|
||||
#include "ofxMessage.h"
|
||||
#include "common/current.h"
|
||||
#include "coreengine.h"
|
||||
#include "pluginprogressreporter.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QMetaObject>
|
||||
#include <QThread>
|
||||
#include <QtGlobal>
|
||||
#include <string.h>
|
||||
#include <QString>
|
||||
#include "paraminstance.h"
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const std::string k_image_field_none_str(kOfxImageFieldNone);
|
||||
const std::string k_image_field_upper_str(kOfxImageFieldUpper);
|
||||
const std::string k_image_field_lower_str(kOfxImageFieldLower);
|
||||
|
||||
QString format_ofx_message(const char *format, va_list args)
|
||||
{
|
||||
char buffer[1024];
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
const int needed = vsnprintf(buffer, sizeof(buffer), format, args_copy);
|
||||
va_end(args_copy);
|
||||
if (needed < 0) {
|
||||
return QString();
|
||||
}
|
||||
if (needed < static_cast<int>(sizeof(buffer))) {
|
||||
return QString::fromUtf8(buffer);
|
||||
}
|
||||
QByteArray dynamic_buffer(needed + 1, 0);
|
||||
const int written =
|
||||
vsnprintf(dynamic_buffer.data(), dynamic_buffer.size(), format, args);
|
||||
if (written < 0) {
|
||||
return QString();
|
||||
}
|
||||
return QString::fromUtf8(dynamic_buffer.constData());
|
||||
}
|
||||
|
||||
const std::string &field_order_for_params(const VideoParams ¶ms)
|
||||
{
|
||||
switch (params.interlacing()) {
|
||||
case VideoParams::k_interlace_none:
|
||||
return k_image_field_none_str;
|
||||
case VideoParams::k_interlaced_top_first:
|
||||
return k_image_field_upper_str;
|
||||
case VideoParams::k_interlaced_bottom_first:
|
||||
return k_image_field_lower_str;
|
||||
}
|
||||
return k_image_field_none_str;
|
||||
}
|
||||
|
||||
class DeferredRedoCommand : public UndoCommand {
|
||||
public:
|
||||
explicit DeferredRedoCommand(UndoCommand *inner)
|
||||
: inner_(inner)
|
||||
{
|
||||
}
|
||||
|
||||
~DeferredRedoCommand() override
|
||||
{
|
||||
delete inner_;
|
||||
}
|
||||
|
||||
Project *get_relevant_project() const override
|
||||
{
|
||||
return inner_ ? inner_->get_relevant_project() : nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
void redo() override
|
||||
{
|
||||
if (skip_first_redo_) {
|
||||
skip_first_redo_ = false;
|
||||
return;
|
||||
}
|
||||
if (inner_) {
|
||||
inner_->redo_now();
|
||||
}
|
||||
}
|
||||
|
||||
void undo() override
|
||||
{
|
||||
if (inner_) {
|
||||
inner_->undo_now();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
UndoCommand *inner_ = nullptr;
|
||||
bool skip_first_redo_ = true;
|
||||
};
|
||||
|
||||
ActiveViewerProvider active_viewer_provider_;
|
||||
|
||||
ViewerOutput *get_active_viewer_output()
|
||||
{
|
||||
return active_viewer_provider_ ? active_viewer_provider_() : nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void set_active_viewer_provider(ActiveViewerProvider provider)
|
||||
{
|
||||
active_viewer_provider_ = std::move(provider);
|
||||
}
|
||||
|
||||
const std::string &OlivePluginInstance::getDefaultOutputFielding() const
|
||||
{
|
||||
return field_order_for_params(params_);
|
||||
}
|
||||
|
||||
void OlivePluginInstance::setNode(std::shared_ptr<PluginNode> node)
|
||||
{
|
||||
node_ = node;
|
||||
for (const auto &entry : getParams()) {
|
||||
if (!entry.second) {
|
||||
continue;
|
||||
}
|
||||
if (auto *bound = dynamic_cast<NodeBoundParam *>(entry.second)) {
|
||||
bound->set_node(node_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OfxStatus OlivePluginInstance::vmessage(const char *type, const char *id,
|
||||
const char *format, va_list args)
|
||||
{
|
||||
const QString message = format_ofx_message(format, args);
|
||||
if (message.isEmpty()) {
|
||||
return kOfxStatFailed;
|
||||
}
|
||||
|
||||
const bool is_question =
|
||||
strncmp(type, kOfxMessageQuestion, strlen(kOfxMessageQuestion)) == 0;
|
||||
OfxStatus result = kOfxStatOK;
|
||||
auto show_message = [&]() {
|
||||
if (is_question) {
|
||||
const auto ret = QMessageBox::question(
|
||||
nullptr, "", message, QMessageBox::Ok, QMessageBox::Cancel);
|
||||
result = (ret == QMessageBox::Ok) ? kOfxStatReplyYes :
|
||||
kOfxStatReplyNo;
|
||||
} else {
|
||||
QMessageBox::information(nullptr, "", message);
|
||||
result = kOfxStatOK;
|
||||
}
|
||||
};
|
||||
|
||||
if (is_gui_thread()) {
|
||||
show_message();
|
||||
} else if (auto *app = QCoreApplication::instance()) {
|
||||
if (is_question) {
|
||||
QMetaObject::invokeMethod(app, show_message,
|
||||
Qt::BlockingQueuedConnection);
|
||||
} else {
|
||||
QMetaObject::invokeMethod(app, show_message, Qt::QueuedConnection);
|
||||
}
|
||||
} else if (is_question) {
|
||||
result = kOfxStatReplyNo;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
OfxStatus OlivePluginInstance::setPersistentMessage(const char *type,
|
||||
const char *id,
|
||||
const char *format,
|
||||
va_list args)
|
||||
{
|
||||
const QString message = format_ofx_message(format, args);
|
||||
if (message.isEmpty()) {
|
||||
return kOfxStatFailed;
|
||||
}
|
||||
|
||||
ErrorType error_type;
|
||||
// If This is a error message
|
||||
if (strncmp(type, kOfxMessageError, strlen(kOfxMessageError)) == 0) {
|
||||
error_type = ErrorType::error;
|
||||
}
|
||||
// A warning
|
||||
else if (strncmp(type, kOfxMessageWarning, strlen(kOfxMessageWarning)) ==
|
||||
0) {
|
||||
error_type = ErrorType::warning;
|
||||
}
|
||||
// A simple information
|
||||
else if (strncmp(type, kOfxMessageMessage, strlen(kOfxMessageMessage)) ==
|
||||
0) {
|
||||
error_type = ErrorType::message;
|
||||
} else {
|
||||
return kOfxStatFailed;
|
||||
}
|
||||
|
||||
auto update_ui = [this, error_type, message]() {
|
||||
persistentErrors_.append({ error_type, message });
|
||||
switch (error_type) {
|
||||
case ErrorType::error:
|
||||
QMessageBox::critical(nullptr, "", message);
|
||||
break;
|
||||
case ErrorType::warning:
|
||||
QMessageBox::warning(nullptr, "", message);
|
||||
break;
|
||||
case ErrorType::message:
|
||||
QMessageBox::information(nullptr, "", message);
|
||||
break;
|
||||
}
|
||||
if (node_) {
|
||||
emit node_->message_count_changed();
|
||||
}
|
||||
};
|
||||
|
||||
if (is_gui_thread()) {
|
||||
update_ui();
|
||||
} else if (auto *app = QCoreApplication::instance()) {
|
||||
QMetaObject::invokeMethod(app, update_ui, Qt::QueuedConnection);
|
||||
}
|
||||
return kOfxStatOK;
|
||||
}
|
||||
OfxStatus OlivePluginInstance::clearPersistentMessage()
|
||||
{
|
||||
auto clear_ui = [this]() {
|
||||
persistentErrors_.clear();
|
||||
// TODO: tell the shell to remove message.
|
||||
if (node_) {
|
||||
emit node_->message_count_changed();
|
||||
}
|
||||
};
|
||||
if (is_gui_thread()) {
|
||||
clear_ui();
|
||||
} else if (auto *app = QCoreApplication::instance()) {
|
||||
QMetaObject::invokeMethod(app, clear_ui, Qt::QueuedConnection);
|
||||
}
|
||||
return kOfxStatOK;
|
||||
}
|
||||
void OlivePluginInstance::getProjectSize(double &x_size, double &y_size) const
|
||||
{
|
||||
double par = params_.pixel_aspect_ratio().to_double();
|
||||
x_size = params_.width() * par;
|
||||
y_size = params_.height();
|
||||
}
|
||||
void OlivePluginInstance::getProjectOffset(double &x_offset,
|
||||
double &y_offset) const
|
||||
{
|
||||
double par = params_.pixel_aspect_ratio().to_double();
|
||||
x_offset = params_.x() * par;
|
||||
y_offset = params_.y();
|
||||
}
|
||||
void OlivePluginInstance::getProjectExtent(double &x_size, double &y_size) const
|
||||
{
|
||||
double par = params_.pixel_aspect_ratio().to_double();
|
||||
x_size = params_.width() * par;
|
||||
y_size = params_.height();
|
||||
}
|
||||
double OlivePluginInstance::getProjectPixelAspectRatio() const
|
||||
{
|
||||
double par = params_.pixel_aspect_ratio().to_double();
|
||||
if (par == 0.0) {
|
||||
return 1.0; // default PAR when not explicitly set
|
||||
}
|
||||
return par;
|
||||
}
|
||||
double OlivePluginInstance::getFrameRate() const
|
||||
{
|
||||
return params_.frame_rate().to_double();
|
||||
}
|
||||
|
||||
double OlivePluginInstance::getEffectDuration() const
|
||||
{
|
||||
// Return a default duration value
|
||||
return 100.0;
|
||||
}
|
||||
|
||||
double OlivePluginInstance::getFrameRecursive() const
|
||||
{
|
||||
// Return current frame (this would typically be set by the host during rendering)
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void OlivePluginInstance::getRenderScaleRecursive(double &x, double &y) const
|
||||
{
|
||||
// Return default render scale (1.0, 1.0)
|
||||
x = 1.0;
|
||||
y = 1.0;
|
||||
}
|
||||
OFX::Host::Param::Instance *
|
||||
OlivePluginInstance::newParam(const std::string &name,
|
||||
OFX::Host::Param::Descriptor &desc)
|
||||
{
|
||||
const std::string &type = desc.getType();
|
||||
|
||||
if (type == kOfxParamTypeInteger) {
|
||||
return new IntegerInstance(node_, desc, this);
|
||||
} else if (type == kOfxParamTypeDouble) {
|
||||
return new DoubleInstance(node_, name, desc, this);
|
||||
} else if (type == kOfxParamTypeBoolean) {
|
||||
return new BooleanInstance(node_, name, desc, this);
|
||||
} else if (type == kOfxParamTypeChoice) {
|
||||
return new ChoiceInstance(node_, name, desc, this);
|
||||
} else if (type == kOfxParamTypeString) {
|
||||
return new StringInstance(node_, name, desc, this);
|
||||
} else if (type == kOfxParamTypeRGBA) {
|
||||
return new RGBAInstance(node_, name, desc, this);
|
||||
} else if (type == kOfxParamTypeRGB) {
|
||||
return new RGBInstance(node_, name, desc, this);
|
||||
} else if (type == kOfxParamTypeDouble2D) {
|
||||
return new Double2DInstance(node_, name, desc, this);
|
||||
} else if (type == kOfxParamTypeInteger2D) {
|
||||
return new Integer2DInstance(node_, name, desc, this);
|
||||
} else if (type == kOfxParamTypeDouble3D) {
|
||||
return new Double3DInstance(node_, name, desc, this);
|
||||
} else if (type == kOfxParamTypeInteger3D) {
|
||||
return new Integer3DInstance(node_, name, desc, this);
|
||||
} else if (type == kOfxParamTypeCustom || type == kOfxParamTypeBytes) {
|
||||
return new CustomInstance(node_, name, desc, this);
|
||||
} else if (type == kOfxParamTypeGroup) {
|
||||
return new GroupInstance(desc, this);
|
||||
} else if (type == kOfxParamTypePage) {
|
||||
return new PageInstance(desc, this);
|
||||
} else if (type == kOfxParamTypePushButton) {
|
||||
return new PushbuttonInstance(node_, name, desc, this);
|
||||
}
|
||||
|
||||
return nullptr; // 未实现的类型
|
||||
}
|
||||
OfxStatus OlivePluginInstance::editBegin(const std::string &name)
|
||||
{
|
||||
edit_depth_++;
|
||||
if (edit_depth_ == 1) {
|
||||
edit_command_ = nullptr;
|
||||
edit_label_.clear();
|
||||
edit_first_label_.clear();
|
||||
edit_param_count_ = 0;
|
||||
if (!name.empty()) {
|
||||
edit_first_label_ =
|
||||
QCoreApplication::translate("OlivePluginInstance", "Change %1")
|
||||
.arg(QString::fromStdString(name));
|
||||
}
|
||||
}
|
||||
return kOfxStatOK;
|
||||
}
|
||||
OfxStatus OlivePluginInstance::editEnd()
|
||||
{
|
||||
if (edit_depth_ > 0) {
|
||||
edit_depth_--;
|
||||
}
|
||||
if (edit_depth_ == 0 && edit_command_) {
|
||||
QString label = edit_label_;
|
||||
if (label.isEmpty()) {
|
||||
if (edit_param_count_ <= 1 && !edit_first_label_.isEmpty()) {
|
||||
label = edit_first_label_;
|
||||
} else if (edit_param_count_ > 1 && !edit_first_label_.isEmpty()) {
|
||||
label = QCoreApplication::translate("OlivePluginInstance",
|
||||
"%1 (+%2)")
|
||||
.arg(edit_first_label_)
|
||||
.arg(edit_param_count_ - 1);
|
||||
} else {
|
||||
label = QCoreApplication::translate("OlivePluginInstance",
|
||||
"Edit Parameters");
|
||||
}
|
||||
}
|
||||
EngineCore::instance()->undo_stack()->push(edit_command_, label);
|
||||
edit_command_ = nullptr;
|
||||
edit_label_.clear();
|
||||
edit_first_label_.clear();
|
||||
edit_param_count_ = 0;
|
||||
}
|
||||
return kOfxStatOK;
|
||||
}
|
||||
|
||||
void OlivePluginInstance::submit_undo_command(UndoCommand *command,
|
||||
const QString &label)
|
||||
{
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (edit_depth_ > 0) {
|
||||
if (!edit_command_) {
|
||||
edit_command_ = new MultiUndoCommand();
|
||||
}
|
||||
edit_param_count_++;
|
||||
if (!label.isEmpty() && edit_first_label_.isEmpty()) {
|
||||
edit_first_label_ = label;
|
||||
}
|
||||
|
||||
command->redo_now();
|
||||
edit_command_->add_child(new DeferredRedoCommand(command));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_gui_thread()) {
|
||||
command->redo_now();
|
||||
delete command;
|
||||
return;
|
||||
}
|
||||
|
||||
EngineCore::instance()->undo_stack()->push(command, label);
|
||||
}
|
||||
|
||||
void OlivePluginInstance::progressStart(const std::string &message,
|
||||
const std::string &messageid)
|
||||
{
|
||||
(void)messageid;
|
||||
progress_cancelled_ = false;
|
||||
progress_active_ = true;
|
||||
|
||||
auto *app = qobject_cast<QApplication *>(QCoreApplication::instance());
|
||||
if (!app) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress_reporter_) {
|
||||
progress_reporter_->close();
|
||||
progress_reporter_->deleteLater();
|
||||
}
|
||||
|
||||
QString dialog_message = message.empty() ? QStringLiteral("Processing...") :
|
||||
QString::fromStdString(message);
|
||||
|
||||
progress_reporter_ = create_plugin_progress_reporter(
|
||||
dialog_message, QStringLiteral("OpenFX"));
|
||||
QObject::connect(progress_reporter_, &PluginProgressReporter::cancelled,
|
||||
progress_reporter_,
|
||||
[this]() { progress_cancelled_ = true; });
|
||||
progress_reporter_->show();
|
||||
}
|
||||
|
||||
void OlivePluginInstance::progressEnd()
|
||||
{
|
||||
progress_active_ = false;
|
||||
progress_cancelled_ = false;
|
||||
|
||||
if (progress_reporter_) {
|
||||
progress_reporter_->close();
|
||||
progress_reporter_->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
bool OlivePluginInstance::progressUpdate(double t)
|
||||
{
|
||||
if (!progress_active_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (progress_reporter_) {
|
||||
double clamped = qBound(0.0, t, 1.0);
|
||||
progress_reporter_->set_progress(clamped);
|
||||
}
|
||||
|
||||
return !progress_cancelled_;
|
||||
}
|
||||
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
OfxStatus OlivePluginInstance::contextAttachedAction()
|
||||
{
|
||||
if (!open_gl_enabled_) {
|
||||
return kOfxStatReplyDefault;
|
||||
}
|
||||
return kOfxStatOK;
|
||||
}
|
||||
|
||||
OfxStatus OlivePluginInstance::contextDetachedAction()
|
||||
{
|
||||
if (!open_gl_enabled_) {
|
||||
return kOfxStatReplyDefault;
|
||||
}
|
||||
return kOfxStatOK;
|
||||
}
|
||||
#endif
|
||||
|
||||
double OlivePluginInstance::timeLineGetTime()
|
||||
{
|
||||
if (ViewerOutput *viewer = get_active_viewer_output()) {
|
||||
return viewer->get_playhead().to_double();
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void OlivePluginInstance::timeLineGotoTime(double t)
|
||||
{
|
||||
if (ViewerOutput *viewer = get_active_viewer_output()) {
|
||||
viewer->set_playhead(olive::core::Rational::from_double(t));
|
||||
}
|
||||
}
|
||||
|
||||
void OlivePluginInstance::timeLineGetBounds(double &t1, double &t2)
|
||||
{
|
||||
if (ViewerOutput *viewer = get_active_viewer_output()) {
|
||||
t1 = 0.0;
|
||||
t2 = viewer->get_length().to_double();
|
||||
return;
|
||||
}
|
||||
|
||||
t1 = 0.0;
|
||||
t2 = 0.0;
|
||||
}
|
||||
|
||||
void OlivePluginInstance::setCustomInArgs(const std::string &action,
|
||||
OFX::Host::Property::Set &in_args)
|
||||
{
|
||||
if (action == kOfxImageEffectActionRender ||
|
||||
action == kOfxImageEffectActionBeginSequenceRender ||
|
||||
action == kOfxImageEffectActionEndSequenceRender) {
|
||||
in_args.setIntProperty(kOfxImageEffectPropOpenGLEnabled,
|
||||
open_gl_enabled_ ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
OFX::Host::ImageEffect::ClipInstance *OlivePluginInstance::newClipInstance(
|
||||
OFX::Host::ImageEffect::Instance *plugin,
|
||||
OFX::Host::ImageEffect::ClipDescriptor *descriptor, int index)
|
||||
{
|
||||
// Create a new clip instance
|
||||
OliveClipInstance *clip_instance =
|
||||
new OliveClipInstance(plugin, *descriptor, params_);
|
||||
|
||||
// Initialize base class clip properties from VideoParams so that
|
||||
// setupClipPreferencesArgs and plugin constructors (which may fetch
|
||||
// clips and query their properties before getClipPreferences is called)
|
||||
// have valid defaults instead of kOfxImageComponentNone / kOfxBitDepthNone.
|
||||
std::string depth = kOfxBitDepthFloat; // host default
|
||||
std::string comp = kOfxImageComponentRGBA; // host default
|
||||
|
||||
switch (params_.format()) {
|
||||
case core::PixelFormat::u8:
|
||||
depth = kOfxBitDepthByte;
|
||||
break;
|
||||
case core::PixelFormat::u16:
|
||||
depth = kOfxBitDepthShort;
|
||||
break;
|
||||
case core::PixelFormat::f16:
|
||||
depth = kOfxBitDepthHalf;
|
||||
break;
|
||||
case core::PixelFormat::f32:
|
||||
depth = kOfxBitDepthFloat;
|
||||
break;
|
||||
default:
|
||||
break; // keep F32 default
|
||||
}
|
||||
|
||||
switch (params_.channel_count()) {
|
||||
case 1:
|
||||
comp = kOfxImageComponentAlpha;
|
||||
break;
|
||||
case 3:
|
||||
comp = kOfxImageComponentRGB;
|
||||
break;
|
||||
case 4:
|
||||
comp = kOfxImageComponentRGBA;
|
||||
break;
|
||||
default:
|
||||
break; // keep RGBA default
|
||||
}
|
||||
|
||||
clip_instance->setPixelDepth(depth);
|
||||
clip_instance->setComponents(comp);
|
||||
|
||||
return clip_instance;
|
||||
}
|
||||
|
||||
OlivePluginInstance::~OlivePluginInstance()
|
||||
{
|
||||
if (!QCoreApplication::instance() ||
|
||||
qEnvironmentVariableIsSet("OAK_OFX_ITEST")) {
|
||||
_created = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* 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_OLIVE_INSTANCE_H
|
||||
#define OAK_OLIVE_INSTANCE_H
|
||||
#include "ofxCore.h"
|
||||
#include "ofxImageEffect.h"
|
||||
#include <QString>
|
||||
#include "ofxhImageEffect.h"
|
||||
#include "node/plugins/plugin.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "undo/undocommand.h"
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
#include <QCoreApplication>
|
||||
#include <QPointer>
|
||||
#include <QThread>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qlist.h>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
inline bool is_gui_thread()
|
||||
{
|
||||
if (auto *app = QCoreApplication::instance()) {
|
||||
return QThread::currentThread() == app->thread();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
class ViewerOutput;
|
||||
namespace plugin
|
||||
{
|
||||
class PluginNode;
|
||||
class PluginProgressReporter;
|
||||
enum class ErrorType { error, warning, message };
|
||||
struct PersistentErrors {
|
||||
ErrorType type;
|
||||
QString message;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Provider returning the currently active viewer
|
||||
*
|
||||
* Registered by the UI layer, which resolves the viewer through the panel
|
||||
* manager. Without a provider, the OFX timeline suite falls back to its
|
||||
* safe defaults (current time 0, empty bounds, seeking does nothing).
|
||||
*/
|
||||
using ActiveViewerProvider = std::function<ViewerOutput *()>;
|
||||
|
||||
void set_active_viewer_provider(ActiveViewerProvider provider);
|
||||
|
||||
class OlivePluginInstance : public OFX::Host::ImageEffect::Instance {
|
||||
public:
|
||||
OlivePluginInstance(OFX::Host::ImageEffect::ImageEffectPlugin *plugin,
|
||||
OFX::Host::ImageEffect::Descriptor &desc,
|
||||
const std::string &context, bool interactive)
|
||||
: OFX::Host::ImageEffect::Instance(plugin, desc, context, interactive)
|
||||
{
|
||||
}
|
||||
OlivePluginInstance(OlivePluginInstance &instance)
|
||||
: Instance(instance._plugin, *instance._descriptor, instance._context,
|
||||
instance._interactive)
|
||||
{
|
||||
// Do NOT shallow-copy _clips: Instance::~Instance() deletes them,
|
||||
// which would cause a double-free. Clips are re-created in populate().
|
||||
_created = instance._created;
|
||||
_clipPrefsDirty = instance._clipPrefsDirty;
|
||||
_continuousSamples = instance._continuousSamples;
|
||||
_frameVarying = instance._frameVarying;
|
||||
_outputPreMultiplication = instance._outputPreMultiplication;
|
||||
_outputFielding = instance._outputFielding;
|
||||
_outputFrameRate = instance._outputFrameRate;
|
||||
}
|
||||
explicit OlivePluginInstance(Instance &instance)
|
||||
: Instance(instance) {};
|
||||
~OlivePluginInstance() override;
|
||||
const std::string &getDefaultOutputFielding() const override;
|
||||
|
||||
void setVideoParam(VideoParams params)
|
||||
{
|
||||
this->params_ = params;
|
||||
}
|
||||
void setNode(std::shared_ptr<PluginNode> node);
|
||||
std::shared_ptr<PluginNode> node() const
|
||||
{
|
||||
return node_;
|
||||
}
|
||||
void setOpenGLEnabled(bool enabled)
|
||||
{
|
||||
open_gl_enabled_ = enabled;
|
||||
}
|
||||
bool isCreated() const
|
||||
{
|
||||
return _created;
|
||||
}
|
||||
OFX::Host::ImageEffect::ClipInstance *
|
||||
newClipInstance(OFX::Host::ImageEffect::Instance *plugin,
|
||||
OFX::Host::ImageEffect::ClipDescriptor *descriptor,
|
||||
int index) override;
|
||||
|
||||
OfxStatus vmessage(const char *type, const char *id, const char *format,
|
||||
va_list args) override;
|
||||
|
||||
OfxStatus setPersistentMessage(const char *type, const char *id,
|
||||
const char *format, va_list args) override;
|
||||
|
||||
OfxStatus clearPersistentMessage() override;
|
||||
int persistent_message_count() const
|
||||
{
|
||||
return persistentErrors_.size();
|
||||
}
|
||||
const QList<PersistentErrors> &persistent_messages() const
|
||||
{
|
||||
return persistentErrors_;
|
||||
}
|
||||
|
||||
void getProjectSize(double &x_size, double &y_size) const override;
|
||||
void getProjectOffset(double &x_offset, double &y_offset) const override;
|
||||
void getProjectExtent(double &x_size, double &y_size) const override;
|
||||
// The pixel aspect ratio of the current project
|
||||
double getProjectPixelAspectRatio() const override;
|
||||
|
||||
// The duration of the effect
|
||||
// This contains the duration of the plug-in effect, in frames.
|
||||
double getEffectDuration() const override;
|
||||
|
||||
// For an instance, this is the frame rate of the project the effect is in.
|
||||
double getFrameRate() const override;
|
||||
|
||||
/// This is called whenever a param is changed by the plugin so that
|
||||
/// the recursive instanceChangedAction will be fed the correct frame
|
||||
double getFrameRecursive() const override;
|
||||
|
||||
/// This is called whenever a param is changed by the plugin so that
|
||||
/// the recursive instanceChangedAction will be fed the correct
|
||||
/// renderScale
|
||||
void getRenderScaleRecursive(double &x, double &y) const override;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// overridden for Param::SetInstance
|
||||
|
||||
/// make a parameter instance
|
||||
///
|
||||
/// Client host code needs to implement this
|
||||
OFX::Host::Param::Instance *
|
||||
newParam(const std::string &name,
|
||||
OFX::Host::Param::Descriptor &descriptor) override;
|
||||
|
||||
void submit_undo_command(UndoCommand *command, const QString &label);
|
||||
|
||||
/// Triggered when the plug-in calls OfxParameterSuiteV1::paramEditBegin
|
||||
///
|
||||
/// Client host code needs to implement this
|
||||
virtual OfxStatus editBegin(const std::string &name) override;
|
||||
|
||||
/// Triggered when the plug-in calls OfxParameterSuiteV1::paramEditEnd
|
||||
///
|
||||
/// Client host code needs to implement this
|
||||
virtual OfxStatus editEnd() override;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// overridden for Progress::ProgressI
|
||||
|
||||
/// Start doing progress.
|
||||
virtual void progressStart(const std::string &message,
|
||||
const std::string &messageid) override;
|
||||
|
||||
/// finish yer progress
|
||||
virtual void progressEnd() override;
|
||||
|
||||
/// set the progress to some level of completion, returns
|
||||
/// false if you should abandon processing, true to continue
|
||||
virtual bool progressUpdate(double t) override;
|
||||
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
virtual OfxStatus contextAttachedAction() override;
|
||||
virtual OfxStatus contextDetachedAction() override;
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// overridden for TimeLine::TimeLineI
|
||||
|
||||
/// get the current time on the timeline. This is not necessarily the same
|
||||
/// time as being passed to an action (eg render)
|
||||
virtual double timeLineGetTime();
|
||||
|
||||
/// set the timeline to a specific time
|
||||
virtual void timeLineGotoTime(double t);
|
||||
|
||||
/// get the first and last times available on the effect's timeline
|
||||
virtual void timeLineGetBounds(double &t1, double &t2);
|
||||
|
||||
void setCustomInArgs(const std::string &action,
|
||||
OFX::Host::Property::Set &in_args) override;
|
||||
|
||||
private:
|
||||
QList<PersistentErrors> persistentErrors_;
|
||||
VideoParams params_;
|
||||
std::shared_ptr<PluginNode> node_ = nullptr;
|
||||
int edit_depth_ = 0;
|
||||
MultiUndoCommand *edit_command_ = nullptr;
|
||||
QString edit_label_;
|
||||
QString edit_first_label_;
|
||||
int edit_param_count_ = 0;
|
||||
QPointer<PluginProgressReporter> progress_reporter_;
|
||||
bool progress_cancelled_ = false;
|
||||
bool progress_active_ = false;
|
||||
bool open_gl_enabled_ = false;
|
||||
|
||||
public:
|
||||
std::mutex &mutex()
|
||||
{
|
||||
return mutex_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 "paraminstance.h"
|
||||
|
||||
#include "coreengine.h"
|
||||
#include "oliveplugininstance.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
void submit_undo_command(const std::shared_ptr<PluginNode> &node,
|
||||
UndoCommand *command, const QString &label)
|
||||
{
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
auto *instance = node->getPluginInstance();
|
||||
auto *olive_instance = dynamic_cast<OlivePluginInstance *>(instance);
|
||||
if (olive_instance) {
|
||||
olive_instance->submit_undo_command(command, label);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_gui_thread()) {
|
||||
command->redo_now();
|
||||
delete command;
|
||||
return;
|
||||
}
|
||||
|
||||
EngineCore::instance()->undo_stack()->push(command, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 "pluginprogressreporter.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief No-op reporter used when no UI factory is registered
|
||||
*
|
||||
* Never emits cancelled(), so processing always continues.
|
||||
*/
|
||||
class NullPluginProgressReporter : public PluginProgressReporter {
|
||||
public:
|
||||
void set_progress(double value) override
|
||||
{
|
||||
(void)value;
|
||||
}
|
||||
|
||||
void show() override
|
||||
{
|
||||
}
|
||||
|
||||
void close() override
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
PluginProgressReporterFactory reporter_factory_;
|
||||
|
||||
}
|
||||
|
||||
void set_plugin_progress_reporter_factory(
|
||||
PluginProgressReporterFactory factory)
|
||||
{
|
||||
reporter_factory_ = std::move(factory);
|
||||
}
|
||||
|
||||
PluginProgressReporter *
|
||||
create_plugin_progress_reporter(const QString &message, const QString &title)
|
||||
{
|
||||
if (reporter_factory_) {
|
||||
return reporter_factory_(message, title);
|
||||
}
|
||||
|
||||
return new NullPluginProgressReporter();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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_PLUGIN_PROGRESS_REPORTER_H
|
||||
#define OAK_PLUGIN_PROGRESS_REPORTER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief UI-independent interface for reporting plugin progress
|
||||
*
|
||||
* The engine cannot show UI itself, so OFX progress reporting goes through
|
||||
* this interface. The UI layer registers a factory (see
|
||||
* set_plugin_progress_reporter_factory()) that creates a reporter wrapping a
|
||||
* ProgressDialog; without a factory, a no-op reporter is used instead.
|
||||
*/
|
||||
class PluginProgressReporter : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PluginProgressReporter(QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~PluginProgressReporter() override = default;
|
||||
|
||||
virtual void set_progress(double value) = 0;
|
||||
|
||||
virtual void show() = 0;
|
||||
|
||||
virtual void close() = 0;
|
||||
|
||||
signals:
|
||||
void cancelled();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Factory creating a PluginProgressReporter for a progress session
|
||||
*
|
||||
* Registered by the UI layer at startup. The caller takes ownership of the
|
||||
* returned reporter.
|
||||
*/
|
||||
using PluginProgressReporterFactory =
|
||||
std::function<PluginProgressReporter *(const QString &message,
|
||||
const QString &title)>;
|
||||
|
||||
void set_plugin_progress_reporter_factory(
|
||||
PluginProgressReporterFactory factory);
|
||||
|
||||
/**
|
||||
* @brief Create a progress reporter through the registered factory
|
||||
*
|
||||
* Without a factory, returns a no-op reporter so engine code can run
|
||||
* headless. The caller takes ownership of the returned reporter.
|
||||
*/
|
||||
PluginProgressReporter *
|
||||
create_plugin_progress_reporter(const QString &message, const QString &title);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OAK_PLUGIN_PROGRESS_REPORTER_H
|
||||
Reference in New Issue
Block a user