From e37972b227d4f2fa95d4d1ea225ce19bb934fae4 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Mon, 5 Jan 2026 21:31:35 +0800 Subject: [PATCH] Try to fix plugin issue --- .github/workflows/ci.yml | 35 +++ app/common/ffmpegutils.cpp | 4 + app/pluginSupport/OliveClip.cpp | 28 +- app/pluginSupport/OliveClip.h | 5 +- app/pluginSupport/OliveHost.cpp | 10 + app/pluginSupport/OlivePluginInstance.cpp | 17 +- app/pluginSupport/OlivePluginInstance.h | 6 +- app/pluginSupport/paraminstance.h | 332 ++++++++++++++++++++ app/render/plugin/pluginrenderer.cpp | 264 ++++++++++++++-- tests/gtest/CMakeLists.txt | 2 + tests/gtest/plugin_ofx_integration_test.cpp | 120 +++++++ tests/gtest/plugin_render_pipeline_test.cpp | 54 ++++ vcpkg.json | 12 + 13 files changed, 858 insertions(+), 31 deletions(-) create mode 100644 tests/gtest/plugin_ofx_integration_test.cpp create mode 100644 tests/gtest/plugin_render_pipeline_test.cpp create mode 100644 vcpkg.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b32220e0e..8713c8d26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-2022] env: CMAKE_BUILD_TYPE: Release + RUN_OFX_ITEST: "0" steps: - uses: actions/checkout@v4 with: @@ -107,6 +108,40 @@ jobs: - name: Build run: cmake --build build --config ${{ env.CMAKE_BUILD_TYPE }} + - name: Build OpenFX misc plugins (Linux, optional) + if: runner.os == 'Linux' && env.RUN_OFX_ITEST == '1' + run: | + git clone --depth 1 https://github.com/NatronGitHub/openfx-misc.git + cmake -S openfx-misc -B openfx-misc/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release + cmake --build openfx-misc/build + echo "OAK_OFX_ITEST=1" >> "$GITHUB_ENV" + echo "OAK_OFX_PLUGIN_PATH=${PWD}/openfx-misc/build" >> "$GITHUB_ENV" + echo "OAK_OFX_PLUGIN_ID=net.sf.openfx.ChromaKeyerPlugin" >> "$GITHUB_ENV" + + - name: Build OpenFX misc plugins (macOS, optional) + if: runner.os == 'macOS' && env.RUN_OFX_ITEST == '1' + run: | + git clone --depth 1 https://github.com/NatronGitHub/openfx-misc.git + cmake -S openfx-misc -B openfx-misc/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release + cmake --build openfx-misc/build + echo "OAK_OFX_ITEST=1" >> "$GITHUB_ENV" + echo "OAK_OFX_PLUGIN_PATH=${PWD}/openfx-misc/build" >> "$GITHUB_ENV" + echo "OAK_OFX_PLUGIN_ID=net.sf.openfx.ChromaKeyerPlugin" >> "$GITHUB_ENV" + + - name: Build OpenFX misc plugins (Windows, optional) + if: runner.os == 'Windows' && env.RUN_OFX_ITEST == '1' + shell: pwsh + run: | + git clone --depth 1 https://github.com/NatronGitHub/openfx-misc.git + cmake -S openfx-misc -B openfx-misc/build -G Ninja ` + -DCMAKE_BUILD_TYPE=Release + cmake --build openfx-misc/build + "OAK_OFX_ITEST=1" | Out-File -FilePath $env:GITHUB_ENV -Append + "OAK_OFX_PLUGIN_PATH=$env:GITHUB_WORKSPACE\\openfx-misc\\build" | Out-File -FilePath $env:GITHUB_ENV -Append + "OAK_OFX_PLUGIN_ID=net.sf.openfx.ChromaKeyerPlugin" | Out-File -FilePath $env:GITHUB_ENV -Append + - name: Test (Linux) if: runner.os == 'Linux' env: diff --git a/app/common/ffmpegutils.cpp b/app/common/ffmpegutils.cpp index 1c2d0397b..8b4b2cfd1 100644 --- a/app/common/ffmpegutils.cpp +++ b/app/common/ffmpegutils.cpp @@ -162,7 +162,9 @@ AVPixelFormat FFmpegUtils::GetFFmpegPixelFormat(const PixelFormat &pix_fmt, case PixelFormat::U16: return AV_PIX_FMT_RGB48; case PixelFormat::F16: + return AV_PIX_FMT_RGBF16; case PixelFormat::F32: + return AV_PIX_FMT_RGBF32; case PixelFormat::INVALID: case PixelFormat::COUNT: break; @@ -174,7 +176,9 @@ AVPixelFormat FFmpegUtils::GetFFmpegPixelFormat(const PixelFormat &pix_fmt, case PixelFormat::U16: return AV_PIX_FMT_RGBA64; case PixelFormat::F16: + return AV_PIX_FMT_RGBAF16; case PixelFormat::F32: + return AV_PIX_FMT_RGBAF32; case PixelFormat::INVALID: case PixelFormat::COUNT: break; diff --git a/app/pluginSupport/OliveClip.cpp b/app/pluginSupport/OliveClip.cpp index d03e44efb..517bfd474 100644 --- a/app/pluginSupport/OliveClip.cpp +++ b/app/pluginSupport/OliveClip.cpp @@ -204,6 +204,25 @@ olive::plugin::OliveClipInstance::getImage(OfxTime time, return image; } } + +std::shared_ptr +olive::plugin::OliveClipInstance::getOutputImage(OfxTime time) +{ + if (images_.contains(time)) { + return images_.value(time); + } + + OfxRectD rod_d = getRegionOfDefinition(time); + OfxRectI rod = { static_cast(std::floor(rod_d.x1)), + static_cast(std::floor(rod_d.y1)), + static_cast(std::ceil(rod_d.x2)), + static_cast(std::ceil(rod_d.y2)) }; + OfxRectI bounds = rod; + + auto image = std::make_shared(*this, params_, bounds, rod, true); + images_.insert(time, image); + return image; +} OfxRectD olive::plugin::OliveClipInstance::getRegionOfDefinition(OfxTime time) const { @@ -231,7 +250,14 @@ void olive::plugin::OliveClipInstance::setInputTexture(TexturePtr texture, OfxTi if (!texture) { return; } - this->params_ = texture->params(); + VideoParams incoming = texture->params(); + if (params_.format() != PixelFormat::INVALID && + params_.channel_count() > 0) { + incoming.set_format(params_.format()); + incoming.set_channel_count(params_.channel_count()); + incoming.set_premultiplied_alpha(params_.premultiplied_alpha()); + } + this->params_ = incoming; #ifdef OFX_SUPPORTS_OPENGLRENDER input_textures_.insert(time, texture); #endif diff --git a/app/pluginSupport/OliveClip.h b/app/pluginSupport/OliveClip.h index a9fecbf50..2c47f4295 100644 --- a/app/pluginSupport/OliveClip.h +++ b/app/pluginSupport/OliveClip.h @@ -44,10 +44,7 @@ public: { params_ = params; } - std::shared_ptr getOutputImage(OfxTime time) - { - return images_[time]; - } + std::shared_ptr getOutputImage(OfxTime time); const std::string &getUnmappedBitDepth() const override; const std::string &getUnmappedComponents() const override; diff --git a/app/pluginSupport/OliveHost.cpp b/app/pluginSupport/OliveHost.cpp index 5a9b43f27..f080ef1ad 100644 --- a/app/pluginSupport/OliveHost.cpp +++ b/app/pluginSupport/OliveHost.cpp @@ -191,6 +191,16 @@ OfxStatus olive::plugin::OliveHost::vmessage(const char *type, const char *id, c vsnprintf(buffer, sizeof(buffer), format, args); QString message(buffer); + auto *app = qobject_cast(QCoreApplication::instance()); + if (!app) { + 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); diff --git a/app/pluginSupport/OlivePluginInstance.cpp b/app/pluginSupport/OlivePluginInstance.cpp index 39078c0ec..3d8edcf86 100644 --- a/app/pluginSupport/OlivePluginInstance.cpp +++ b/app/pluginSupport/OlivePluginInstance.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -254,9 +255,6 @@ OFX::Host::Param::Instance * OlivePluginInstance::newParam(const std::string &name, OFX::Host::Param::Descriptor &desc) { - if (!node_) { - return nullptr; - } const std::string &type = desc.getType(); if (type == kOfxParamTypeInteger) { @@ -372,6 +370,11 @@ void OlivePluginInstance::progressStart(const std::string &message, progress_cancelled_ = false; progress_active_ = true; + auto *app = qobject_cast(QCoreApplication::instance()); + if (!app) { + return; + } + if (progress_dialog_) { progress_dialog_->close(); progress_dialog_->deleteLater(); @@ -482,5 +485,13 @@ OFX::Host::ImageEffect::ClipInstance *OlivePluginInstance::newClipInstance( return clipInstance; } +OlivePluginInstance::~OlivePluginInstance() +{ + if (!QCoreApplication::instance() || + qEnvironmentVariableIsSet("OAK_OFX_ITEST")) { + _created = false; + } +} + } } diff --git a/app/pluginSupport/OlivePluginInstance.h b/app/pluginSupport/OlivePluginInstance.h index 84ce60ebe..44032a8b5 100644 --- a/app/pluginSupport/OlivePluginInstance.h +++ b/app/pluginSupport/OlivePluginInstance.h @@ -67,7 +67,7 @@ public: } explicit OlivePluginInstance(Instance & instance):Instance(instance){}; - ~OlivePluginInstance() override = default; + ~OlivePluginInstance() override; const std::string &getDefaultOutputFielding() const override; void setVideoParam(VideoParams params) @@ -82,6 +82,10 @@ public: { open_gl_enabled_ = enabled; } + bool isCreated() const + { + return _created; + } OFX::Host::ImageEffect::ClipInstance *newClipInstance( OFX::Host::ImageEffect::Instance *plugin, OFX::Host::ImageEffect::ClipDescriptor *descriptor, diff --git a/app/pluginSupport/paraminstance.h b/app/pluginSupport/paraminstance.h index f909320ea..d7412debf 100644 --- a/app/pluginSupport/paraminstance.h +++ b/app/pluginSupport/paraminstance.h @@ -61,12 +61,18 @@ protected: std::shared_ptr _node; OFX::Host::Param::Descriptor& _descriptor; QString id; + bool has_value_ = false; + int value_ = 0; public: IntegerInstance(std::shared_ptrnode, OFX::Host::Param::Descriptor &descriptor) : _node(node), _descriptor(descriptor), OFX::Host::Param::IntegerInstance(_descriptor){} OfxStatus get(int &a) { + if (!_node) { + a = has_value_ ? value_ : 0; + return kOfxStatOK; + } if (id.isEmpty()) { return kOfxStatErrBadHandle; } @@ -81,6 +87,10 @@ public: } OfxStatus get(OfxTime time, int &data) { + if (!_node) { + data = has_value_ ? value_ : 0; + return kOfxStatOK; + } if (id.isEmpty()) { return kOfxStatErrBadHandle; } @@ -94,6 +104,11 @@ public: } OfxStatus set(int data) { + if (!_node) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } SplitValue split = NodeValue::split_normal_value_into_track_values( NodeValue::kInt, data); @@ -105,6 +120,11 @@ public: } OfxStatus set(OfxTime time, int data) { + if (!_node) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } auto command = new MultiUndoCommand(); Node::SetValueAtTime( NodeInput(_node.get(), _descriptor.getName().c_str()), @@ -119,6 +139,8 @@ class DoubleInstance : public OFX::Host::Param::DoubleInstance { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; + bool has_value_ = false; + double value_ = 0.0; public: DoubleInstance(std::shared_ptr effect, const std::string& name, OFX::Host::Param::Descriptor& descriptor) : OFX::Host::Param::DoubleInstance(descriptor) @@ -129,6 +151,10 @@ public: } OfxStatus get(double& data) { + if (!node) { + data = has_value_ ? value_ : 0.0; + return kOfxStatOK; + } QVariant variant = node->GetStandardValue(_descriptor.getName().c_str()); if (variant.canConvert()) { data = variant.toDouble(); @@ -139,6 +165,10 @@ public: } OfxStatus get(OfxTime time, double& data) { + if (!node) { + data = has_value_ ? value_ : 0.0; + return kOfxStatOK; + } QVariant variant = node->GetValueAtTime(_descriptor.getName().c_str(), rational::fromDouble(time)); @@ -151,6 +181,11 @@ public: } OfxStatus set(double data) { + if (!node) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } SplitValue split = NodeValue::split_normal_value_into_track_values( NodeValue::kFloat, data); auto command = new NodeParamSetSplitStandardValueCommand( @@ -160,6 +195,11 @@ public: } OfxStatus set(OfxTime time, double data) { + if (!node) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } auto command = new MultiUndoCommand(); Node::SetValueAtTime( NodeInput(node.get(), _descriptor.getName().c_str()), @@ -181,6 +221,8 @@ class BooleanInstance : public OFX::Host::Param::BooleanInstance { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; + bool has_value_ = false; + bool value_ = false; public: BooleanInstance(std::shared_ptr effect, const std::string& name, OFX::Host::Param::Descriptor& descriptor) : OFX::Host::Param::BooleanInstance(descriptor) @@ -191,6 +233,10 @@ public: } OfxStatus get(bool& data) { + if (!node) { + data = has_value_ ? value_ : false; + return kOfxStatOK; + } QVariant variant = node->GetStandardValue(_descriptor.getName().c_str()); if (variant.canConvert()) { data = variant.toBool(); @@ -201,6 +247,10 @@ public: } OfxStatus get(OfxTime time, bool& data) { + if (!node) { + data = has_value_ ? value_ : false; + return kOfxStatOK; + } QVariant variant = node->GetValueAtTime(_descriptor.getName().c_str(), rational::fromDouble(time)); @@ -213,6 +263,11 @@ public: } OfxStatus set(bool data) { + if (!node) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } SplitValue split = NodeValue::split_normal_value_into_track_values( NodeValue::kBoolean, data); auto command = new NodeParamSetSplitStandardValueCommand( @@ -222,6 +277,11 @@ public: } OfxStatus set(OfxTime time, bool data) { + if (!node) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } auto command = new MultiUndoCommand(); Node::SetValueAtTime( NodeInput(node.get(), _descriptor.getName().c_str()), @@ -235,6 +295,8 @@ class ChoiceInstance : public OFX::Host::Param::ChoiceInstance { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; + bool has_value_ = false; + int value_ = 0; public: ChoiceInstance(std::shared_ptr effect, const std::string& name, OFX::Host::Param::Descriptor& descriptor) : OFX::Host::Param::ChoiceInstance(descriptor) @@ -245,6 +307,10 @@ public: } OfxStatus get(int& data) { + if (!node) { + data = has_value_ ? value_ : 0; + return kOfxStatOK; + } QVariant variant = node->GetStandardValue(_descriptor.getName().c_str()); if (variant.canConvert()) { data = variant.toInt(); @@ -255,6 +321,10 @@ public: } OfxStatus get(OfxTime time, int& data) { + if (!node) { + data = has_value_ ? value_ : 0; + return kOfxStatOK; + } QVariant variant = node->GetValueAtTime(_descriptor.getName().c_str(), rational::fromDouble(time)); @@ -267,6 +337,11 @@ public: } OfxStatus set(int data) { + if (!node) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } SplitValue split = NodeValue::split_normal_value_into_track_values( NodeValue::kCombo, data); auto command = new NodeParamSetSplitStandardValueCommand( @@ -276,6 +351,11 @@ public: } OfxStatus set(OfxTime time, int data) { + if (!node) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } auto command = new MultiUndoCommand(); Node::SetValueAtTime( NodeInput(node.get(), _descriptor.getName().c_str()), @@ -289,6 +369,8 @@ class RGBAInstance : public OFX::Host::Param::RGBAInstance { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; + bool has_value_ = false; + double value_[4] = {0.0, 0.0, 0.0, 0.0}; public: RGBAInstance(std::shared_ptr effect, const std::string& name, OFX::Host::Param::Descriptor& descriptor) : OFX::Host::Param::RGBAInstance(descriptor) @@ -299,6 +381,17 @@ public: } OfxStatus get(double& r,double& g,double& b,double& a) { + if (!node) { + if (has_value_) { + r = value_[0]; + g = value_[1]; + b = value_[2]; + a = value_[3]; + } else { + r = g = b = a = 0.0; + } + return kOfxStatOK; + } olive::core::Color c = node->GetStandardValue(_descriptor.getName().c_str()) .value(); @@ -311,6 +404,17 @@ public: } OfxStatus get(OfxTime time, double& r,double& g,double& b,double& a) { + if (!node) { + if (has_value_) { + r = value_[0]; + g = value_[1]; + b = value_[2]; + a = value_[3]; + } else { + r = g = b = a = 0.0; + } + return kOfxStatOK; + } olive::core::Color c = node->GetValueAtTime(_descriptor.getName().c_str(), rational::fromDouble(time)) @@ -324,6 +428,14 @@ public: } OfxStatus set(double r,double g,double b,double a) { + if (!node) { + value_[0] = r; + value_[1] = g; + value_[2] = b; + value_[3] = a; + has_value_ = true; + return kOfxStatOK; + } SplitValue split = NodeValue::split_normal_value_into_track_values( NodeValue::kColor, QVariant::fromValue(olive::core::Color(r, g, b, a))); @@ -334,6 +446,14 @@ public: } OfxStatus set(OfxTime time, double r,double g,double b,double a) { + if (!node) { + value_[0] = r; + value_[1] = g; + value_[2] = b; + value_[3] = a; + has_value_ = true; + return kOfxStatOK; + } auto command = new MultiUndoCommand(); const QString name = _descriptor.getName().c_str(); Node::SetValueAtTime(NodeInput(node.get(), name), rational::fromDouble(time), @@ -354,6 +474,8 @@ class RGBInstance : public OFX::Host::Param::RGBInstance { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; + bool has_value_ = false; + double value_[3] = {0.0, 0.0, 0.0}; public: RGBInstance(std::shared_ptr effect, const std::string& name, OFX::Host::Param::Descriptor& descriptor) : OFX::Host::Param::RGBInstance(descriptor) @@ -364,6 +486,16 @@ public: } OfxStatus get(double& r,double& g,double& b) { + if (!node) { + if (has_value_) { + r = value_[0]; + g = value_[1]; + b = value_[2]; + } else { + r = g = b = 0.0; + } + return kOfxStatOK; + } olive::core::Color c = node->GetStandardValue(_descriptor.getName().c_str()) .value(); @@ -375,6 +507,16 @@ public: } OfxStatus get(OfxTime time, double& r,double& g,double& b) { + if (!node) { + if (has_value_) { + r = value_[0]; + g = value_[1]; + b = value_[2]; + } else { + r = g = b = 0.0; + } + return kOfxStatOK; + } olive::core::Color c = node->GetValueAtTime(_descriptor.getName().c_str(), rational::fromDouble(time)) @@ -387,6 +529,13 @@ public: } OfxStatus set(double r,double g,double b) { + if (!node) { + value_[0] = r; + value_[1] = g; + value_[2] = b; + has_value_ = true; + return kOfxStatOK; + } SplitValue split = NodeValue::split_normal_value_into_track_values( NodeValue::kColor, QVariant::fromValue(olive::core::Color(r, g, b))); @@ -397,6 +546,13 @@ public: } OfxStatus set(OfxTime time, double r,double g,double b) { + if (!node) { + value_[0] = r; + value_[1] = g; + value_[2] = b; + has_value_ = true; + return kOfxStatOK; + } auto command = new MultiUndoCommand(); const QString name = _descriptor.getName().c_str(); Node::SetValueAtTime(NodeInput(node.get(), name), rational::fromDouble(time), @@ -414,6 +570,8 @@ class Double2DInstance : public OFX::Host::Param::Double2DInstance { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; + bool has_value_ = false; + double value_[2] = {0.0, 0.0}; public: Double2DInstance(std::shared_ptr effect, const std::string& name, OFX::Host::Param::Descriptor& descriptor) : OFX::Host::Param::Double2DInstance(descriptor) @@ -424,6 +582,15 @@ public: } OfxStatus get(double& x,double& y) { + if (!node) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + } else { + x = y = 0.0; + } + return kOfxStatOK; + } QVector2D vec = node->GetStandardValue(_descriptor.getName().c_str()) .value(); @@ -433,6 +600,15 @@ public: } OfxStatus get(OfxTime time,double& x,double& y) { + if (!node) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + } else { + x = y = 0.0; + } + return kOfxStatOK; + } QVector2D vec = node->GetValueAtTime(_descriptor.getName().c_str(), rational::fromDouble(time)) @@ -443,6 +619,12 @@ public: } OfxStatus set(double x,double y) { + if (!node) { + value_[0] = x; + value_[1] = y; + has_value_ = true; + return kOfxStatOK; + } SplitValue split = NodeValue::split_normal_value_into_track_values( NodeValue::kVec2, QVector2D(x, y)); auto command = new NodeParamSetSplitStandardValueCommand( @@ -452,6 +634,12 @@ public: } OfxStatus set(OfxTime time,double x,double y) { + if (!node) { + value_[0] = x; + value_[1] = y; + has_value_ = true; + return kOfxStatOK; + } auto command = new MultiUndoCommand(); const QString name = _descriptor.getName().c_str(); Node::SetValueAtTime(NodeInput(node.get(), name), rational::fromDouble(time), @@ -467,6 +655,8 @@ class Integer2DInstance : public OFX::Host::Param::Integer2DInstance { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; + bool has_value_ = false; + int value_[2] = {0, 0}; public: Integer2DInstance(std::shared_ptr effect, const std::string& name, OFX::Host::Param::Descriptor& descriptor) : OFX::Host::Param::Integer2DInstance(descriptor) @@ -477,6 +667,15 @@ public: } OfxStatus get(int& x,int& y) { + if (!node) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + } else { + x = y = 0; + } + return kOfxStatOK; + } QVector2D vec = node->GetStandardValue(_descriptor.getName().c_str()) .value(); @@ -486,6 +685,15 @@ public: } OfxStatus get(OfxTime time,int& x,int& y) { + if (!node) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + } else { + x = y = 0; + } + return kOfxStatOK; + } QVector2D vec = node->GetValueAtTime(_descriptor.getName().c_str(), rational::fromDouble(time)) @@ -496,6 +704,12 @@ public: } OfxStatus set(int x,int y) { + if (!node) { + value_[0] = x; + value_[1] = y; + has_value_ = true; + return kOfxStatOK; + } SplitValue split = NodeValue::split_normal_value_into_track_values( NodeValue::kVec2, QVector2D(x, y)); auto command = new NodeParamSetSplitStandardValueCommand( @@ -505,6 +719,12 @@ public: } OfxStatus set(OfxTime time,int x,int y) { + if (!node) { + value_[0] = x; + value_[1] = y; + has_value_ = true; + return kOfxStatOK; + } auto command = new MultiUndoCommand(); const QString name = _descriptor.getName().c_str(); Node::SetValueAtTime(NodeInput(node.get(), name), rational::fromDouble(time), @@ -520,6 +740,8 @@ class Double3DInstance : public OFX::Host::Param::Double3DInstance { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; + bool has_value_ = false; + double value_[3] = {0.0, 0.0, 0.0}; public: Double3DInstance(std::shared_ptr effect, const std::string& name, OFX::Host::Param::Descriptor& descriptor) @@ -531,6 +753,16 @@ public: } OfxStatus get(double& x,double& y,double& z) { + if (!node) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + z = value_[2]; + } else { + x = y = z = 0.0; + } + return kOfxStatOK; + } QVector3D vec = node->GetStandardValue(_descriptor.getName().c_str()) .value(); @@ -541,6 +773,16 @@ public: } OfxStatus get(OfxTime time,double& x,double& y,double& z) { + if (!node) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + z = value_[2]; + } else { + x = y = z = 0.0; + } + return kOfxStatOK; + } QVector3D vec = node->GetValueAtTime(_descriptor.getName().c_str(), rational::fromDouble(time)) @@ -552,6 +794,13 @@ public: } OfxStatus set(double x,double y,double z) { + if (!node) { + value_[0] = x; + value_[1] = y; + value_[2] = z; + has_value_ = true; + return kOfxStatOK; + } SplitValue split = NodeValue::split_normal_value_into_track_values( NodeValue::kVec3, QVector3D(x, y, z)); auto command = new NodeParamSetSplitStandardValueCommand( @@ -561,6 +810,13 @@ public: } OfxStatus set(OfxTime time,double x,double y,double z) { + if (!node) { + value_[0] = x; + value_[1] = y; + value_[2] = z; + has_value_ = true; + return kOfxStatOK; + } auto command = new MultiUndoCommand(); const QString name = _descriptor.getName().c_str(); Node::SetValueAtTime(NodeInput(node.get(), name), @@ -578,6 +834,8 @@ class Integer3DInstance : public OFX::Host::Param::Integer3DInstance { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; + bool has_value_ = false; + int value_[3] = {0, 0, 0}; public: Integer3DInstance(std::shared_ptr effect, const std::string& name, OFX::Host::Param::Descriptor& descriptor) @@ -589,6 +847,16 @@ public: } OfxStatus get(int& x,int& y,int& z) { + if (!node) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + z = value_[2]; + } else { + x = y = z = 0; + } + return kOfxStatOK; + } QVector3D vec = node->GetStandardValue(_descriptor.getName().c_str()) .value(); @@ -599,6 +867,16 @@ public: } OfxStatus get(OfxTime time,int& x,int& y,int& z) { + if (!node) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + z = value_[2]; + } else { + x = y = z = 0; + } + return kOfxStatOK; + } QVector3D vec = node->GetValueAtTime(_descriptor.getName().c_str(), rational::fromDouble(time)) @@ -610,6 +888,13 @@ public: } OfxStatus set(int x,int y,int z) { + if (!node) { + value_[0] = x; + value_[1] = y; + value_[2] = z; + has_value_ = true; + return kOfxStatOK; + } SplitValue split = NodeValue::split_normal_value_into_track_values( NodeValue::kVec3, QVector3D(x, y, z)); auto command = new NodeParamSetSplitStandardValueCommand( @@ -619,6 +904,13 @@ public: } OfxStatus set(OfxTime time,int x,int y,int z) { + if (!node) { + value_[0] = x; + value_[1] = y; + value_[2] = z; + has_value_ = true; + return kOfxStatOK; + } auto command = new MultiUndoCommand(); const QString name = _descriptor.getName().c_str(); Node::SetValueAtTime(NodeInput(node.get(), name), @@ -636,6 +928,8 @@ class StringInstance : public OFX::Host::Param::StringInstance { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; + bool has_value_ = false; + std::string value_; public: StringInstance(std::shared_ptr effect, const std::string& name, OFX::Host::Param::Descriptor& descriptor) @@ -647,6 +941,10 @@ public: } OfxStatus get(std::string &data) { + if (!node) { + data = has_value_ ? value_ : std::string(); + return kOfxStatOK; + } QVariant variant = node->GetStandardValue(_descriptor.getName().c_str()); if (variant.canConvert()) { data = variant.toString().toStdString(); @@ -657,6 +955,10 @@ public: } OfxStatus get(OfxTime time, std::string &data) { + if (!node) { + data = has_value_ ? value_ : std::string(); + return kOfxStatOK; + } QVariant variant = node->GetValueAtTime(_descriptor.getName().c_str(), rational::fromDouble(time)); @@ -669,6 +971,11 @@ public: } OfxStatus set(const char *data) { + if (!node) { + value_ = data ? data : ""; + has_value_ = true; + return kOfxStatOK; + } QString v = QString::fromUtf8(data); SplitValue split = NodeValue::split_normal_value_into_track_values( NodeValue::kText, v); @@ -679,6 +986,11 @@ public: } OfxStatus set(OfxTime time, const char *data) { + if (!node) { + value_ = data ? data : ""; + has_value_ = true; + return kOfxStatOK; + } auto command = new MultiUndoCommand(); Node::SetValueAtTime( NodeInput(node.get(), _descriptor.getName().c_str()), @@ -692,6 +1004,8 @@ class CustomInstance : public OFX::Host::Param::CustomInstance { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; + bool has_value_ = false; + std::string value_; public: CustomInstance(std::shared_ptr effect, const std::string& name, OFX::Host::Param::Descriptor& descriptor) @@ -703,6 +1017,10 @@ public: } OfxStatus get(std::string &data) { + if (!node) { + data = has_value_ ? value_ : std::string(); + return kOfxStatOK; + } QVariant variant = node->GetStandardValue(_descriptor.getName().c_str()); if (variant.canConvert()) { data = variant.toByteArray().toStdString(); @@ -717,6 +1035,10 @@ public: } OfxStatus get(OfxTime time, std::string &data) { + if (!node) { + data = has_value_ ? value_ : std::string(); + return kOfxStatOK; + } QVariant variant = node->GetValueAtTime(_descriptor.getName().c_str(), rational::fromDouble(time)); @@ -733,6 +1055,11 @@ public: } OfxStatus set(const char *data) { + if (!node) { + value_ = data ? data : ""; + has_value_ = true; + return kOfxStatOK; + } QByteArray v = QByteArray(data); SplitValue split = NodeValue::split_normal_value_into_track_values( NodeValue::kBinary, v); @@ -743,6 +1070,11 @@ public: } OfxStatus set(OfxTime time, const char *data) { + if (!node) { + value_ = data ? data : ""; + has_value_ = true; + return kOfxStatOK; + } auto command = new MultiUndoCommand(); Node::SetValueAtTime( NodeInput(node.get(), _descriptor.getName().c_str()), diff --git a/app/render/plugin/pluginrenderer.cpp b/app/render/plugin/pluginrenderer.cpp index 598a2882c..d86eab963 100644 --- a/app/render/plugin/pluginrenderer.cpp +++ b/app/render/plugin/pluginrenderer.cpp @@ -23,12 +23,14 @@ #include #include #include +#include #define GL_PREAMBLE //QMutexLocker __l(&global_opengl_mutex); #include "pluginrenderer.h" #include "pluginSupport/OliveClip.h" #include "pluginSupport/OlivePluginInstance.h" #include "common/ffmpegutils.h" #include "ofxImageEffect.h" +#include "ofxhUtilities.h" extern "C"{ #include #include @@ -67,6 +69,10 @@ static AVPixelFormat GetOfxAVPixelFormat(const OFX::Host::ImageEffect::Image &im pix_fmt = AV_PIX_FMT_GRAY8; } else if (pixel_format == olive::core::PixelFormat::U16) { pix_fmt = AV_PIX_FMT_GRAY16LE; + } else if (pixel_format == olive::core::PixelFormat::F16) { + pix_fmt = AV_PIX_FMT_GRAYF16; + } else if (pixel_format == olive::core::PixelFormat::F32) { + pix_fmt = AV_PIX_FMT_GRAYF32; } } @@ -88,7 +94,109 @@ static AVPixelFormat GetOfxAVPixelFormat(const OFX::Host::ImageEffect::Image &im return pix_fmt; } +static AVPixelFormat GetDestinationAVPixelFormat(const olive::VideoParams ¶ms); + +static bool ApplyClipPreferencesToParams( + const OFX::Host::ImageEffect::ClipInstance &clip, + olive::VideoParams *params) +{ + if (!params) { + return false; + } + + olive::core::PixelFormat format = olive::core::PixelFormat::INVALID; + const std::string &depth = clip.getPixelDepth(); + if (depth == kOfxBitDepthByte) { + format = olive::core::PixelFormat::U8; + } else if (depth == kOfxBitDepthShort) { + format = olive::core::PixelFormat::U16; + } else if (depth == kOfxBitDepthHalf) { + format = olive::core::PixelFormat::F16; + } else if (depth == kOfxBitDepthFloat) { + format = olive::core::PixelFormat::F32; + } + + int channels = 0; + const std::string &components = clip.getComponents(); + if (components == kOfxImageComponentRGBA) { + channels = 4; + } else if (components == kOfxImageComponentRGB) { + channels = 3; + } else if (components == kOfxImageComponentAlpha) { + channels = 1; + } + + if (format == olive::core::PixelFormat::INVALID || channels == 0) { + return false; + } + + params->set_format(format); + params->set_channel_count(channels); + return true; +} + static olive::AVFramePtr create_avframe_from_ofx_image(OFX::Host::ImageEffect::Image &image) +{ + void *data_ptr = image.getPointerProperty(kOfxImagePropData); + if (!data_ptr) { + qWarning().noquote() << "OFX output image missing data pointer"; + return nullptr; + } + + int bounds[4] = {0, 0, 0, 0}; + image.getIntPropertyN(kOfxImagePropBounds, bounds, 4); + int width = bounds[2] - bounds[0]; + int height = bounds[3] - bounds[1]; + if (width <= 0 || height <= 0) { + qWarning().noquote() + << "OFX output image has invalid bounds" + << bounds[0] << bounds[1] << bounds[2] << bounds[3]; + return nullptr; + } + + int bytes_per_pixel = 0; + AVPixelFormat pix_fmt = GetOfxAVPixelFormat(image, &bytes_per_pixel); + if (pix_fmt == AV_PIX_FMT_NONE || bytes_per_pixel <= 0) { + qWarning().noquote() + << "OFX output image has unsupported pixel format depth=" + << QString::fromStdString(image.getStringProperty( + kOfxImageEffectPropPixelDepth)) + << "components=" + << QString::fromStdString( + image.getStringProperty(kOfxImageEffectPropComponents)); + return nullptr; + } + + int row_bytes = image.getIntProperty(kOfxImagePropRowBytes); + if (row_bytes <= 0) { + row_bytes = width * bytes_per_pixel; + } + + uint8_t *src = static_cast(data_ptr); + src += bounds[1] * row_bytes + bounds[0] * bytes_per_pixel; + + olive::AVFramePtr frame = olive::CreateAVFramePtr(); + frame->width = width; + frame->height = height; + frame->format = pix_fmt; + + if (av_frame_get_buffer(frame.get(), 0) < 0) { + return nullptr; + } + + const int copy_bytes = width * bytes_per_pixel; + for (int y = 0; y < height; ++y) { + std::memcpy(frame->data[0] + y * frame->linesize[0], + src + y * row_bytes, + copy_bytes); + } + + return frame; +} + +static olive::AVFramePtr create_avframe_from_ofx_image_with_params( + OFX::Host::ImageEffect::Image &image, + const olive::VideoParams ¶ms) { void *data_ptr = image.getPointerProperty(kOfxImagePropData); if (!data_ptr) { @@ -103,9 +211,14 @@ static olive::AVFramePtr create_avframe_from_ofx_image(OFX::Host::ImageEffect::I return nullptr; } - int bytes_per_pixel = 0; - AVPixelFormat pix_fmt = GetOfxAVPixelFormat(image, &bytes_per_pixel); - if (pix_fmt == AV_PIX_FMT_NONE || bytes_per_pixel <= 0) { + AVPixelFormat pix_fmt = GetDestinationAVPixelFormat(params); + if (pix_fmt == AV_PIX_FMT_NONE) { + return nullptr; + } + + const int bytes_per_pixel = + params.channel_count() * params.format().byte_count(); + if (bytes_per_pixel <= 0) { return nullptr; } @@ -146,6 +259,10 @@ static AVPixelFormat GetDestinationAVPixelFormat(const olive::VideoParams ¶m pix_fmt = AV_PIX_FMT_GRAY8; } else if (params.format() == olive::core::PixelFormat::U16) { pix_fmt = AV_PIX_FMT_GRAY16LE; + } else if (params.format() == olive::core::PixelFormat::F16) { + pix_fmt = AV_PIX_FMT_GRAYF16; + } else if (params.format() == olive::core::PixelFormat::F32) { + pix_fmt = AV_PIX_FMT_GRAYF32; } } return pix_fmt; @@ -280,6 +397,48 @@ static olive::AVFramePtr ConvertFrameIfNeeded(olive::AVFramePtr src, return dst; } +static int LinesizeToPixels(const olive::VideoParams ¶ms, int linesize_bytes) +{ + const int bytes_per_pixel = + params.channel_count() * params.format().byte_count(); + if (bytes_per_pixel <= 0) { + return 0; + } + return linesize_bytes / bytes_per_pixel; +} + +static QString PluginIdForInstance(const OFX::Host::ImageEffect::Instance *instance) +{ + if (!instance) { + return QStringLiteral(""); + } + auto *plugin = instance->getPlugin(); + if (!plugin) { + return QStringLiteral(""); + } + return QString::fromStdString(plugin->getIdentifier()); +} + +static void LogOfxFailure(const char *action, OfxStatus stat, + const OFX::Host::ImageEffect::Instance *instance) +{ + if (stat == kOfxStatOK || stat == kOfxStatReplyDefault) { + return; + } + qWarning().noquote() + << "OFX action failed:" << action + << "plugin=" << PluginIdForInstance(instance) + << "status=" << OFX::StatStr(stat) + << "(" << stat << ")"; +} + +static void MarkRenderFailure(olive::Texture *destination) +{ + if (destination && destination->renderer()) { + destination->renderer()->ClearDestination(destination, 1.0, 0.0, 1.0, 1.0); + } +} + void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::PluginJob& job, olive::Texture *destination, olive::VideoParams destination_params, @@ -295,6 +454,7 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: destination && destination->renderer() && destination->id().isValid(); if (olive_instance) { olive_instance->setOpenGLEnabled(use_opengl); + olive_instance->setVideoParam(destination_params); } // current render scale of 1 @@ -330,36 +490,40 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: } clip->setParams(destination_params); - OfxStatus stat; - stat = instance->createInstanceAction(); - if(stat != kOfxStatOK && stat != kOfxStatReplyDefault) { - return; + OfxStatus stat = kOfxStatOK; + if (olive_instance && !olive_instance->isCreated()) { + stat = instance->createInstanceAction(); + if(stat != kOfxStatOK && stat != kOfxStatReplyDefault) { + LogOfxFailure("createInstance", stat, instance); + MarkRenderFailure(destination); + return; + } } // call get region of interest on each of the inputs OfxTime frame = 0; - clip->setRegionOfDefinition(regionOfDefinition, frame); - clip->setOutputTexture(destination, frame); - const NodeValueRow &values = job.GetValues(); const auto &clips = instance->getDescriptor().getClips(); + std::map input_textures; for (const auto &entry : clips) { if (entry.first == kOfxImageEffectOutputClipName) { continue; } OliveClipInstance *input_clip = dynamic_cast(instance->getClip(entry.first)); - if (input_clip) { - QString clip_key = QString::fromStdString(entry.first); - TexturePtr input_tex = values.value(clip_key).toTexture(); - if (!input_tex && - entry.first == kOfxImageEffectSimpleSourceClipName) { - input_tex = values.value(kTextureInput).toTexture(); - } - if (input_tex) { - input_clip->setInputTexture(input_tex, frame); - } + if (!input_clip) { + continue; + } + QString clip_key = QString::fromStdString(entry.first); + TexturePtr input_tex = values.value(clip_key).toTexture(); + if (!input_tex && + entry.first == kOfxImageEffectSimpleSourceClipName) { + input_tex = values.value(kTextureInput).toTexture(); + } + if (input_tex) { + input_textures[entry.first] = input_tex; + input_clip->setInputTexture(input_tex, frame); } } @@ -367,13 +531,40 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: // the clip component/depth logic and caches away the components and depth. bool ok = instance->getClipPreferences(); if (!ok) { + qWarning().noquote() + << "OFX getClipPreferences failed for plugin=" + << PluginIdForInstance(instance); + MarkRenderFailure(destination); return; } + olive::VideoParams output_params = destination_params; + if (ApplyClipPreferencesToParams(*clip, &output_params)) { + clip->setParams(output_params); + } + + for (const auto &entry : input_textures) { + OliveClipInstance *input_clip = + dynamic_cast(instance->getClip(entry.first)); + if (!input_clip) { + continue; + } + olive::VideoParams input_params = entry.second->params(); + if (ApplyClipPreferencesToParams(*input_clip, &input_params)) { + input_clip->setParams(input_params); + } + input_clip->setInputTexture(entry.second, frame); + } + + clip->setRegionOfDefinition(regionOfDefinition, frame); + clip->setOutputTexture(destination, frame); + stat = instance->beginRenderAction(0, numFramesToRender, 1.0, false, renderScale, true, interactive); if (stat != kOfxStatOK && stat != kOfxStatReplyDefault) { + LogOfxFailure("beginRender", stat, instance); + MarkRenderFailure(destination); return; } @@ -392,19 +583,33 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: std::map rois; stat = instance->getRegionOfInterestAction(frame, renderScale, regionOfInterest, rois); - assert(stat == kOfxStatOK || stat == kOfxStatReplyDefault); + if (stat != kOfxStatOK && stat != kOfxStatReplyDefault) { + LogOfxFailure("getRegionOfInterest", stat, instance); + MarkRenderFailure(destination); + return; + } // render a frame const char *render_field = GetRenderFieldForParams(destination_params); stat = instance->renderAction(0, render_field, renderWindow, renderScale, true, interactive, interactive); - assert(stat == kOfxStatOK); + if (stat != kOfxStatOK && stat != kOfxStatReplyDefault) { + LogOfxFailure("render", stat, instance); + MarkRenderFailure(destination); + instance->endRenderAction(0, numFramesToRender, 1.0, interactive, + renderScale, true, interactive); + return; + } // get the output image buffer (CPU path only) std::shared_ptr output_image; if (!use_opengl) { output_image = clip->getOutputImage(frame); if (!output_image) { + qWarning().noquote() + << "OFX getOutputImage returned null for plugin=" + << PluginIdForInstance(instance); + MarkRenderFailure(destination); instance->endRenderAction(frame, numFramesToRender, 1.0, interactive, renderScale, true, interactive); return; @@ -424,12 +629,27 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: if (!use_opengl) { AVFramePtr frame_ptr = create_avframe_from_ofx_image(*output_image); if (!frame_ptr) { + frame_ptr = create_avframe_from_ofx_image_with_params( + *output_image, output_params); + } + if (!frame_ptr) { + qWarning().noquote() + << "OFX output image conversion failed for plugin=" + << PluginIdForInstance(instance); instance->endRenderAction(0, numFramesToRender, 1.0, interactive, renderScale, true, interactive); return; } AVFramePtr converted = ConvertFrameIfNeeded(frame_ptr, destination_params); destination->handleFrame(converted); + if (destination->renderer() && converted && converted->data[0]) { + int linesize_pixels = + LinesizeToPixels(destination_params, converted->linesize[0]); + if (linesize_pixels <= 0) { + linesize_pixels = destination_params.effective_width(); + } + destination->Upload(converted->data[0], linesize_pixels); + } } else { AVFramePtr frame_ptr = ReadbackTextureToFrame(destination, destination_params); diff --git a/tests/gtest/CMakeLists.txt b/tests/gtest/CMakeLists.txt index 3896f93ca..4eaf2db9d 100644 --- a/tests/gtest/CMakeLists.txt +++ b/tests/gtest/CMakeLists.txt @@ -18,6 +18,8 @@ add_executable(olive-gtest plugin_support_test.cpp plugin_support_image_test.cpp plugin_support_clip_test.cpp + plugin_render_pipeline_test.cpp + plugin_ofx_integration_test.cpp codec_frame_test.cpp codec_exportcodec_test.cpp codec_exportformat_test.cpp diff --git a/tests/gtest/plugin_ofx_integration_test.cpp b/tests/gtest/plugin_ofx_integration_test.cpp new file mode 100644 index 000000000..4ceb00a6e --- /dev/null +++ b/tests/gtest/plugin_ofx_integration_test.cpp @@ -0,0 +1,120 @@ +#include + +#include + +extern "C" { +#include +} + +#include "common/ffmpegutils.h" +#include "node/value.h" +#include "pluginSupport/OliveHost.h" +#include "pluginSupport/OlivePluginInstance.h" +#include "render/job/pluginjob.h" +#include "render/plugin/pluginrenderer.h" +#include "render/texture.h" +#include "render/videoparams.h" + +namespace { + +olive::TexturePtr CreateSolidTexture(const olive::VideoParams ¶ms) +{ + olive::AVFramePtr frame = olive::CreateAVFramePtr(); + frame->format = olive::FFmpegUtils::GetFFmpegPixelFormat( + params.format(), params.channel_count()); + frame->width = params.width(); + frame->height = params.height(); + if (frame->format == AV_PIX_FMT_NONE) { + return nullptr; + } + if (av_frame_get_buffer(frame.get(), 0) < 0) { + return nullptr; + } + if (av_frame_make_writable(frame.get()) < 0) { + return nullptr; + } + + const int linesize = frame->linesize[0]; + for (int y = 0; y < frame->height; ++y) { + std::memset(frame->data[0] + y * linesize, 0x7f, linesize); + } + + olive::TexturePtr texture = std::make_shared(params); + texture->handleFrame(frame); + return texture; +} + +} // namespace + +TEST(PluginIntegration, ChromaKeyerCreateAndRender) +{ + const char *itest = std::getenv("OAK_OFX_ITEST"); + if (!itest || std::string(itest) != "1") { + GTEST_SKIP() << "OAK_OFX_ITEST not enabled"; + } + + const char *path = std::getenv("OAK_OFX_PLUGIN_PATH"); + if (!path || std::string(path).empty()) { + GTEST_SKIP() << "OAK_OFX_PLUGIN_PATH not set"; + } + + std::string plugin_id = "net.sf.openfx.ChromaKeyerPlugin"; + if (const char *env_id = std::getenv("OAK_OFX_PLUGIN_ID")) { + if (*env_id) { + plugin_id = env_id; + } + } + + olive::plugin::loadPlugins(QString::fromUtf8(path)); + + auto *cache = OFX::Host::PluginCache::getPluginCache(); + OFX::Host::Plugin *found = nullptr; + for (auto *plug : cache->getPlugins()) { + if (plug && plug->getIdentifier() == plugin_id) { + found = plug; + break; + } + } + if (!found) { + GTEST_SKIP() << "Plugin not found: " << plugin_id; + } + + auto *image_effect = + dynamic_cast(found); + ASSERT_TRUE(image_effect); + + const auto &contexts = image_effect->getContexts(); + std::string context = kOfxImageEffectContextFilter; + if (!contexts.empty() && + contexts.find(kOfxImageEffectContextFilter) == contexts.end()) { + context = *contexts.begin(); + } + + OFX::Host::ImageEffect::Instance *instance = + image_effect->createInstance(context, nullptr); + ASSERT_TRUE(instance); + + auto *olive_instance = + dynamic_cast(instance); + ASSERT_TRUE(olive_instance); + + olive::VideoParams params(320, 240, olive::core::PixelFormat::U8, 4); + olive_instance->setVideoParam(params); + + olive::TexturePtr input = CreateSolidTexture(params); + ASSERT_TRUE(input); + + olive::NodeValueRow row; + row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName), + olive::NodeValue(olive::NodeValue::kTexture, input)); + row.insert(QStringLiteral("Bg"), + olive::NodeValue(olive::NodeValue::kTexture, input)); + + olive::plugin::PluginJob job(instance, nullptr, row); + olive::TexturePtr output = std::make_shared(params); + + olive::plugin::PluginRenderer renderer; + renderer.RenderPlugin(input, job, output.get(), params, true, false); + + EXPECT_TRUE(output->frame()); +} diff --git a/tests/gtest/plugin_render_pipeline_test.cpp b/tests/gtest/plugin_render_pipeline_test.cpp new file mode 100644 index 000000000..8cf406f23 --- /dev/null +++ b/tests/gtest/plugin_render_pipeline_test.cpp @@ -0,0 +1,54 @@ +#include + +#include "node/traverser.h" +#include "node/value.h" +#include "render/job/pluginjob.h" +#include "render/texture.h" +#include "render/videoparams.h" + +namespace { + +class PluginJobTraverser : public olive::NodeTraverser { +public: + void Resolve(olive::NodeValue &value) + { + ResolveJobs(value); + } + + bool called() const + { + return called_; + } + +protected: + olive::TexturePtr ProcessPluginJob(olive::TexturePtr /*texture*/, + olive::TexturePtr destination, + const olive::Node * /*node*/) override + { + called_ = true; + return destination; + } + +private: + bool called_ = false; +}; + +} // namespace + +TEST(PluginRenderPipeline, PluginJobIsResolved) +{ + olive::VideoParams params(320, 240, olive::core::PixelFormat::U8, 4); + + olive::plugin::PluginJob job(nullptr, nullptr, olive::NodeValueRow()); + olive::TexturePtr job_tex = olive::Texture::Job(params, job); + + olive::NodeValue val(olive::NodeValue::kTexture, job_tex); + + PluginJobTraverser traverser; + traverser.SetCacheVideoParams(params); + traverser.Resolve(val); + + EXPECT_TRUE(traverser.called()); + ASSERT_TRUE(val.toTexture()); + EXPECT_NE(val.toTexture().get(), job_tex.get()); +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 000000000..dcc721db6 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,12 @@ +{ + "name": "oak-video-editor", + "version-string": "0.0.0", + "dependencies": [ + "ffmpeg", + "openimageio", + "opencolorio", + "openexr", + "expat", + "portaudio" + ] +}