diff --git a/app/node/plugins/Plugin.cpp b/app/node/plugins/Plugin.cpp index 7cc49ad77..cb4c5eed0 100644 --- a/app/node/plugins/Plugin.cpp +++ b/app/node/plugins/Plugin.cpp @@ -239,9 +239,7 @@ BuildDefaultValues(const std::map &pa continue; } const auto &props = param.second->getProperties(); - if (props.getIntProperty(kOfxParamPropSecret) != 0) { - continue; - } + bool is_secret = props.getIntProperty(kOfxParamPropSecret) != 0; const QString input_id = QString::fromStdString(param.second->getName()); if (input_id.isEmpty()) { @@ -381,9 +379,7 @@ olive::plugin::PluginNode::PluginNode( continue; } const auto &props = param.second->getProperties(); - if (props.getIntProperty(kOfxParamPropSecret) != 0) { - continue; - } + bool is_secret = props.getIntProperty(kOfxParamPropSecret) != 0; if (type == NodeValue::kNone) { continue; } @@ -396,6 +392,9 @@ olive::plugin::PluginNode::PluginNode( } else { AddInput(input_id, type); } + if (is_secret) { + SetInputFlag(input_id, kInputFlagHidden); + } const QString label = QString::fromStdString(param.second->getLabel()); if (!label.isEmpty()) { diff --git a/app/pluginSupport/OliveClip.cpp b/app/pluginSupport/OliveClip.cpp index daeb4cf15..864890a84 100644 --- a/app/pluginSupport/OliveClip.cpp +++ b/app/pluginSupport/OliveClip.cpp @@ -346,7 +346,11 @@ const std::string &olive::plugin::OliveClipInstance::getPremult() const } double olive::plugin::OliveClipInstance::getAspectRatio() const { - return params_.pixel_aspect_ratio().toDouble(); + double par = params_.pixel_aspect_ratio().toDouble(); + if (par == 0.0) { + return 1.0; // default PAR when not explicitly set + } + return par; } double olive::plugin::OliveClipInstance::getFrameRate() const { @@ -569,7 +573,17 @@ void olive::plugin::OliveClipInstance::setInputTexture(TexturePtr texture, OfxTi } 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. diff --git a/app/pluginSupport/OlivePluginInstance.cpp b/app/pluginSupport/OlivePluginInstance.cpp index 30a54dbba..eb1033ff2 100644 --- a/app/pluginSupport/OlivePluginInstance.cpp +++ b/app/pluginSupport/OlivePluginInstance.cpp @@ -286,10 +286,11 @@ void OlivePluginInstance::getProjectExtent(double &xSize, double &ySize) const } double OlivePluginInstance::getProjectPixelAspectRatio() const { - return Current::getInstance() - .currentVideoParams() - .pixel_aspect_ratio() - .toDouble(); + double par = params_.pixel_aspect_ratio().toDouble(); + if (par == 0.0) { + return 1.0; // default PAR when not explicitly set + } + return par; } double OlivePluginInstance::getFrameRate() const { @@ -550,7 +551,49 @@ OFX::Host::ImageEffect::ClipInstance *OlivePluginInstance::newClipInstance( int index) { // Create a new clip instance - OFX::Host::ImageEffect::ClipInstance* clipInstance = new OliveClipInstance(plugin, *descriptor, params_); + OliveClipInstance* clipInstance = 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 + } + + clipInstance->setPixelDepth(depth); + clipInstance->setComponents(comp); + return clipInstance; } diff --git a/app/pluginSupport/OlivePluginInstance.h b/app/pluginSupport/OlivePluginInstance.h index 9ae617e86..a060d7137 100644 --- a/app/pluginSupport/OlivePluginInstance.h +++ b/app/pluginSupport/OlivePluginInstance.h @@ -67,7 +67,8 @@ public: : Instance(instance._plugin, *instance._descriptor, instance._context, instance._interactive) { - _clips=instance._clips; + // 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; @@ -75,7 +76,6 @@ public: _outputPreMultiplication=instance._outputPreMultiplication; _outputFielding=instance._outputFielding; _outputFrameRate=instance._outputFrameRate; - } explicit OlivePluginInstance(Instance & instance):Instance(instance){}; ~OlivePluginInstance() override; diff --git a/app/pluginSupport/paraminstance.h b/app/pluginSupport/paraminstance.h index 4ab3f023b..355236204 100644 --- a/app/pluginSupport/paraminstance.h +++ b/app/pluginSupport/paraminstance.h @@ -112,7 +112,16 @@ public: : OFX::Host::Param::IntegerInstance(descriptor, paramSet) , _node(node) , _descriptor(descriptor) - {} + , id(_descriptor.getName().c_str()) + { + try { + value_ = _descriptor.getProperties().getIntProperty(kOfxParamPropDefault); + has_value_ = true; + } catch (...) { + value_ = 0; + has_value_ = false; + } + } void SetNode(const std::shared_ptr &new_node) override { _node = new_node; @@ -128,7 +137,7 @@ public: } QVariant variant=_node->GetStandardValue(id); - if (variant.typeId()==QVariant::Int) { + if (variant.canConvert()) { a=variant.toInt(); return kOfxStatOK; } @@ -145,7 +154,7 @@ public: return kOfxStatErrBadHandle; } QVariant variant=_node->GetValueAtTime(id, rational::fromDouble(time)); - if (variant.typeId()==QVariant::Int) { + if (variant.canConvert()) { data=variant.toInt(); return kOfxStatOK; } @@ -165,7 +174,6 @@ public: auto command = new NodeParamSetSplitStandardValueCommand( NodeInput(_node.get(), _descriptor.getName().c_str()), split); SubmitUndoCommand(_node, command, ParamChangeLabel(_descriptor)); - id=_descriptor.getName().c_str(); return kOfxStatOK; } OfxStatus set(OfxTime time, int data) @@ -180,7 +188,6 @@ public: NodeInput(_node.get(), _descriptor.getName().c_str()), rational::fromDouble(time), data, 0, command, true); SubmitUndoCommand(_node, command, ParamChangeLabel(_descriptor)); - id=_descriptor.getName().c_str(); return kOfxStatOK; } }; @@ -200,6 +207,13 @@ public: , _descriptor(descriptor) { (void)name; + try { + value_ = _descriptor.getProperties().getDoubleProperty(kOfxParamPropDefault); + has_value_ = true; + } catch (...) { + value_ = 0.0; + has_value_ = false; + } } void SetNode(const std::shared_ptr &new_node) override { @@ -315,6 +329,8 @@ public: , _descriptor(descriptor) { (void)name; + value_ = DefaultValue(); + has_value_ = true; } void SetNode(const std::shared_ptr &new_node) override { @@ -403,6 +419,13 @@ public: , _descriptor(descriptor) { (void)name; + try { + value_ = _descriptor.getProperties().getIntProperty(kOfxParamPropDefault); + has_value_ = true; + } catch (...) { + value_ = 0; + has_value_ = false; + } } void SetNode(const std::shared_ptr &new_node) override { @@ -1135,6 +1158,13 @@ public: , _descriptor(descriptor) { (void)name; + try { + value_ = _descriptor.getProperties().getStringProperty(kOfxParamPropDefault); + has_value_ = true; + } catch (...) { + value_.clear(); + has_value_ = false; + } } void SetNode(const std::shared_ptr &new_node) override { diff --git a/app/render/plugin/pluginrenderer.cpp b/app/render/plugin/pluginrenderer.cpp index 5d9fd971f..c9810f6d9 100644 --- a/app/render/plugin/pluginrenderer.cpp +++ b/app/render/plugin/pluginrenderer.cpp @@ -36,11 +36,14 @@ #include #include #include +#include #include #include #include #define GL_PREAMBLE //QMutexLocker __l(&global_opengl_mutex); #include "pluginrenderer.h" +#include "core.h" +#include "undo/undostack.h" #include "pluginSupport/OliveClip.h" #include "pluginSupport/OlivePluginInstance.h" #include "common/ffmpegutils.h" @@ -1229,8 +1232,8 @@ static void LogImageProps(const char *label, OFX::Host::ImageEffect::Image *image) { if (!image) { - qWarning().noquote() << "OFX image props" << label << ""; - return; + /*qWarning().noquote() << "OFX image props" << label << ""; + return;*/ } int bounds[4] = {0, 0, 0, 0}; int rod[4] = {0, 0, 0, 0}; @@ -1241,13 +1244,13 @@ static void LogImageProps(const char *label, image->getStringProperty(kOfxImageEffectPropPixelDepth); const std::string &components = image->getStringProperty(kOfxImageEffectPropComponents); - qWarning().noquote() + /*qWarning().noquote() << "OFX image props" << label << "pixelDepth=" << QString::fromStdString(depth) << "components=" << QString::fromStdString(components) << "rowBytes=" << row_bytes << "bounds=" << bounds[0] << bounds[1] << bounds[2] << bounds[3] - << "rod=" << rod[0] << rod[1] << rod[2] << rod[3]; + << "rod=" << rod[0] << rod[1] << rod[2] << rod[3];*/ } // 作用:渲染失败时标记目标画面(紫色)提示错误。 @@ -1258,6 +1261,30 @@ static void MarkRenderFailure(olive::TexturePtr destination) destination->renderer()->ClearDestination(destination.get(), 1.0, 0.0, 1.0, 1.0); } } + +/// Show an error dialog and undo the last operation. Must be called from the GUI thread. +static void ShowErrorDialogAndUndo(const QString &message) +{ + if (auto *core = olive::Core::instance()) { + if (auto *stack = core->undo_stack()) { + if (stack->CanUndo()) { + stack->undo(); + } + } + } + QMessageBox::critical(nullptr, QObject::tr("Plugin Error"), message); +} + +/// Schedule an error dialog + undo on the GUI thread from a render thread. +static void ScheduleErrorDialogAndUndo(const QString &message) +{ + if (auto *app = QCoreApplication::instance()) { + QMetaObject::invokeMethod(app, [message]() { + ShowErrorDialogAndUndo(message); + }, Qt::QueuedConnection); + } +} + static olive::AVFramePtr DownloadTextureToFrame(const olive::TexturePtr &tex) { if (!tex || tex->IsDummy() || !tex->renderer()) { @@ -1357,6 +1384,16 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: destination->id().isValid(); if (olive_instance) { olive_instance->setVideoParam(destination_params); + // Ensure all clip instances inherit the project's params so that + // getAspectRatio/getFrameRate etc. return valid values before + // createInstanceAction (which may call fetchClip and query them). + for (int i = 0; i < olive_instance->getNClips(); ++i) { + OliveClipInstance *clip = dynamic_cast( + olive_instance->getNthClip(i)); + if (clip) { + clip->setParams(destination_params); + } + } } // current render scale of 1 @@ -1439,21 +1476,44 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: } } - // call getClipPreferences to know which format plugin requires - OFX::Host::Property::Set args; - args.setDoubleProperty(kOfxPropTime, frame); - double render_scale_array[] = { - renderScale.x, renderScale.y - }; - args.setDoublePropertyN(kOfxImageEffectPropRenderScale, render_scale_array, 2); - instance->setupClipPreferencesArgs(args); - // now we need to call getClipPreferences on the instance so that it does - // the clip component/depth logic and caches away the components and depth. - bool ok = instance->getClipPreferences(); + // call getClipPreferences to know which format plugin requires. + // getClipPreferences internally calls setupClipPreferencesArgs, which + // validates that all connected input clips have the same frame rate. + // Wrap in try/catch because setupClipPreferencesArgs throws on validation + // failure and would otherwise crash the render thread. + bool ok = false; + try { + ok = instance->getClipPreferences(); + } catch (const OFX::Host::Property::Exception &e) { + qWarning().noquote() << "OFX getClipPreferences threw exception for plugin=" + << PluginIdForInstance(instance) + << "stat=" << e.getStatus(); + MarkRenderFailure(destination); + ScheduleErrorDialogAndUndo( + QObject::tr("Plugin %1 failed because connected inputs have different frame rates.\n" + "The last operation has been undone.") + .arg(PluginIdForInstance(instance))); + return; + } catch (const std::exception &e) { + qWarning().noquote() << "OFX getClipPreferences threw exception for plugin=" + << PluginIdForInstance(instance) + << "what=" << e.what(); + MarkRenderFailure(destination); + ScheduleErrorDialogAndUndo( + QObject::tr("Plugin %1 encountered an error: %2\n" + "The last operation has been undone.") + .arg(PluginIdForInstance(instance), + QString::fromUtf8(e.what()))); + return; + } if (!ok) { qWarning().noquote() << "OFX getClipPreferences failed for plugin=" << PluginIdForInstance(instance); MarkRenderFailure(destination); + ScheduleErrorDialogAndUndo( + QObject::tr("Plugin %1 failed to get clip preferences.\n" + "The last operation has been undone.") + .arg(PluginIdForInstance(instance))); return; } /// RoI is in canonical coords. @@ -1526,9 +1586,19 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: stat = instance->getRegionOfInterestAction(frame, renderScale, regionOfInterest, rois); if (stat != kOfxStatOK && stat != kOfxStatReplyDefault) { - LogOfxFailure("getRegionOfInterest", stat, instance); - MarkRenderFailure(destination); - return; + // Some plugins (e.g. CImg filters) return BadHandle from getRegionOfInterest + // when internal clip/property handles are not fully initialized. + // Treat this as a non-fatal error and fall back to default RoI. + if (stat == kOfxStatErrBadHandle) { + qWarning().noquote() + << "OFX getRegionOfInterest returned BadHandle for plugin=" + << PluginIdForInstance(instance) + << "- using default RoI"; + } else { + LogOfxFailure("getRegionOfInterest", stat, instance); + MarkRenderFailure(destination); + return; + } } // set correct format for output // Query plugin-supported depths and pick best according to our priority: diff --git a/tests/gtest/plugin_ofx_misc_test.cpp b/tests/gtest/plugin_ofx_misc_test.cpp index 4a7777621..68b6cd282 100644 --- a/tests/gtest/plugin_ofx_misc_test.cpp +++ b/tests/gtest/plugin_ofx_misc_test.cpp @@ -85,7 +85,10 @@ TexturePtr CreateSolidTexture(const VideoParams ¶ms, uint32_t fill_value = 0 } // Helper to create a gradient texture -TexturePtr CreateGradientTexture(const VideoParams ¶ms) +// For U8: gradient is 0-255 per byte +// For Float: gradient is 0.0-1.0 per component +template +TexturePtr CreateGradientTextureT(const VideoParams ¶ms, float scale) { AVFramePtr frame = CreateAVFramePtr(); frame->format = FFmpegUtils::GetFFmpegPixelFormat( @@ -104,9 +107,9 @@ TexturePtr CreateGradientTexture(const VideoParams ¶ms) const int linesize = frame->linesize[0]; for (int y = 0; y < frame->height; ++y) { - uint8_t *row = frame->data[0] + y * linesize; - uint8_t value = static_cast((y * 255) / frame->height); - for (int x = 0; x < linesize; ++x) { + T *row = reinterpret_cast(frame->data[0] + y * linesize); + T value = static_cast((y * scale) / frame->height); + for (int x = 0; x < frame->width * params.channel_count(); ++x) { row[x] = value; } } @@ -116,6 +119,21 @@ TexturePtr CreateGradientTexture(const VideoParams ¶ms) return texture; } +TexturePtr CreateGradientTexture(const VideoParams ¶ms) +{ + switch (params.format()) { + case core::PixelFormat::U8: + return CreateGradientTextureT(params, 255.0f); + case core::PixelFormat::U16: + return CreateGradientTextureT(params, 65535.0f); + case core::PixelFormat::F16: + case core::PixelFormat::F32: + return CreateGradientTextureT(params, 1.0f); + default: + return nullptr; + } +} + // Helper function to find and render a plugin bool RenderPlugin(const std::string &plugin_id, const VideoParams ¶ms, @@ -217,7 +235,7 @@ TEST(PluginMisc, MirrorHorizontal) } // Mirror plugin typically works with 8-bit - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateGradientTexture(params); ASSERT_NE(input, nullptr); @@ -239,7 +257,7 @@ TEST(PluginMisc, TransformTranslate) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -261,7 +279,7 @@ TEST(PluginMisc, ColorCorrect) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U16, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -279,7 +297,7 @@ TEST(PluginMisc, Saturation) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -301,7 +319,7 @@ TEST(PluginMisc, GaussianBlur) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -324,7 +342,7 @@ TEST(PluginMisc, Crop) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -342,7 +360,7 @@ TEST(PluginMisc, Grade) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U16, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -364,7 +382,7 @@ TEST(PluginMisc, NonExistentPlugin) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -386,7 +404,7 @@ TEST(PluginMisc, CImgSharpen) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -404,7 +422,7 @@ TEST(PluginMisc, CImgDenoise) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -416,6 +434,24 @@ TEST(PluginMisc, CImgDenoise) EXPECT_TRUE(result) << "CImgDenoise plugin should produce output"; } +TEST(PluginMisc, CImgBilateral) +{ + if (ShouldSkipTest()) { + GTEST_SKIP() << "OFX integration test not enabled"; + } + + VideoParams params(320, 240, core::PixelFormat::F32, 4); + TexturePtr input = CreateSolidTexture(params, 0x80); + ASSERT_NE(input, nullptr); + + NodeValueRow row; + row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName), + NodeValue(NodeValue::kTexture, input)); + + bool result = RenderPlugin("net.sf.cimg.CImgBilateral", params, row, true); + EXPECT_TRUE(result) << "CImgBilateral plugin should produce output"; +} + // ============================================================================ // Merge Plugin Tests (for transitions/compositing) // ============================================================================ @@ -426,7 +462,7 @@ TEST(PluginMisc, MergeOver) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -450,7 +486,7 @@ TEST(PluginMisc, Keyer) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U16, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -472,7 +508,7 @@ TEST(PluginMisc, CornerPin) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -490,7 +526,7 @@ TEST(PluginMisc, LensDistortion) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -512,7 +548,7 @@ TEST(PluginMisc, Invert) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -530,7 +566,7 @@ TEST(PluginMisc, Gamma) GTEST_SKIP() << "OFX integration test not enabled"; } - VideoParams params(320, 240, core::PixelFormat::U8, 4); + VideoParams params(320, 240, core::PixelFormat::F32, 4); TexturePtr input = CreateSolidTexture(params, 0x80); ASSERT_NE(input, nullptr); @@ -568,6 +604,27 @@ TEST(PluginMisc, ListAvailablePlugins) SUCCEED(); } +TEST(PluginMisc, CImgGuided_MultiInput) +{ + if (ShouldSkipTest()) GTEST_SKIP() << "OFX integration test not enabled"; + // CImgGuided is a multi-input plugin (Source + Mask). + // This test verifies that connecting both inputs does not trigger + // the frame-rate mismatch exception in setupClipPreferencesArgs. + VideoParams params(320, 240, core::PixelFormat::F32, 4); + TexturePtr source = CreateSolidTexture(params, 0x80); + TexturePtr mask = CreateSolidTexture(params, 0x40); + ASSERT_NE(source, nullptr); + ASSERT_NE(mask, nullptr); + + NodeValueRow row; + row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName), + NodeValue(NodeValue::kTexture, source)); + row.insert(QStringLiteral("Mask"), + NodeValue(NodeValue::kTexture, mask)); + bool result = RenderPlugin("net.sf.cimg.CImgGuided", params, row, true); + EXPECT_TRUE(result) << "CImgGuided plugin should produce output with both Source and Mask connected"; +} + } // namespace test } // namespace plugin } // namespace olive diff --git a/third_party/openfx/HostSupport/src/ofxhImageEffect.cpp b/third_party/openfx/HostSupport/src/ofxhImageEffect.cpp index 042e44491..8cef2745d 100644 --- a/third_party/openfx/HostSupport/src/ofxhImageEffect.cpp +++ b/third_party/openfx/HostSupport/src/ofxhImageEffect.cpp @@ -29,6 +29,7 @@ #include #include +#include namespace OFX { @@ -2004,31 +2005,47 @@ namespace OFX { const OfxRectD *h2, OfxPropertySetHandle *h3) { + ClipInstance *clipInstance = nullptr; try { if (!h3) { + std::cerr << "[clipGetImage] h3 is null" << std::endl; return kOfxStatErrBadHandle; } - ClipInstance *clipInstance = reinterpret_cast(h1); + clipInstance = reinterpret_cast(h1); - if (!clipInstance || !clipInstance->verifyMagic()) { + if (!clipInstance) { + std::cerr << "[clipGetImage] clipInstance is null" << std::endl; + *h3 = NULL; + return kOfxStatErrBadHandle; + } + if (!clipInstance->verifyMagic()) { + std::cerr << "[clipGetImage] verifyMagic failed for clip=" << clipInstance->getName() << std::endl; *h3 = NULL; return kOfxStatErrBadHandle; } Image* image = clipInstance->getImage(time,h2); if(!image) { + std::cerr << "[clipGetImage] getImage returned null for clip=" << clipInstance->getName() << " time=" << time << std::endl; *h3 = NULL; - return kOfxStatFailed; } *h3 = image->getPropHandle(); + if (!*h3) { + std::cerr << "[clipGetImage] getPropHandle returned null for clip=" << clipInstance->getName() << std::endl; + return kOfxStatErrBadHandle; + } return kOfxStatOK; + } catch (const std::exception &e) { + std::cerr << "[clipGetImage] exception: " << e.what() << " clip=" << (clipInstance ? clipInstance->getName() : "null") << std::endl; + *h3 = NULL; + return kOfxStatErrBadHandle; } catch (...) { + std::cerr << "[clipGetImage] unknown exception clip=" << (clipInstance ? clipInstance->getName() : "null") << std::endl; *h3 = NULL; - return kOfxStatErrBadHandle; } } @@ -2095,28 +2112,40 @@ namespace OFX { OfxTime time, OfxRectD *bounds) { + ClipInstance *clipInstance = nullptr; try { if (!bounds) { + std::cerr << "[clipGetRegionOfDefinition] bounds is null" << std::endl; return kOfxStatErrBadHandle; } - ClipInstance *clipInstance = reinterpret_cast(clip); + clipInstance = reinterpret_cast(clip); - if (!clipInstance || !clipInstance->verifyMagic()) { + if (!clipInstance) { + std::cerr << "[clipGetRegionOfDefinition] clipInstance is null" << std::endl; + bounds->x1 = bounds->y1 = bounds->x2 = bounds->y2 = 0.; + return kOfxStatErrBadHandle; + } + if (!clipInstance->verifyMagic()) { + std::cerr << "[clipGetRegionOfDefinition] verifyMagic failed for clip=" << clipInstance->getName() << std::endl; bounds->x1 = bounds->y1 = bounds->x2 = bounds->y2 = 0.; - return kOfxStatErrBadHandle; } *bounds = clipInstance->getRegionOfDefinition(time); if (bounds->x2 < bounds->x1 || bounds->y2 < bounds->y1) { - // the RoD is invalid (empty is OK) - + std::cerr << "[clipGetRegionOfDefinition] invalid RoD for clip=" << clipInstance->getName() << std::endl; return kOfxStatFailed; } return kOfxStatOK; + } catch (const std::exception &e) { + std::cerr << "[clipGetRegionOfDefinition] exception: " << e.what() << " clip=" << (clipInstance ? clipInstance->getName() : "null") << std::endl; + bounds->x1 = bounds->y1 = bounds->x2 = bounds->y2 = 0.; + return kOfxStatErrBadHandle; } catch (...) { + std::cerr << "[clipGetRegionOfDefinition] unknown exception clip=" << (clipInstance ? clipInstance->getName() : "null") << std::endl; + bounds->x1 = bounds->y1 = bounds->x2 = bounds->y2 = 0.; return kOfxStatErrBadHandle; } }