From e851653fa3fc4b15709e4f109fde5d6a18c23aae Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Fri, 16 Jan 2026 21:11:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=9D=E8=AF=95=E4=BF=AE=E5=A4=8D=E7=B2=89?= =?UTF-8?q?=E7=B4=AB=E5=B1=8F=E6=9C=AA=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 +- TODO-zh.md | 65 -- app/node/plugins/Plugin.cpp | 155 +++- app/pluginSupport/OliveClip.cpp | 33 +- app/pluginSupport/OliveClip.h | 4 +- app/pluginSupport/OlivePluginInstance.cpp | 13 + app/pluginSupport/OlivePluginInstance.h | 5 +- app/pluginSupport/image.h | 5 +- app/pluginSupport/paraminstance.h | 97 +- app/render/opengl/openglrenderer.cpp | 15 +- app/render/opengl/openglrenderer.h | 4 +- app/render/plugin/pluginrenderer.cpp | 849 ++++++++++++++++-- app/render/plugin/pluginrenderer.h | 10 + app/render/previewautocacher.cpp | 20 +- app/render/renderer.cpp | 32 +- app/render/renderer.h | 9 + app/render/rendermanager.cpp | 2 +- app/render/renderprocessor.cpp | 50 +- app/render/texture.cpp | 6 +- app/render/texture.h | 16 +- app/render/videoparams.h | 15 +- .../nodeparamview/nodeparamviewcontext.cpp | 4 +- .../nodeparamview/nodeparamviewdockarea.cpp | 10 + .../nodeparamview/nodeparamviewdockarea.h | 1 + .../nodeparamviewwidgetbridge.cpp | 59 +- app/widget/viewer/viewer.cpp | 47 +- app/widget/viewer/viewerdisplay.cpp | 32 +- docs/ofx-pluginrenderer-functions-zh.md | 44 + ext/core | 2 +- .../HostSupport/include/ofxhUtilities.h | 4 +- 30 files changed, 1377 insertions(+), 238 deletions(-) delete mode 100644 TODO-zh.md create mode 100644 docs/ofx-pluginrenderer-functions-zh.md diff --git a/.gitignore b/.gitignore index 1fd4fdcf0..ac79e8897 100644 --- a/.gitignore +++ b/.gitignore @@ -98,4 +98,9 @@ compile_flags.txt *_qmlcache.qrc .idea/ -.vimspector.json \ No newline at end of file +.vimspector.json + +AGENTS.md +.codex +operations-log.md +verification.md \ No newline at end of file diff --git a/TODO-zh.md b/TODO-zh.md deleted file mode 100644 index cba41ad2d..000000000 --- a/TODO-zh.md +++ /dev/null @@ -1,65 +0,0 @@ -# OpenFX 支持 TODO(中文说明) - -下面是 README 里 OpenFX TODO 的中文翻译与详细说明。每条都尽量解释“是什么、为什么需要、该往哪里改”。 - -1) 实现插件发现与加载流程 -- 现状:`app/node/factory.cpp` 自己临时创建 Host/PluginCache 并扫描,`loadPlugins()` 只是一段占位逻辑。 -- 为什么需要:没有统一的发现/加载入口,插件扫描路径和实例化流程很容易分叉,后续维护困难。 -- 已完成: - - 在 `app/pluginSupport/OliveHost.cpp` 实现 `loadPlugins()`:创建 Host/PluginCache,注册到 OFX 全局缓存,设置 host path,按需追加路径并扫描插件文件。 - - `app/node/factory.cpp` 统一通过 `loadPlugins()` 初始化插件,再从全局缓存取插件注册到节点库。 - -2) 输出剪辑图像的缓冲区管理(Output Clip) -- 现状:`OliveClipInstance::getImage()` 返回空的 OFX Image,没有分配像素内存,也没有设置 `kOfxImagePropData`。 -- 为什么需要:插件渲染时会往 `kOfxImagePropData` 写入像素,如果这里没分配就会导致崩溃或黑屏。 -- 可能改动:为 Output Clip 分配像素缓冲(例如 `std::vector`),填充 `kOfxImagePropData`、`kOfxImagePropRowBytes`、`kOfxImagePropBounds`,并保证生命周期覆盖渲染过程。 - -3) 输入剪辑图像的拉取(Input Clip Fetch) -- 现状:`OliveClipInstance::getImage()` 对输入剪辑直接返回一个空 Image,没有真正把输入帧填进去。 -- 为什么需要:多数插件需要输入图像做处理,没有输入就无法正确工作。 -- 可能改动:根据当前时间 `time` 从渲染管线/缓存/纹理中取出输入帧,设置 OFX Image 的 data/bounds/rowBytes 等属性并返回。 - -4) 渲染路径中设置每帧输出数据与 ROD/Bounds -- 现状:`app/render/plugin/pluginrenderer.cpp` 中已接了 Image->AVFrame,但仍需要保证每帧输出图像属性正确(ROD/Bounds)。 -- 为什么需要:插件对 ROD 和 bounds 非常敏感,用错会导致裁剪错误或错位。 -- 可能改动:在渲染前或 render action 前,按当前时间/ROI 计算并设置 Output Clip 的 `kOfxImagePropBounds`、`kOfxImagePropRegionOfDefinition` 等。 - -5) 参数类型支持不完整(Param Instances) -- 现状:`OlivePluginInstance::newParam()` 只支持少量类型,`app/node/plugins/Plugin.cpp` 里也没有处理 Group/Page 等。 -- 为什么需要:复杂插件大量依赖 String/3D/Custom 等参数,不支持就会缺参数或崩溃。 -- 可能改动:补齐 String、Double3D/Integer3D、Group/Page、Custom/Bytes 等参数实例,并在节点输入映射里增加对应类型。 - -6) editBegin/editEnd、Progress、Timeline 等回调还只是空实现 -- 现状:`OlivePluginInstance` 中多处函数是空壳或默认返回。 -- 为什么需要:插件在编辑参数、显示进度、根据时间线上下文渲染时依赖这些回调。 -- 已完成: - - editBegin/editEnd:实现编辑会话计数,保证调用合法并可用于后续扩展。 - - progressStart/Update/End:接入 `ProgressDialog`,支持取消并返回给插件停止信号。 - - timeLineGetTime/GotoTime/Bounds:关联当前活跃的时间线/面板播放头,读取与设置时间。 - -7) 持久消息展示与清理机制需要完善 -- 现状:我们已在 Host/Instance 里保存消息并能弹窗,但 UI 面板还需要稳定地展示、更新和清理。 -- 为什么需要:插件经常用 persistent message 提示错误或警告,需要可追踪、可清除。 -- 已完成: - - Host/Instance 持久消息保存,节点右上角显示数量徽标。 - - 参数面板顶部展示消息列表,并可点击清除按钮移除消息。 - - 清除/新增消息后通过信号更新 UI。 -8) Project Extent / Fielding 行为待确认 -- 现状:`OlivePluginInstance::getProjectExtent()` 有 “TODO” 注释。 -- 为什么需要:OFX 插件会根据项目尺寸、扫描线场信息做渲染决策。 -- 已完成: - - `getProjectExtent()` 明确返回项目宽高,不再留 TODO。 - - `getDefaultOutputFielding()` 根据 `VideoParams::interlacing()` 映射为 OFX fielding。 - - 渲染时 `renderAction()` 的 field 参数根据项目是否交错决定(progressive 用 `None`,interlaced 用 `Both`)。 - -9) OpenGL Render Suite 支持 -- 现状:`OliveClipInstance::loadTexture()` 返回 null,OpenGL 渲染路径未实现。 -- 为什么需要:有些 OFX 插件只支持 OpenGL 渲染,不支持 CPU 渲染。 -- 已完成: - - Host 端声明 `kOfxImageEffectPropOpenGLRenderSupported = true`。 - - `OliveClipInstance::loadTexture()` 返回真实的 OpenGL 纹理句柄。 - - 渲染前设置 Output Clip 的目标纹理,输入纹理走 OpenGL 纹理路径。 - ---- - -如果你希望,我可以把这些 TODO 拆成“优先级 + 预计工作量 + 依赖关系”的形式,方便你逐条推进。 diff --git a/app/node/plugins/Plugin.cpp b/app/node/plugins/Plugin.cpp index da9bf4a35..30ab815f6 100644 --- a/app/node/plugins/Plugin.cpp +++ b/app/node/plugins/Plugin.cpp @@ -21,6 +21,9 @@ #include "render/rendermanager.h" #include "render/job/pluginjob.h" #include "pluginSupport/OlivePluginInstance.h" + +#include +#include static QString ClipLabelForName(const std::string &name, const OFX::Host::ImageEffect::ClipDescriptor *desc) { @@ -150,6 +153,71 @@ olive::plugin::PluginNode::PluginNode( SetInputProperty(input_id, QStringLiteral("ui_page"), page_for_param.value(input_id)); } + if (type == NodeValue::kCombo || type == NodeValue::kStrCombo) { + QStringList option_labels; + QStringList option_values; + const int label_count = + props.getDimension(kOfxParamPropChoiceOption); + const int value_count = + props.getDimension(kOfxParamPropChoiceEnum); + + for (int i = 0; i < label_count; ++i) { + const std::string &label = + props.getStringProperty(kOfxParamPropChoiceOption, i); + option_labels.append(QString::fromStdString(label)); + } + + for (int i = 0; i < value_count; ++i) { + const std::string &value = + props.getStringProperty(kOfxParamPropChoiceEnum, i); + option_values.append(QString::fromStdString(value)); + } + + if (option_labels.isEmpty() && !option_values.isEmpty()) { + option_labels = option_values; + } + if (option_values.isEmpty() && !option_labels.isEmpty()) { + option_values = option_labels; + } + + const int order_count = + props.getDimension(kOfxParamPropChoiceOrder); + if (order_count == option_labels.size() && + option_labels.size() == option_values.size()) { + QVector indices(option_labels.size()); + for (int i = 0; i < indices.size(); ++i) { + indices[i] = i; + } + + std::stable_sort(indices.begin(), indices.end(), + [&](int a, int b) { + return props.getIntProperty( + kOfxParamPropChoiceOrder, + a) < + props.getIntProperty( + kOfxParamPropChoiceOrder, + b); + }); + + QStringList ordered_labels; + QStringList ordered_values; + for (int index : indices) { + ordered_labels.append(option_labels.at(index)); + ordered_values.append(option_values.at(index)); + } + option_labels = ordered_labels; + option_values = ordered_values; + } + + if (!option_labels.isEmpty()) { + SetComboBoxStrings(input_id, option_labels); + if (type == NodeValue::kStrCombo) { + SetInputProperty(input_id, + QStringLiteral("combo_value_str"), + option_values); + } + } + } } const auto &clips = plugin_instance_->getDescriptor().getClips(); @@ -162,9 +230,21 @@ olive::plugin::PluginNode::PluginNode( SetInputName(input_id, ClipLabelForName(entry.first, entry.second)); has_texture_input = true; } - if (!has_texture_input) { - AddInput(kTextureInput, NodeValue::kTexture); - SetInputName(kTextureInput, tr("Texture")); + + + const QString source_id = + QString::fromUtf8(kOfxImageEffectSimpleSourceClipName); + if (HasInputWithID(source_id)) { + SetEffectInput(source_id); + } else if (HasInputWithID(kTextureInput)) { + SetEffectInput(kTextureInput); + } + else { + if (has_texture_input) { + AddInput(kTextureInput, NodeValue::kTexture); + SetInputName(kTextureInput, tr("Texture")); + SetEffectInput(kTextureInput); + } } } @@ -198,23 +278,77 @@ void olive::plugin::PluginNode::ProcessSamples(const NodeValueRow &values, SampleBuffer &output, int index) const { - (void)values; - (void)input; - (void)output; - (void)index; + Q_UNUSED(values) + Q_UNUSED(index) + + if (!input.is_allocated() || input.channel_count() == 0 || + input.sample_count() == 0) { + if (output.is_allocated()) { + output.silence(); + } + return; + } + + if (!output.is_allocated() || + output.channel_count() != input.channel_count() || + output.sample_count() != input.sample_count()) { + output.set_audio_params(input.audio_params()); + output.set_sample_count(input.sample_count()); + output.allocate(); + } + + if (!output.is_allocated()) { + return; + } + + for (int channel = 0; channel < input.channel_count(); ++channel) { + output.fast_set(input, channel); + } } void olive::plugin::PluginNode::GenerateFrame(FramePtr frame, const GenerateJob &job) const { - (void)frame; - (void)job; + Q_UNUSED(job) + + if (!frame) { + return; + } + + if (!frame->is_allocated()) { + frame->allocate(); + } + + if (!frame->is_allocated()) { + return; + } + + std::memset(frame->data(), 0, static_cast(frame->allocated_size())); } void olive::plugin::PluginNode::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const { - TexturePtr tex = value.value(kTextureInput).toTexture(); + for (auto it = value.cbegin(); it != value.cend(); ++it) { + const NodeValue &input_value = it.value(); + if (input_value.type() == NodeValue::kTexture || + input_value.type() == NodeValue::kNone) { + continue; + } + NodeValue tagged = input_value; + tagged.set_tag(it.key()); + table->Push(tagged); + } + + TexturePtr tex = nullptr; + const QString source_key = + QString::fromUtf8(kOfxImageEffectSimpleSourceClipName); + if (value.contains(source_key)) { + tex = value.value(source_key).toTexture(); + } + if (!tex) { + tex = value.value(kTextureInput).toTexture(); + } if (!tex) { for (auto it = value.cbegin(); it != value.cend(); ++it) { if (it.value().type() == NodeValue::kTexture) { @@ -227,6 +361,7 @@ void olive::plugin::PluginNode::Value(const NodeValueRow &value, } if (tex && plugin_instance_) { PluginJob job(plugin_instance_, this, value, globals.time().in()); + table->Push(NodeValue::kTexture, tex->toJob(job), this); } } diff --git a/app/pluginSupport/OliveClip.cpp b/app/pluginSupport/OliveClip.cpp index a3ed569bd..23123a337 100644 --- a/app/pluginSupport/OliveClip.cpp +++ b/app/pluginSupport/OliveClip.cpp @@ -406,23 +406,14 @@ olive::plugin::OliveClipInstance::getImage(OfxTime time, static_cast(std::floor(rod_d.y1)), static_cast(std::ceil(rod_d.x2)), static_cast(std::ceil(rod_d.y2)) }; + (void)optionalBounds; + // Always return full-frame images to keep input data consistent. OfxRectI bounds = rod; - if (optionalBounds) { - bounds.x1 = static_cast(std::floor(optionalBounds->x1)); - bounds.y1 = static_cast(std::floor(optionalBounds->y1)); - bounds.x2 = static_cast(std::ceil(optionalBounds->x2)); - bounds.y2 = static_cast(std::ceil(optionalBounds->y2)); - } - // Clamp bounds to ROD to keep host/plugin coords consistent. - 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); if (name_ == "Output") { if (!images_.contains(time)) { // make a new ref counted image - images_.insert(time, std::make_shared(*const_cast(this), + images_.insert(time, new Image(*const_cast(this), params_, bounds, rod, true)); } @@ -435,13 +426,13 @@ olive::plugin::OliveClipInstance::getImage(OfxTime time, images_[time]->EnsureAllocatedFromParams(params_, bounds, rod, true); // return it - return images_[time].get(); + return images_[time]; } else { if (images_.contains(time)) { - std::shared_ptr image = images_.value(time); + Image* image = images_.value(time); image->EnsureAllocatedFromParams(params_, bounds, rod, false); image->addReference(); - return image.get(); + return image; } // Fetch on demand for the input clip. @@ -455,7 +446,7 @@ olive::plugin::OliveClipInstance::getImage(OfxTime time, } } -std::shared_ptr +OFX::Host::ImageEffect::Image* olive::plugin::OliveClipInstance::getOutputImage(OfxTime time) { if (images_.contains(time)) { @@ -469,7 +460,7 @@ olive::plugin::OliveClipInstance::getOutputImage(OfxTime time) static_cast(std::ceil(rod_d.y2)) }; OfxRectI bounds = rod; - auto image = std::make_shared(*this, params_, bounds, rod, true); + auto image = new Image(*this, params_, bounds, rod, true); images_.insert(time, image); return image; } @@ -530,18 +521,18 @@ void olive::plugin::OliveClipInstance::setInputTexture(TexturePtr texture, OfxTi static_cast(std::ceil(rod_d.x2)), static_cast(std::ceil(rod_d.y2)) }; - std::shared_ptr image; + Image* image; if (images_.contains(time)) { image = images_.value(time); image->EnsureAllocatedFromParams(params_, bounds, regionOfDefinition, false); } else { - image = std::make_shared(*this, params_, bounds, + image = new Image(*this, params_, bounds, regionOfDefinition, false); images_.insert(time, image); } - uint8_t *dst = image->data(); + uint8_t *dst = (uint8_t*)image->data(); if (!dst) { return; } @@ -603,6 +594,8 @@ copy_pixels: std::memcpy(dst + y * dst_row_bytes, src + y * src_row_bytes, copy_bytes); } + + } void olive::plugin::OliveClipInstance::setOutputTexture(TexturePtr texture, diff --git a/app/pluginSupport/OliveClip.h b/app/pluginSupport/OliveClip.h index 357a1bd32..85a4c9b13 100644 --- a/app/pluginSupport/OliveClip.h +++ b/app/pluginSupport/OliveClip.h @@ -44,7 +44,7 @@ public: { params_ = params; } - std::shared_ptr getOutputImage(OfxTime time); + OFX::Host::ImageEffect::Image* getOutputImage(OfxTime time); const std::string &getUnmappedBitDepth() const override; const std::string &getUnmappedComponents() const override; @@ -83,7 +83,7 @@ private: OfxRectD defaultRegionOfDefinitions_; std::string name_; - QMap> images_; + QMap images_; #ifdef OFX_SUPPORTS_OPENGLRENDER QMap input_textures_; QMap output_textures_; diff --git a/app/pluginSupport/OlivePluginInstance.cpp b/app/pluginSupport/OlivePluginInstance.cpp index ac366bb4c..d998d029c 100644 --- a/app/pluginSupport/OlivePluginInstance.cpp +++ b/app/pluginSupport/OlivePluginInstance.cpp @@ -160,6 +160,19 @@ const std::string &OlivePluginInstance::getDefaultOutputFielding() const return FieldOrderForParams(params_); } +void OlivePluginInstance::setNode(std::shared_ptr node) +{ + node_ = node; + for (const auto &entry : getParams()) { + if (!entry.second) { + continue; + } + if (auto *bound = dynamic_cast(entry.second)) { + bound->SetNode(node_); + } + } +} + OfxStatus OlivePluginInstance::vmessage(const char *type, const char *id, const char *format, va_list args) { diff --git a/app/pluginSupport/OlivePluginInstance.h b/app/pluginSupport/OlivePluginInstance.h index 44032a8b5..e1908c94d 100644 --- a/app/pluginSupport/OlivePluginInstance.h +++ b/app/pluginSupport/OlivePluginInstance.h @@ -74,9 +74,10 @@ public: { this->params_=params; } - void setNode(std::shared_ptr node) + void setNode(std::shared_ptr node); + std::shared_ptr node() const { - node_ = node; + return node_; } void setOpenGLEnabled(bool enabled) { diff --git a/app/pluginSupport/image.h b/app/pluginSupport/image.h index 6beb3d6aa..44f21e97b 100644 --- a/app/pluginSupport/image.h +++ b/app/pluginSupport/image.h @@ -21,10 +21,13 @@ #define 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 +#include #include #include namespace olive @@ -41,7 +44,7 @@ public: bool clear = true); ~Image(); uint8_t *data() { - return image_.empty() ? nullptr : image_.data(); + return (uint8_t *)getPointerProperty(kOfxImagePropData); } int width(); int height(); diff --git a/app/pluginSupport/paraminstance.h b/app/pluginSupport/paraminstance.h index 8450e3db2..c627a1e2d 100644 --- a/app/pluginSupport/paraminstance.h +++ b/app/pluginSupport/paraminstance.h @@ -42,7 +42,14 @@ inline QString ParamChangeLabel(const OFX::Host::Param::Descriptor &descriptor) void SubmitUndoCommand(const std::shared_ptr &node, UndoCommand *command, const QString &label); -class PushbuttonInstance : public OFX::Host::Param::PushbuttonInstance { +class NodeBoundParam { +public: + virtual ~NodeBoundParam() = default; + virtual void SetNode(const std::shared_ptr &node) = 0; +}; + +class PushbuttonInstance : public OFX::Host::Param::PushbuttonInstance, + public NodeBoundParam { protected: std::shared_ptr node; OFX::Host::Param::Descriptor *_descriptor; @@ -54,9 +61,14 @@ public: { _descriptor = &descriptor; }; + void SetNode(const std::shared_ptr &new_node) override + { + node = new_node; + } }; -class IntegerInstance : public OFX::Host::Param::IntegerInstance { +class IntegerInstance : public OFX::Host::Param::IntegerInstance, + public NodeBoundParam { protected: std::shared_ptr _node; OFX::Host::Param::Descriptor& _descriptor; @@ -69,6 +81,10 @@ public: , _node(node) , _descriptor(descriptor) {} + void SetNode(const std::shared_ptr &new_node) override + { + _node = new_node; + } OfxStatus get(int &a) { if (!_node) { @@ -137,7 +153,8 @@ public: } }; -class DoubleInstance : public OFX::Host::Param::DoubleInstance { +class DoubleInstance : public OFX::Host::Param::DoubleInstance, + public NodeBoundParam { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; @@ -151,6 +168,10 @@ public: { (void)name; } + void SetNode(const std::shared_ptr &new_node) override + { + node = new_node; + } OfxStatus get(double& data) { if (!node) { @@ -219,7 +240,8 @@ public: } }; -class BooleanInstance : public OFX::Host::Param::BooleanInstance { +class BooleanInstance : public OFX::Host::Param::BooleanInstance, + public NodeBoundParam { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; @@ -233,6 +255,10 @@ public: { (void)name; } + void SetNode(const std::shared_ptr &new_node) override + { + node = new_node; + } OfxStatus get(bool& data) { if (!node) { @@ -293,7 +319,8 @@ public: } }; -class ChoiceInstance : public OFX::Host::Param::ChoiceInstance { +class ChoiceInstance : public OFX::Host::Param::ChoiceInstance, + public NodeBoundParam { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; @@ -307,6 +334,10 @@ public: { (void)name; } + void SetNode(const std::shared_ptr &new_node) override + { + node = new_node; + } OfxStatus get(int& data) { if (!node) { @@ -367,7 +398,8 @@ public: } }; -class RGBAInstance : public OFX::Host::Param::RGBAInstance { +class RGBAInstance : public OFX::Host::Param::RGBAInstance, + public NodeBoundParam { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; @@ -381,6 +413,10 @@ public: { (void)name; } + void SetNode(const std::shared_ptr &new_node) override + { + node = new_node; + } OfxStatus get(double& r,double& g,double& b,double& a) { if (!node) { @@ -472,7 +508,8 @@ public: }; -class RGBInstance : public OFX::Host::Param::RGBInstance { +class RGBInstance : public OFX::Host::Param::RGBInstance, + public NodeBoundParam { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; @@ -486,6 +523,10 @@ public: { (void)name; } + void SetNode(const std::shared_ptr &new_node) override + { + node = new_node; + } OfxStatus get(double& r,double& g,double& b) { if (!node) { @@ -568,7 +609,8 @@ public: } }; -class Double2DInstance : public OFX::Host::Param::Double2DInstance { +class Double2DInstance : public OFX::Host::Param::Double2DInstance, + public NodeBoundParam { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; @@ -582,6 +624,10 @@ public: { (void)name; } + void SetNode(const std::shared_ptr &new_node) override + { + node = new_node; + } OfxStatus get(double& x,double& y) { if (!node) { @@ -653,7 +699,8 @@ public: } }; -class Integer2DInstance : public OFX::Host::Param::Integer2DInstance { +class Integer2DInstance : public OFX::Host::Param::Integer2DInstance, + public NodeBoundParam { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; @@ -667,6 +714,10 @@ public: { (void)name; } + void SetNode(const std::shared_ptr &new_node) override + { + node = new_node; + } OfxStatus get(int& x,int& y) { if (!node) { @@ -738,7 +789,8 @@ public: } }; -class Double3DInstance : public OFX::Host::Param::Double3DInstance { +class Double3DInstance : public OFX::Host::Param::Double3DInstance, + public NodeBoundParam { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; @@ -753,6 +805,10 @@ public: { (void)name; } + void SetNode(const std::shared_ptr &new_node) override + { + node = new_node; + } OfxStatus get(double& x,double& y,double& z) { if (!node) { @@ -832,7 +888,8 @@ public: } }; -class Integer3DInstance : public OFX::Host::Param::Integer3DInstance { +class Integer3DInstance : public OFX::Host::Param::Integer3DInstance, + public NodeBoundParam { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; @@ -847,6 +904,10 @@ public: { (void)name; } + void SetNode(const std::shared_ptr &new_node) override + { + node = new_node; + } OfxStatus get(int& x,int& y,int& z) { if (!node) { @@ -926,7 +987,8 @@ public: } }; -class StringInstance : public OFX::Host::Param::StringInstance { +class StringInstance : public OFX::Host::Param::StringInstance, + public NodeBoundParam { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; @@ -941,6 +1003,10 @@ public: { (void)name; } + void SetNode(const std::shared_ptr &new_node) override + { + node = new_node; + } OfxStatus get(std::string &data) { if (!node) { @@ -1002,7 +1068,8 @@ public: } }; -class CustomInstance : public OFX::Host::Param::CustomInstance { +class CustomInstance : public OFX::Host::Param::CustomInstance, + public NodeBoundParam { protected: std::shared_ptr node; OFX::Host::Param::Descriptor& _descriptor; @@ -1017,6 +1084,10 @@ public: { (void)name; } + void SetNode(const std::shared_ptr &new_node) override + { + node = new_node; + } OfxStatus get(std::string &data) { if (!node) { diff --git a/app/render/opengl/openglrenderer.cpp b/app/render/opengl/openglrenderer.cpp index 255d01719..a7ad4cf0a 100644 --- a/app/render/opengl/openglrenderer.cpp +++ b/app/render/opengl/openglrenderer.cpp @@ -187,6 +187,9 @@ QVariant OpenGLRenderer::CreateNativeTexture(int width, int height, int depth, const void *data, int linesize) { GL_PREAMBLE; + if (!EnsureContextCurrent(__FUNCTION__)) { + return QVariant(); + } bool is_3d = depth > 1; @@ -237,7 +240,10 @@ void OpenGLRenderer::AttachTextureAsDestination(const QVariant &texture) void OpenGLRenderer::DetachTextureAsDestination() { - functions_->glBindFramebuffer(GL_FRAMEBUFFER, 0); + // QOpenGLWidget renders to a non-zero default FBO. + const GLuint default_fbo = + context_ ? context_->defaultFramebufferObject() : 0; + functions_->glBindFramebuffer(GL_FRAMEBUFFER, default_fbo); } void OpenGLRenderer::DestroyNativeTexture(QVariant texture) @@ -423,6 +429,12 @@ void OpenGLRenderer::Blit(QVariant s, AcceleratedJob& a_job, Texture *destinatio { GL_PREAMBLE; try { + if (!destination) { + // Ensure we're drawing to the default framebuffer for this context. + GLuint fbo = context_ ? context_->defaultFramebufferObject() : 0; + functions_->glBindFramebuffer(GL_FRAMEBUFFER, fbo); + } + ShaderJob &s_job=dynamic_cast(a_job); ShaderJob job(s_job); // If this node is iterative, we'll pick up which input here @@ -694,6 +706,7 @@ void OpenGLRenderer::Blit(QVariant s, AcceleratedJob& a_job, Texture *destinatio PRINT_GL_ERRORS; functions_->glDrawArrays(GL_TRIANGLES, 0, blit_vertices.size() / 3); } + } if (destination) { diff --git a/app/render/opengl/openglrenderer.h b/app/render/opengl/openglrenderer.h index cec990eed..094b28072 100644 --- a/app/render/opengl/openglrenderer.h +++ b/app/render/opengl/openglrenderer.h @@ -73,6 +73,8 @@ public: return context_; } + bool EnsureContextCurrent(const char *caller); + protected: virtual void Blit(QVariant shader, olive::AcceleratedJob& job, olive::Texture *destination, @@ -99,8 +101,6 @@ private: static GLenum GetPixelFormat(int channel_count); - bool EnsureContextCurrent(const char *caller); - void PrepareInputTexture(GLenum target, Texture::Interpolation interp); void ClearDestinationInternal(double r = 0.0, double g = 0.0, diff --git a/app/render/plugin/pluginrenderer.cpp b/app/render/plugin/pluginrenderer.cpp index 53a807745..5cff62d59 100644 --- a/app/render/plugin/pluginrenderer.cpp +++ b/app/render/plugin/pluginrenderer.cpp @@ -20,27 +20,44 @@ // // Created by mikesolar on 25-10-19. // +#include "ofxCore.h" +#include "ofxhPropertySuite.h" +#include "olive/core/render/pixelformat.h" #include "render/texture.h" +#include "render/opengl/openglrenderer.h" +#include "node/value.h" +#include "render/videoparams.h" +#include #include #include #include #include #include +#include +#include #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 "ofxhParam.h" #include "ofxImageEffect.h" #include "ofxhUtilities.h" #include "ofxGPURender.h" +#include "olive/core/util/color.h" extern "C"{ #include #include #include } + +// 作用:从 OFX Image 属性推导 FFmpeg 像素格式,并返回每像素字节数。 +// Purpose: Infer FFmpeg pixel format from OFX image properties and return bytes-per-pixel. static AVPixelFormat GetOfxAVPixelFormat(const OFX::Host::ImageEffect::Image &image, int *bytes_per_pixel) { @@ -98,8 +115,158 @@ static AVPixelFormat GetOfxAVPixelFormat(const OFX::Host::ImageEffect::Image &im return pix_fmt; } +// 作用:为插件实例注入当前帧的参数值,避免依赖节点实时回读。 +static void ApplyParamOverrides(OFX::Host::ImageEffect::Instance &instance, + const olive::NodeValueRow &values, + OfxTime time) +{ + const auto ¶ms = instance.getParams(); + for (const auto &entry : params) { + if (!entry.second) { + continue; + } + const QString key = QString::fromStdString(entry.first); + if (!values.contains(key)) { + continue; + } + const olive::NodeValue &value = values.value(key); + if (value.type() == olive::NodeValue::kNone || + value.type() == olive::NodeValue::kTexture || + value.type() == olive::NodeValue::kSamples) { + continue; + } + const std::string &type = entry.second->getType(); + + if (type == kOfxParamTypeInteger) { + if (auto *param = + dynamic_cast( + entry.second)) { + param->set(time, value.data().toInt()); + } + continue; + } + if (type == kOfxParamTypeDouble) { + if (auto *param = + dynamic_cast( + entry.second)) { + param->set(time, value.data().toDouble()); + } + continue; + } + if (type == kOfxParamTypeBoolean) { + if (auto *param = + dynamic_cast( + entry.second)) { + param->set(time, value.data().toBool()); + } + continue; + } + if (type == kOfxParamTypeChoice) { + if (auto *param = + dynamic_cast( + entry.second)) { + param->set(time, value.data().toInt()); + } + continue; + } + if (type == kOfxParamTypeString || type == kOfxParamTypeCustom || + type == kOfxParamTypeBytes || type == kOfxParamTypeStrChoice) { + if (auto *param = + dynamic_cast( + entry.second)) { + const QByteArray utf8 = value.data().toString().toUtf8(); + param->set(time, utf8.constData()); + } + continue; + } + if (type == kOfxParamTypeRGBA) { + if (auto *param = + dynamic_cast( + entry.second)) { + if (value.data().canConvert()) { + const auto c = value.data().value(); + param->set(time, c.red(), c.green(), c.blue(), c.alpha()); + } else if (value.data().canConvert()) { + const QVector4D v = value.data().value(); + param->set(time, v.x(), v.y(), v.z(), v.w()); + } else if (value.data().canConvert()) { + const QVector3D v = value.data().value(); + param->set(time, v.x(), v.y(), v.z(), 1.0); + } + } + continue; + } + if (type == kOfxParamTypeRGB) { + if (auto *param = + dynamic_cast( + entry.second)) { + if (value.data().canConvert()) { + const auto c = value.data().value(); + param->set(time, c.red(), c.green(), c.blue()); + } else if (value.data().canConvert()) { + const QVector4D v = value.data().value(); + param->set(time, v.x(), v.y(), v.z()); + } else if (value.data().canConvert()) { + const QVector3D v = value.data().value(); + param->set(time, v.x(), v.y(), v.z()); + } + } + continue; + } + if (type == kOfxParamTypeDouble2D) { + if (auto *param = + dynamic_cast( + entry.second)) { + if (value.data().canConvert()) { + const QVector2D v = value.data().value(); + param->set(time, v.x(), v.y()); + } + } + continue; + } + if (type == kOfxParamTypeInteger2D) { + if (auto *param = + dynamic_cast( + entry.second)) { + if (value.data().canConvert()) { + const QVector2D v = value.data().value(); + param->set(time, static_cast(v.x()), + static_cast(v.y())); + } + } + continue; + } + if (type == kOfxParamTypeDouble3D) { + if (auto *param = + dynamic_cast( + entry.second)) { + if (value.data().canConvert()) { + const QVector3D v = value.data().value(); + param->set(time, v.x(), v.y(), v.z()); + } + } + continue; + } + if (type == kOfxParamTypeInteger3D) { + if (auto *param = + dynamic_cast( + entry.second)) { + if (value.data().canConvert()) { + const QVector3D v = value.data().value(); + param->set(time, static_cast(v.x()), + static_cast(v.y()), + static_cast(v.z())); + } + } + continue; + } + } +} + static AVPixelFormat GetDestinationAVPixelFormat(const olive::VideoParams ¶ms); +// 作用:读取 clip 偏好(像素深度与分量)并更新 VideoParams。 +// Purpose: Apply clip preferences (depth/components) into VideoParams. static bool ApplyClipPreferencesToParams( const OFX::Host::ImageEffect::ClipInstance &clip, olive::VideoParams *params) @@ -139,6 +306,344 @@ static bool ApplyClipPreferencesToParams( return true; } +// 作用:将 OFX bit depth 字符串映射为内部 PixelFormat。 +// Purpose: Map OFX bit depth string to internal PixelFormat. +static olive::core::PixelFormat PixelFormatFromOfxDepth( + const std::string &depth) +{ + if (depth == kOfxBitDepthByte) { + return olive::core::PixelFormat::U8; + } + if (depth == kOfxBitDepthShort) { + return olive::core::PixelFormat::U16; + } + if (depth == kOfxBitDepthHalf) { + return olive::core::PixelFormat::F16; + } + if (depth == kOfxBitDepthFloat) { + return olive::core::PixelFormat::F32; + } + return olive::core::PixelFormat::INVALID; +} + +// 作用:将内部 PixelFormat 转为 OFX bit depth 字符串。 +// Purpose: Map internal PixelFormat to OFX bit depth string. +static const char *OfxDepthFromPixelFormat(olive::core::PixelFormat format) +{ + switch (format) { + case olive::core::PixelFormat::U8: + return kOfxBitDepthByte; + case olive::core::PixelFormat::U16: + return kOfxBitDepthShort; + case olive::core::PixelFormat::F16: + return kOfxBitDepthHalf; + case olive::core::PixelFormat::F32: + return kOfxBitDepthFloat; + case olive::core::PixelFormat::INVALID: + case olive::core::PixelFormat::COUNT: + break; + } + return kOfxBitDepthNone; +} + +// 作用:将 OFX components 字符串映射为通道数。 +// Purpose: Map OFX components string to channel count. +static int ChannelCountFromOfxComponent(const std::string &components) +{ + if (components == kOfxImageComponentRGBA) { + return 4; + } + if (components == kOfxImageComponentRGB) { + return 3; + } + if (components == kOfxImageComponentAlpha) { + return 1; + } + return 0; +} + +// 作用:将通道数映射为 OFX components 字符串。 +// Purpose: Map channel count to OFX components string. +static const char *OfxComponentsFromChannels(int channel_count) +{ + switch (channel_count) { + case 1: + return kOfxImageComponentAlpha; + case 3: + return kOfxImageComponentRGB; + case 4: + return kOfxImageComponentRGBA; + default: + break; + } + return kOfxImageComponentNone; +} + +// 作用:判断插件是否支持指定像素深度。 +// Purpose: Check whether effect supports a given pixel depth. +static bool EffectSupportsPixelDepth( + const OFX::Host::ImageEffect::Instance &instance, + const std::string &depth) +{ + const auto &effect_props = instance.getDescriptor().getProps(); + const int depth_count = + effect_props.getDimension(kOfxImageEffectPropSupportedPixelDepths); + for (int i = 0; i < depth_count; ++i) { + if (effect_props.getStringProperty( + kOfxImageEffectPropSupportedPixelDepths, i) == depth) { + return true; + } + } + return false; +} + +// 作用:判断 clip 是否支持指定组件格式。 +// Purpose: Check whether clip supports a given components string. +static bool ClipSupportsComponents( + const OFX::Host::ImageEffect::ClipInstance &clip, + const std::string &components) +{ + const auto &supported_components = clip.getSupportedComponents(); + for (const auto &comp : supported_components) { + if (comp == components) { + return true; + } + } + return false; +} + +// 作用:估算从源参数到目标参数的转换代价,用于排序选择。 +// Purpose: Estimate conversion cost from source to target params for ranking. +static int ConversionCost(const olive::VideoParams &src, + const olive::VideoParams &dst) +{ + const int src_bpp = src.channel_count() * src.format().byte_count(); + const int dst_bpp = dst.channel_count() * dst.format().byte_count(); + int cost = std::abs(dst_bpp - src_bpp); + if (src.format() != dst.format()) { + cost += 4; + } + if (src.channel_count() != dst.channel_count()) { + cost += 2; + } + return cost; +} + +// 作用:判断目标参数能否转换为可用的 AVPixelFormat。 +// Purpose: Check if params map to a valid AVPixelFormat. +static bool ParamsConvertible(const olive::VideoParams ¶ms) +{ + return GetDestinationAVPixelFormat(params) != AV_PIX_FMT_NONE; +} + +// 作用:在 clip 偏好无效时,选择一个插件支持的输出格式。 +// Purpose: Pick a supported output format when clip preferences are invalid. +static void ChooseSupportedOutputParams( + const OFX::Host::ImageEffect::Instance &instance, + const OFX::Host::ImageEffect::ClipInstance &clip, + const olive::VideoParams &preferred, + olive::VideoParams *out) +{ + if (!out) { + return; + } + + *out = preferred; + + const char *preferred_components = + OfxComponentsFromChannels(preferred.channel_count()); + if (std::strcmp(preferred_components, kOfxImageComponentNone) != 0 && + ClipSupportsComponents(clip, preferred_components)) { + out->set_channel_count(preferred.channel_count()); + } else if (ClipSupportsComponents(clip, kOfxImageComponentRGBA)) { + out->set_channel_count(4); + } else if (ClipSupportsComponents(clip, kOfxImageComponentRGB)) { + out->set_channel_count(3); + } else if (ClipSupportsComponents(clip, kOfxImageComponentAlpha)) { + out->set_channel_count(1); + } + + const olive::core::PixelFormat preferred_format = preferred.format(); + const std::array candidates = { + preferred_format, + olive::core::PixelFormat::F16, + olive::core::PixelFormat::F32, + olive::core::PixelFormat::U16, + olive::core::PixelFormat::U8, + }; + for (const auto &candidate : candidates) { + if (candidate == olive::core::PixelFormat::INVALID) { + continue; + } + if (!EffectSupportsPixelDepth( + instance, OfxDepthFromPixelFormat(candidate))) { + continue; + } + olive::VideoParams test_params = *out; + test_params.set_format(candidate); + if (!ParamsConvertible(test_params)) { + continue; + } + out->set_format(candidate); + return; + } +} + +static olive::TexturePtr ConvertTextureForParams( + olive::TexturePtr src, + const olive::VideoParams &dst_params); + +// 作用:根据插件能力与偏好选择输入格式并执行转换。 +// Purpose: Select a supported input format and convert texture for the clip. +static olive::TexturePtr ConvertTextureForClip( + const OFX::Host::ImageEffect::Instance &instance, + const OFX::Host::ImageEffect::ClipInstance &clip, + olive::TexturePtr src, + const olive::VideoParams &preferred_params, + bool force_preferred, + olive::VideoParams *out_params) +{ + if (!src || !out_params) { + return nullptr; + } + + const olive::VideoParams &src_params = src->params(); + auto add_candidate = [](std::vector &list, + const olive::VideoParams ¶ms) { + for (const auto &existing : list) { + if (existing.format() == params.format() && + existing.channel_count() == params.channel_count()) { + return; + } + } + list.push_back(params); + }; + + std::vector channel_candidates; + const auto &supported_components = clip.getSupportedComponents(); + for (const auto &comp : supported_components) { + int channels = ChannelCountFromOfxComponent(comp); + if (channels > 0 && + std::find(channel_candidates.begin(), + channel_candidates.end(), + channels) == channel_candidates.end()) { + channel_candidates.push_back(channels); + } + } + if (channel_candidates.empty() && preferred_params.channel_count() > 0) { + channel_candidates.push_back(preferred_params.channel_count()); + } + + std::vector format_candidates; + const auto &effect_props = instance.getDescriptor().getProps(); + const int depth_count = + effect_props.getDimension(kOfxImageEffectPropSupportedPixelDepths); + for (int i = 0; i < depth_count; ++i) { + olive::core::PixelFormat fmt = + PixelFormatFromOfxDepth(effect_props.getStringProperty( + kOfxImageEffectPropSupportedPixelDepths, i)); + if (fmt != olive::core::PixelFormat::INVALID && + std::find(format_candidates.begin(), + format_candidates.end(), + fmt) == format_candidates.end()) { + format_candidates.push_back(fmt); + } + } + if (format_candidates.empty() && + preferred_params.format() != olive::core::PixelFormat::INVALID) { + format_candidates.push_back(preferred_params.format()); + } + + std::vector candidates; + add_candidate(candidates, preferred_params); + + const bool prefer_rgba8 = + (preferred_params.format() == olive::core::PixelFormat::U8 || + preferred_params.format() == olive::core::PixelFormat::INVALID) && + ClipSupportsComponents(clip, kOfxImageComponentRGBA) && + EffectSupportsPixelDepth(instance, kOfxBitDepthByte); + if (prefer_rgba8) { + olive::VideoParams rgba_candidate = src_params; + rgba_candidate.set_format(olive::core::PixelFormat::U8); + rgba_candidate.set_channel_count(4); + if (ParamsConvertible(rgba_candidate)) { + add_candidate(candidates, rgba_candidate); + } + } + + for (olive::core::PixelFormat fmt : format_candidates) { + for (int channels : channel_candidates) { + if (fmt == olive::core::PixelFormat::INVALID || channels <= 0) { + continue; + } + olive::VideoParams candidate = src_params; + candidate.set_format(fmt); + candidate.set_channel_count(channels); + if (!ParamsConvertible(candidate)) { + continue; + } + add_candidate(candidates, candidate); + } + } + + if (candidates.empty()) { + return nullptr; + } + + std::stable_sort(candidates.begin(), candidates.end(), + [&src_params, &preferred_params, prefer_rgba8, force_preferred](const auto &a, + const auto &b) { + if (force_preferred) { + const bool a_pref = (a.format() == preferred_params.format() && + a.channel_count() == preferred_params.channel_count()); + const bool b_pref = (b.format() == preferred_params.format() && + b.channel_count() == preferred_params.channel_count()); + if (a_pref != b_pref) { + return a_pref; + } + } + if (prefer_rgba8) { + const bool a_rgba8 = + a.format() == olive::core::PixelFormat::U8 && + a.channel_count() == 4; + const bool b_rgba8 = + b.format() == olive::core::PixelFormat::U8 && + b.channel_count() == 4; + if (a_rgba8 != b_rgba8) { + return a_rgba8; + } + } + const int cost_a = ConversionCost(src_params, a); + const int cost_b = ConversionCost(src_params, b); + if (cost_a != cost_b) { + return cost_a < cost_b; + } + if (a.format() == preferred_params.format() && + a.channel_count() == preferred_params.channel_count()) { + return true; + } + return false; + }); + + for (const auto &candidate : candidates) { + if (candidate.format() == src_params.format() && + candidate.channel_count() == src_params.channel_count()) { + *out_params = src_params; + return src; + } + olive::TexturePtr converted = + ConvertTextureForParams(src, candidate); + if (converted) { + *out_params = candidate; + return converted; + } + } + + return nullptr; +} + +// 作用:从 OFX Image 复制数据到 AVFrame(按图像属性推导格式)。 +// Purpose: Copy OFX Image data into an AVFrame with inferred format. static olive::AVFramePtr create_avframe_from_ofx_image(OFX::Host::ImageEffect::Image &image) { void *data_ptr = image.getPointerProperty(kOfxImagePropData); @@ -198,6 +703,8 @@ static olive::AVFramePtr create_avframe_from_ofx_image(OFX::Host::ImageEffect::I return frame; } +// 作用:按指定 VideoParams 复制 OFX Image 到 AVFrame。 +// Purpose: Copy OFX Image data into an AVFrame using target VideoParams. static olive::AVFramePtr create_avframe_from_ofx_image_with_params( OFX::Host::ImageEffect::Image &image, const olive::VideoParams ¶ms) @@ -253,6 +760,8 @@ static olive::AVFramePtr create_avframe_from_ofx_image_with_params( return frame; } +// 作用:将 VideoParams 映射为最终输出的 AVPixelFormat。 +// Purpose: Map VideoParams to the final AVPixelFormat. static AVPixelFormat GetDestinationAVPixelFormat(const olive::VideoParams ¶ms) { AVPixelFormat pix_fmt = @@ -272,6 +781,8 @@ static AVPixelFormat GetDestinationAVPixelFormat(const olive::VideoParams ¶m return pix_fmt; } +// 作用:根据交错设置返回 OFX render field 字符串。 +// Purpose: Return OFX render field string based on interlacing. static const char *GetRenderFieldForParams(const olive::VideoParams ¶ms) { switch (params.interlacing()) { @@ -284,6 +795,8 @@ static const char *GetRenderFieldForParams(const olive::VideoParams ¶ms) return kOfxImageFieldNone; } +// 作用:从 GPU 纹理回读到 AVFrame(必要时做格式转换)。 +// Purpose: Read back GPU texture into AVFrame with format conversion if needed. static olive::AVFramePtr ReadbackTextureToFrame(olive::TexturePtr texture, const olive::VideoParams ¶ms) { @@ -365,6 +878,8 @@ static olive::AVFramePtr ReadbackTextureToFrame(olive::TexturePtr texture, return dst; } +// 作用:将字节行跨度转换为像素行跨度。 +// Purpose: Convert byte stride to pixel stride. int olive::plugin::detail::BytesToPixels(int byte_linesize, const olive::VideoParams ¶ms) { @@ -377,6 +892,8 @@ int olive::plugin::detail::BytesToPixels(int byte_linesize, return byte_linesize / bytes_per_pixel; } +// 作用:必要时将 AVFrame 转换为目标 VideoParams 对应格式。 +// Purpose: Convert AVFrame to match destination VideoParams when needed. static olive::AVFramePtr ConvertFrameIfNeeded(olive::AVFramePtr src, const olive::VideoParams &dst_params) { @@ -652,6 +1169,8 @@ static olive::AVFramePtr ConvertFrameIfNeeded(olive::AVFramePtr src, return dst; } +// 作用:从字节行跨度换算像素行跨度。 +// Purpose: Convert byte line size to pixel line size. static int LinesizeToPixels(const olive::VideoParams ¶ms, int linesize_bytes) { const int bytes_per_pixel = @@ -662,6 +1181,8 @@ static int LinesizeToPixels(const olive::VideoParams ¶ms, int linesize_bytes return linesize_bytes / bytes_per_pixel; } +// 作用:将纹理转换为指定 VideoParams(CPU 路径,必要时回读)。 +// Purpose: Convert texture to target VideoParams (CPU path with readback). static olive::TexturePtr ConvertTextureForParams(olive::TexturePtr src, const olive::VideoParams &dst_params) { @@ -681,23 +1202,43 @@ static olive::TexturePtr ConvertTextureForParams(olive::TexturePtr src, frame = ReadbackTextureToFrame(src, src_params); } if (!frame || !frame->data[0]) { - return src; + return nullptr; } olive::AVFramePtr converted = ConvertFrameIfNeeded(frame, dst_params); if (!converted || !converted->data[0]) { - return src; + return nullptr; + } + if (converted->linesize[0] <= 0) { + return nullptr; } - auto dst = std::make_shared(dst_params); - int linesize_pixels = LinesizeToPixels(dst_params, converted->linesize[0]); - if (linesize_pixels <= 0) { - linesize_pixels = dst_params.effective_width(); + olive::TexturePtr dst; + if (auto *renderer = src->renderer()) { + int linesize_pixels = + LinesizeToPixels(dst_params, converted->linesize[0]); + if (linesize_pixels <= 0) { + linesize_pixels = dst_params.effective_width(); + } + dst = renderer->CreateTexture(dst_params, converted->data[0], + linesize_pixels); + } else { + dst = std::make_shared(dst_params); + int linesize_pixels = + LinesizeToPixels(dst_params, converted->linesize[0]); + if (linesize_pixels <= 0) { + linesize_pixels = dst_params.effective_width(); + } + dst->Upload(converted->data[0], linesize_pixels); + } + if (dst) { + dst->handleFrame(converted); } - dst->Upload(converted->data[0], linesize_pixels); return dst; } +// 作用:安全获取插件标识符,便于日志输出。 +// Purpose: Safely fetch plugin identifier for logging. static QString PluginIdForInstance(const OFX::Host::ImageEffect::Instance *instance) { if (!instance) { @@ -710,6 +1251,8 @@ static QString PluginIdForInstance(const OFX::Host::ImageEffect::Instance *insta return QString::fromStdString(plugin->getIdentifier()); } +// 作用:统一 OFX 调用失败日志输出。 +// Purpose: Centralized logging for OFX action failures. static void LogOfxFailure(const char *action, OfxStatus stat, const OFX::Host::ImageEffect::Instance *instance) { @@ -723,13 +1266,77 @@ static void LogOfxFailure(const char *action, OfxStatus stat, << "(" << stat << ")"; } +// 作用:输出 clip 的声明属性与关联 VideoParams,辅助定位格式不一致。 +// Purpose: Log clip declared properties and VideoParams for debugging. +static void LogClipState(const char *label, + const OFX::Host::ImageEffect::ClipInstance *clip, + const olive::VideoParams *params) +{ + if (!clip) { + qWarning().noquote() << "OFX clip state" << label << ""; + return; + } + qWarning().noquote() + << "OFX clip state" << label + << "name=" << QString::fromStdString(clip->getName()) + << "pixelDepth=" << QString::fromStdString(clip->getPixelDepth()) + << "components=" << QString::fromStdString(clip->getComponents()); + if (params) { + qWarning().noquote() + << "OFX clip params" << label + << "width=" << params->width() + << "height=" << params->height() + << "format=" << static_cast(params->format()) + << "channels=" << params->channel_count(); + } +} + +// 作用:输出 OFX Image 的属性(深度/组件/行跨度/边界)。 +// Purpose: Log OFX image properties (depth/components/stride/bounds). +static void LogImageProps(const char *label, + OFX::Host::ImageEffect::Image *image) +{ + if (!image) { + qWarning().noquote() << "OFX image props" << label << ""; + return; + } + int bounds[4] = {0, 0, 0, 0}; + int rod[4] = {0, 0, 0, 0}; + image->getIntPropertyN(kOfxImagePropBounds, bounds, 4); + image->getIntPropertyN(kOfxImagePropRegionOfDefinition, rod, 4); + const int row_bytes = image->getIntProperty(kOfxImagePropRowBytes); + const std::string &depth = + image->getStringProperty(kOfxImageEffectPropPixelDepth); + const std::string &components = + image->getStringProperty(kOfxImageEffectPropComponents); + 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]; +} + +// 作用:渲染失败时标记目标画面(紫色)提示错误。 +// Purpose: Mark render failure on destination (magenta). static void MarkRenderFailure(olive::TexturePtr destination) { if (destination && destination->renderer()) { destination->renderer()->ClearDestination(destination.get(), 1.0, 0.0, 1.0, 1.0); } } +static olive::AVFramePtr DownloadTextureToFrame(const olive::TexturePtr &tex) +{ + if (!tex || tex->IsDummy() || !tex->renderer()) { + return nullptr; + } + const olive::VideoParams ¶ms = tex->params(); + return ReadbackTextureToFrame(tex, params); +} +// 作用:执行 OFX 插件渲染全流程(准备输入、调用动作、处理输出)。 +// Purpose: Run full OFX plugin render flow (inputs, actions, outputs). void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::PluginJob& job, olive::TexturePtr destination, olive::VideoParams destination_params, @@ -752,7 +1359,6 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: supports_opengl && destination && destination->renderer() && destination->id().isValid(); if (olive_instance) { - olive_instance->setOpenGLEnabled(use_opengl); olive_instance->setVideoParam(destination_params); } @@ -760,35 +1366,16 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: OfxPointD renderScale; renderScale.x = renderScale.y = 1.0; - // The render window is in pixel coordinates - // ie: render scale and a PAR of not 1 - OfxRectI renderWindow; - renderWindow.x1 = renderWindow.y1 = 0; - - - renderWindow.x2 = destination_params.width(); - renderWindow.y2 = destination_params.height(); - - /// RoI is in canonical coords, - OfxRectD regionOfInterest; - regionOfInterest.x1 = regionOfInterest.y1 = 0; - regionOfInterest.x2 = renderWindow.x2 * instance->getProjectPixelAspectRatio(); - regionOfInterest.y2 = renderWindow.y2 * instance->getProjectPixelAspectRatio(); - - OfxRectD regionOfDefinition; - regionOfDefinition.x1 = regionOfDefinition.y1 = 0; - regionOfDefinition.x2 = destination_params.width(); - regionOfDefinition.y2 = destination_params.height(); - int numFramesToRender=1; - OliveClipInstance *clip=dynamic_cast(instance->getClip("Output")); - if (!clip) { + OliveClipInstance *output_clip=dynamic_cast(instance->getClip("Output")); + if (!output_clip) { return; } - clip->setParams(destination_params); + output_clip->setParams(destination_params); + // ensure the instance was created OfxStatus stat = kOfxStatOK; if (olive_instance && !olive_instance->isCreated()) { stat = instance->createInstanceAction(); @@ -802,9 +1389,25 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: // call get region of interest on each of the inputs OfxTime frame = job.time_seconds(); - const NodeValueRow &values = job.GetValues(); - const auto &clips = instance->getDescriptor().getClips(); + const auto &clips = olive_instance->getDescriptor().getClips(); + QString effect_input_id; + if (const auto *node = job.node()) { + effect_input_id = node->GetEffectInputID(); + } + auto is_usable_input = [](const TexturePtr &tex) { + if (!tex) { + return false; + } + if (!tex->IsDummy() && tex->renderer()) { + return true; + } + AVFramePtr frame = tex->frame(); + return frame && frame->data[0]; + }; std::map input_textures; + std::map input_clips; + std::map input_params; + auto values = job.GetValues(); for (const auto &entry : clips) { if (entry.first == kOfxImageEffectOutputClipName) { continue; @@ -814,17 +1417,66 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: 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(); + const QString clip_key = QString::fromStdString(entry.first); + TexturePtr input_tex = nullptr; + if (!effect_input_id.isEmpty() && clip_key == effect_input_id && + is_usable_input(src)) { + input_tex = src; + } else { + input_tex = values.value(clip_key).toTexture(); + if (!input_tex && + entry.first == kOfxImageEffectSimpleSourceClipName) { + input_tex = values.value(kTextureInput).toTexture(); + } } - if (input_tex) { + if (!is_usable_input(input_tex) && + entry.first == kOfxImageEffectSimpleSourceClipName && + is_usable_input(src)) { + input_tex = src; + } + if (is_usable_input(input_tex)) { input_textures[entry.first] = input_tex; + olive::VideoParams params = input_tex->params(); + input_clip->setInputTexture(input_tex, frame); + input_clips[entry.first] = input_clip; + } + } + 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); + instance->getClipPreferences(); + for (const auto &entry : input_clips) { + if (entry.first == kOfxImageEffectOutputClipName) { + continue; + } + OliveClipInstance *input_clip = entry.second; + if (!input_clip) { + continue; + } + const QString clip_key = QString::fromStdString(entry.first); + TexturePtr input_tex = input_textures[entry.first]; + if (!use_opengl) { + AVFramePtr ptr = + ReadbackTextureToFrame(input_tex, input_tex->params()); + input_tex->handleFrame(ptr); + } + if (is_usable_input(input_tex)) { + input_textures[entry.first] = input_tex; + std::string bitdepth = input_clip->getProps() + .getStringProperty(kOfxImageEffectPropPixelDepth); + std::string component = input_clip->getProps() + .getStringProperty(kOfxImageEffectPropComponents); + VideoParams params = input_tex->params(); + params.set_format(PixelFormat::from_ofx(bitdepth)); + params.set_channel_count(component); + ConvertTextureForParams(input_tex, params) ; + input_clip->setInputTexture(input_tex,frame); + input_clips[entry.first] = input_clip; } } - // 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(); @@ -835,41 +1487,24 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: MarkRenderFailure(destination); return; } + // The render window is in pixel coordinates + // ie: render scale and a PAR of not 1 + OfxRectI renderWindow; + renderWindow.x1 = renderWindow.y1 = 0; + renderWindow.x2 = destination_params.width(); + renderWindow.y2 = destination_params.height(); + /// RoI is in canonical coords. + OfxRectD regionOfInterest; + regionOfInterest.x1 = 0.0; + regionOfInterest.y1 = 0.0; + regionOfInterest.x2 = destination_params.width(); + regionOfInterest.y2 = destination_params.height(); + OfxRectD regionOfDefinition = regionOfInterest; - 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_params.set_width(entry.second->params().width()); - input_params.set_height(entry.second->params().height()); - input_params.set_depth(entry.second->params().depth()); - input_params.set_pixel_aspect_ratio( - entry.second->params().pixel_aspect_ratio()); - input_params.set_interlacing(entry.second->params().interlacing()); - input_params.set_premultiplied_alpha( - entry.second->params().premultiplied_alpha()); - input_params.set_divider(entry.second->params().divider());*/ - input_clip->setParams(input_params); - } - olive::TexturePtr converted = - ConvertTextureForParams(entry.second, input_params); - input_clip->setInputTexture(converted, frame); - } - - clip->setRegionOfDefinition(regionOfDefinition, frame); - clip->setOutputTexture(destination, frame); + output_clip->setRegionOfDefinition(regionOfDefinition, frame); + output_clip->setOutputTexture(destination, frame); stat = instance->beginRenderAction(frame, numFramesToRender, 1.0, false, renderScale, true, @@ -901,7 +1536,7 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: return; } - if (!output_params.is_valid()) { + if (!destination_params.is_valid()) { qWarning().noquote() << "OFX render skipped due to invalid output params for plugin=" << PluginIdForInstance(instance); @@ -917,6 +1552,23 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: true, interactive, interactive); if (stat != kOfxStatOK && stat != kOfxStatReplyDefault) { LogOfxFailure("render", stat, instance); + LogClipState("output", output_clip, &destination_params); + for (const auto &entry : input_clips) { + const auto params_it = input_params.find(entry.first); + const olive::VideoParams *params = + (params_it != input_params.end()) ? ¶ms_it->second + : nullptr; + LogClipState("input", entry.second, params); + OFX::Host::ImageEffect::Image *image = + entry.second->getImage(frame, nullptr); + LogImageProps("input", image); + //if (image) { + //image->releaseReference(); + //} + } + OFX::Host::ImageEffect::Image* output_image = + output_clip->getOutputImage(frame); + LogImageProps("output", output_image); MarkRenderFailure(destination); instance->endRenderAction(frame, numFramesToRender, 1.0, interactive, renderScale, true, interactive); @@ -924,9 +1576,9 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: } // get the output image buffer (CPU path only) - std::shared_ptr output_image; + OFX::Host::ImageEffect::Image* output_image; if (!use_opengl) { - output_image = clip->getOutputImage(frame); + output_image = output_clip->getOutputImage(frame); if (!output_image) { qWarning().noquote() << "OFX getOutputImage returned null for plugin=" @@ -936,7 +1588,7 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: renderScale, true, interactive); return; } - } else { + } else { if (!destination || !destination->id().isValid()) { #ifdef OFX_SUPPORTS_OPENGLRENDER DetachOutputTexture(); @@ -952,7 +1604,7 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: 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); + *output_image, destination_params); } if (!frame_ptr) { qWarning().noquote() @@ -962,20 +1614,20 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: renderScale, true, interactive); return; } - AVFramePtr converted = ConvertFrameIfNeeded(frame_ptr, destination_params); + //AVFramePtr converted = ConvertFrameIfNeeded(frame_ptr, destination_params); const AVPixelFormat expected_fmt = GetDestinationAVPixelFormat(destination_params); - destination->handleFrame(converted); - if (destination->renderer() && converted && converted->data[0] && + destination->handleFrame(frame_ptr); + if (destination->renderer() && frame_ptr && frame_ptr->data[0] && (expected_fmt == AV_PIX_FMT_NONE || - converted->format == expected_fmt)) { + frame_ptr->format == expected_fmt)) { int linesize_pixels = - LinesizeToPixels(destination_params, converted->linesize[0]); + LinesizeToPixels(destination_params, frame_ptr->linesize[0]); if (linesize_pixels <= 0) { linesize_pixels = destination_params.effective_width(); } - destination->Upload(converted->data[0], linesize_pixels); - } else if (destination->renderer() && converted && converted->data[0]) { + destination->Upload(frame_ptr->data[0], linesize_pixels); + } else if (destination->renderer() && frame_ptr && frame_ptr->data[0]) { qWarning().noquote() << "OFX output pixel format mismatch for plugin=" << PluginIdForInstance(instance); @@ -987,18 +1639,39 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: DetachOutputTexture(); instance->contextDetachedAction(); #endif - if (frame_ptr) { + } +/* + if (frame_ptr && destination) { AVFramePtr converted = ConvertFrameIfNeeded(frame_ptr, destination_params); + const AVPixelFormat expected_fmt = + GetDestinationAVPixelFormat(destination_params); destination->handleFrame(converted); + if (destination->renderer() && converted && converted->data[0] && + (expected_fmt == AV_PIX_FMT_NONE || + converted->format == expected_fmt)) { + 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 if (destination->renderer() && converted && + converted->data[0]) { + qWarning().noquote() + << "OFX output pixel format mismatch for plugin=" + << PluginIdForInstance(instance); + } } } - - instance->endRenderAction(0, numFramesToRender, 1.0, interactive, renderScale, true,interactive +*/ + instance->endRenderAction(frame, numFramesToRender, 1.0, interactive, renderScale, true,interactive ); } +// 作用:绑定输出纹理到 OFX 的 GL 输出路径。 +// Purpose: Attach output texture for OFX GL rendering. void olive::plugin::PluginRenderer::AttachOutputTexture(olive::TexturePtr texture) { if (!texture) { @@ -1007,6 +1680,8 @@ void olive::plugin::PluginRenderer::AttachOutputTexture(olive::TexturePtr textur AttachTextureAsDestination(texture->id()); } +// 作用:解除 OFX 的 GL 输出绑定。 +// Purpose: Detach OFX GL output binding. void olive::plugin::PluginRenderer::DetachOutputTexture() { DetachTextureAsDestination(); diff --git a/app/render/plugin/pluginrenderer.h b/app/render/plugin/pluginrenderer.h index 033ce26cd..689848613 100644 --- a/app/render/plugin/pluginrenderer.h +++ b/app/render/plugin/pluginrenderer.h @@ -38,15 +38,25 @@ namespace olive { namespace plugin{ namespace detail { +// 作用:将字节行跨度转换为像素跨度,便于纹理读写。 +// Purpose: Convert byte stride to pixel stride for texture I/O. int BytesToPixels(int byte_linesize, const olive::VideoParams ¶ms); } +// 作用:OFX 插件渲染器,负责 CPU/GL 路径下的插件调用和纹理桥接。 +// Purpose: OFX plugin renderer that drives CPU/GL render paths and texture bridging. class PluginRenderer : public olive::OpenGLRenderer{ Q_OBJECT public: PluginRenderer(QObject *parent=nullptr):OpenGLRenderer(parent){}; virtual ~PluginRenderer() override{}; + // 作用:将目标纹理绑定为插件输出。 + // Purpose: Attach destination texture as OFX output. void AttachOutputTexture(olive::TexturePtr texture); + // 作用:解除目标纹理绑定。 + // Purpose: Detach destination texture binding. void DetachOutputTexture(); + // 作用:执行插件渲染流程(参数配置、输入/输出、调用渲染动作)。 + // Purpose: Execute plugin render flow (params, inputs/outputs, render actions). void RenderPlugin(TexturePtr src, olive::plugin::PluginJob& job, olive::TexturePtr destination, olive::VideoParams destination_params, diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 6bd2601f3..cbf0dd46e 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -530,8 +530,11 @@ void PreviewAutoCacher::TryRender() t->property("dry").toBool()); video_immediate_passthroughs_[watcher].append(t); } else { - qWarning() << "Failed to find copied node for SFR ticket"; - t->Finish(); + qWarning() << "Failed to find copied node for SFR ticket, requeueing"; + single_frame_render_ = t; + if (!delayed_requeue_timer_.isActive()) { + delayed_requeue_timer_.start(); + } } } @@ -560,7 +563,11 @@ void PreviewAutoCacher::TryRender() } } } else { - qCritical() << "Failed to find node copy for video job"; + qWarning() << "Failed to find node copy for video job, retrying"; + if (!delayed_requeue_timer_.isActive()) { + delayed_requeue_timer_.start(); + } + break; } if (d.iterator.HasNext()) { @@ -598,7 +605,12 @@ void PreviewAutoCacher::TryRender() RenderAudio(copy, d.context, use_range, d.cache); } else { - qCritical() << "Failed to find node copy for audio job"; + qWarning() << "Failed to find node copy for audio job, retrying"; + pop = false; + if (!delayed_requeue_timer_.isActive()) { + delayed_requeue_timer_.start(); + } + break; } if (pop) { diff --git a/app/render/renderer.cpp b/app/render/renderer.cpp index 54f11593a..6f7499825 100644 --- a/app/render/renderer.cpp +++ b/app/render/renderer.cpp @@ -28,9 +28,18 @@ namespace olive Renderer::Renderer(QObject *parent) : QObject(parent) + , lifetime_(std::make_shared()) { } +Renderer::~Renderer() +{ + destroyed_ = true; + if (lifetime_) { + lifetime_->alive = false; + } +} + TexturePtr Renderer::CreateTexture(const VideoParams ¶ms, const void *data, int linesize) { @@ -68,6 +77,9 @@ TexturePtr Renderer::CreateTexture(const VideoParams ¶ms, const void *data, void Renderer::DestroyTexture(Texture *texture) { + if (destroyed_) { + return; + } if (USE_TEXTURE_CACHE) { // HACK: Dirty, dirty hack. OpenGL uses "contexts" to store all of its data, and each context // can only be used by the thread that created it. However there are also "shared contexts" @@ -142,6 +154,10 @@ QVariant Renderer::GetDefaultShader() void Renderer::Destroy() { + destroyed_ = true; + if (lifetime_) { + lifetime_->alive = false; + } if (!default_shader_.isNull()) { DestroyNativeShader(default_shader_); default_shader_.clear(); @@ -169,7 +185,7 @@ TexturePtr Renderer::CreateTextureFromNativeHandle(const QVariant &v, return nullptr; } - return std::make_shared(this, v, params); + return std::make_shared(this, v, params, lifetime_); } bool Renderer::GetColorContext(const ColorTransformJob &color_job, @@ -323,6 +339,20 @@ void Renderer::BlitColorManaged(const ColorTransformJob &color_job, { ColorContext color_ctx; if (!GetColorContext(color_job, &color_ctx)) { + ShaderJob fallback_job; + fallback_job.Insert(QStringLiteral("ove_maintex"), + color_job.GetInputTexture()); + fallback_job.Insert( + QStringLiteral("ove_mvpmat"), + NodeValue(NodeValue::kMatrix, color_job.GetTransformMatrix())); + + if (destination) { + BlitToTexture(GetDefaultShader(), fallback_job, destination, + color_job.IsClearDestinationEnabled()); + } else { + Blit(GetDefaultShader(), fallback_job, params, + color_job.IsClearDestinationEnabled()); + } return; } diff --git a/app/render/renderer.h b/app/render/renderer.h index 1669e63af..5c6f0c0a5 100644 --- a/app/render/renderer.h +++ b/app/render/renderer.h @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include "common/define.h" #include "node/node.h" @@ -40,6 +42,7 @@ class Renderer : public QObject { Q_OBJECT public: Renderer(QObject *parent = nullptr); + virtual ~Renderer() override; virtual bool Init() = 0; @@ -106,6 +109,10 @@ public: virtual Color GetPixelFromTexture(olive::Texture *texture, const QPointF &pt) = 0; + std::shared_ptr GetLifetime() const + { + return lifetime_; + } protected: virtual void Blit(QVariant shader, olive::AcceleratedJob& job, @@ -122,6 +129,8 @@ protected: virtual void DestroyInternal() = 0; private: + std::atomic destroyed_{false}; + std::shared_ptr lifetime_; struct ColorContext { struct LUT { TexturePtr texture; diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp index 6c0a321b7..3bbbe05a3 100644 --- a/app/render/rendermanager.cpp +++ b/app/render/rendermanager.cpp @@ -90,7 +90,7 @@ RenderThread *RenderManager::CreateThread(Renderer *renderer) { auto t = new RenderThread(renderer, decoder_cache_, shader_cache_, this); render_threads_.push_back(t); - t->start(QThread::IdlePriority); + t->start(QThread::NormalPriority); return t; } diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 20d451b11..be79fd43f 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -635,8 +635,56 @@ TexturePtr RenderProcessor::ProcessPluginJob(TexturePtr texture, plugin_renderer_->PostInit(); } + NodeValueRow &values = plugin_job->GetValues(); + + auto is_usable_texture = [](const TexturePtr &tex) { + if (!tex) { + return false; + } + if (!tex->IsDummy() && tex->renderer()) { + return true; + } + AVFramePtr frame = tex->frame(); + return frame && frame->data[0]; + }; + + TexturePtr src = nullptr; + QString effect_input_id; + if (plugin_job->node()) { + effect_input_id = plugin_job->node()->GetEffectInputID(); + } + if (!effect_input_id.isEmpty()) { + if (TexturePtr effect_tex = values.value(effect_input_id).toTexture(); + is_usable_texture(effect_tex)) { + src = effect_tex; + } + } + if (!src) { + const QString source_key = + QString::fromUtf8(kOfxImageEffectSimpleSourceClipName); + if (TexturePtr source_tex = values.value(source_key).toTexture(); + is_usable_texture(source_tex)) { + src = source_tex; + } else if (TexturePtr effect_tex = + values.value(plugin::kTextureInput).toTexture(); + is_usable_texture(effect_tex)) { + src = effect_tex; + } + } + if (!src) { + for (auto it = values.cbegin(); it != values.cend(); ++it) { + if (it.value().type() == NodeValue::kTexture) { + if (TexturePtr any_tex = it.value().toTexture(); + is_usable_texture(any_tex)) { + src = any_tex; + break; + } + } + } + } + plugin_renderer_->RenderPlugin( - texture, + src, *plugin_job, destination, destination->params(), diff --git a/app/render/texture.cpp b/app/render/texture.cpp index 514df1b3a..d08c1b50b 100644 --- a/app/render/texture.cpp +++ b/app/render/texture.cpp @@ -28,7 +28,7 @@ const Texture::Interpolation Texture::kDefaultInterpolation = Texture::~Texture() { - if (renderer_) { + if (IsRendererAlive()) { renderer_->DestroyTexture(this); } @@ -39,14 +39,14 @@ Texture::~Texture() void Texture::Upload(void *data, int linesize) { - if (renderer_) { + if (IsRendererAlive()) { renderer_->UploadToTexture(this->id(), this->params(), data, linesize); } } void Texture::Download(void *data, int linesize) { - if (renderer_) { + if (IsRendererAlive()) { renderer_->DownloadFromTexture(this->id(), this->params(), data, linesize); } diff --git a/app/render/texture.h b/app/render/texture.h index 6476b4524..f2aac6504 100644 --- a/app/render/texture.h +++ b/app/render/texture.h @@ -21,6 +21,7 @@ #include "common/ffmpegutils.h" +#include #include #include @@ -31,6 +32,9 @@ namespace olive class AcceleratedJob; class Renderer; +struct RendererLifetime { + std::atomic alive{true}; +}; class Texture; using TexturePtr = std::shared_ptr; @@ -46,6 +50,7 @@ public: */ Texture(const VideoParams ¶m) : renderer_(nullptr) + , renderer_lifetime_(nullptr) , params_(param) , job_(nullptr) { @@ -62,8 +67,10 @@ public: * @brief Construct a real texture linked to a renderer backend */ Texture(Renderer *renderer, const QVariant &native, - const VideoParams ¶m) + const VideoParams ¶m, + std::shared_ptr lifetime = nullptr) : renderer_(renderer) + , renderer_lifetime_(lifetime) , params_(param) , id_(native) , job_(nullptr) @@ -158,7 +165,14 @@ public: return frame_; } private: + bool IsRendererAlive() const + { + return renderer_ && + (!renderer_lifetime_ || renderer_lifetime_->alive.load()); + } + Renderer *renderer_; + std::shared_ptr renderer_lifetime_; VideoParams params_; diff --git a/app/render/videoparams.h b/app/render/videoparams.h index dfd36258b..d9495cac5 100644 --- a/app/render/videoparams.h +++ b/app/render/videoparams.h @@ -19,10 +19,12 @@ #ifndef VIDEOPARAMS_H #define VIDEOPARAMS_H +#include "ofxImageEffect.h" #include #include #include #include +#include namespace olive { @@ -173,7 +175,18 @@ public: { channel_count_ = c; } - + void set_channel_count(std::string ofxComponent) + { + if (ofxComponent == kOfxImageComponentAlpha){ + channel_count_ = 1; + } + else if (ofxComponent == kOfxImageComponentRGB){ + channel_count_ = kRGBChannelCount; + } + else if(ofxComponent == kOfxImageComponentRGBA){ + channel_count_ = kRGBAChannelCount; + } + } const rational &pixel_aspect_ratio() const { return pixel_aspect_ratio_; diff --git a/app/widget/nodeparamview/nodeparamviewcontext.cpp b/app/widget/nodeparamview/nodeparamviewcontext.cpp index 520252292..36c1baaf1 100644 --- a/app/widget/nodeparamview/nodeparamviewcontext.cpp +++ b/app/widget/nodeparamview/nodeparamviewcontext.cpp @@ -74,7 +74,7 @@ void NodeParamViewContext::RemoveNode(Node *node, Node *ctx) if (item->GetNode() == node && item->GetContext() == ctx) { emit AboutToDeleteItem(item); - delete item; + dock_area_->RemoveItem(item); it = items_.erase(it); } else { it++; @@ -89,7 +89,7 @@ void NodeParamViewContext::RemoveNodesWithContext(Node *ctx) if (item->GetContext() == ctx) { emit AboutToDeleteItem(item); - delete item; + dock_area_->RemoveItem(item); it = items_.erase(it); } else { it++; diff --git a/app/widget/nodeparamview/nodeparamviewdockarea.cpp b/app/widget/nodeparamview/nodeparamviewdockarea.cpp index 8ab04cc99..3bbcb8c0f 100644 --- a/app/widget/nodeparamview/nodeparamviewdockarea.cpp +++ b/app/widget/nodeparamview/nodeparamviewdockarea.cpp @@ -47,4 +47,14 @@ void NodeParamViewDockArea::AddItem(QDockWidget *item) addDockWidget(Qt::LeftDockWidgetArea, item); } +void NodeParamViewDockArea::RemoveItem(QDockWidget *item) +{ + if (!item) { + return; + } + removeDockWidget(item); + item->setParent(nullptr); + item->deleteLater(); +} + } diff --git a/app/widget/nodeparamview/nodeparamviewdockarea.h b/app/widget/nodeparamview/nodeparamviewdockarea.h index 6fc2dafbd..f97135b36 100644 --- a/app/widget/nodeparamview/nodeparamviewdockarea.h +++ b/app/widget/nodeparamview/nodeparamviewdockarea.h @@ -34,6 +34,7 @@ public: virtual QMenu *createPopupMenu() override; void AddItem(QDockWidget *item); + void RemoveItem(QDockWidget *item); }; } diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index 78c5c8a1b..15c969d66 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -110,12 +110,22 @@ void NodeParamViewWidgetBridge::CreateWidgets() CreateSliders(GetSliderCount(t), parent); break; } - case NodeValue::kCombo: { + case NodeValue::kCombo: + case NodeValue::kStrCombo: { QComboBox *combobox = new QComboBox(parent); QStringList items = GetInnerInput().GetComboBoxStrings(); - foreach (const QString &s, items) { - combobox->addItem(s); + QStringList values = + GetInnerInput().GetProperty("combo_value_str").toStringList(); + const bool use_value_data = + (t == NodeValue::kStrCombo) && !values.isEmpty(); + for (int i = 0; i < items.size(); ++i) { + const QString &label = items.at(i); + if (use_value_data && i < values.size()) { + combobox->addItem(label, values.at(i)); + } else { + combobox->addItem(label); + } } widgets_.append(combobox); @@ -378,6 +388,16 @@ void NodeParamViewWidgetBridge::WidgetCallback() SetInputValue(index, 0); break; } + case NodeValue::kStrCombo: { + QComboBox *cb = static_cast(widgets_.first()); + const QVariant data = cb->currentData(); + if (data.isValid()) { + SetInputValue(data.toString(), 0); + } else { + SetInputValue(cb->currentText(), 0); + } + break; + } case NodeValue::kBezier: { // Widget is a FloatSlider (child of BezierWidget) BezierWidget *bw = static_cast(widgets_.first()); @@ -561,6 +581,22 @@ void NodeParamViewWidgetBridge::UpdateWidgetValues() cb->blockSignals(false); break; } + case NodeValue::kStrCombo: { + QComboBox *cb = static_cast(widgets_.first()); + cb->blockSignals(true); + const QString current = + GetInnerInput().GetValueAtTime(node_time).toString(); + for (int i = 0; i < cb->count(); ++i) { + const QVariant data = cb->itemData(i); + if ((data.isValid() && data.toString() == current) || + (!data.isValid() && cb->itemText(i) == current)) { + cb->setCurrentIndex(i); + break; + } + } + cb->blockSignals(false); + break; + } case NodeValue::kBezier: { BezierWidget *bw = static_cast(widgets_.first()); bw->SetValue(GetInnerInput().GetValueAtTime(node_time).value()); @@ -772,7 +808,7 @@ void NodeParamViewWidgetBridge::SetProperty(const QString &key, } // ComboBox strings changing - if (data_type == NodeValue::kCombo) { + if (data_type == NodeValue::kCombo || data_type == NodeValue::kStrCombo) { if (key == QStringLiteral("combo_str")) { QComboBox *cb = static_cast(widgets_.first()); @@ -784,14 +820,23 @@ void NodeParamViewWidgetBridge::SetProperty(const QString &key, cb->clear(); QStringList items = value.toStringList(); + QStringList values = + GetInnerInput().GetProperty("combo_value_str").toStringList(); + const bool use_value_data = + (data_type == NodeValue::kStrCombo) && !values.isEmpty(); int index = 0; - foreach (const QString &s, items) { + for (int i = 0; i < items.size(); ++i) { + const QString &s = items.at(i); if (s.isEmpty()) { cb->insertSeparator(cb->count()); cb->setItemData(cb->count() - 1, -1); } else { - cb->addItem(s, index); - index++; + if (use_value_data && i < values.size()) { + cb->addItem(s, values.at(i)); + } else { + cb->addItem(s, index); + index++; + } } } diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 2953558b5..1f7cb29d3 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -1270,6 +1270,7 @@ RenderTicketWatcher *ViewerWidget::RequestNextFrameForQueue(bool increment) } watcher = new RenderTicketWatcher(); + watcher->setProperty("start", QDateTime::currentMSecsSinceEpoch()); watcher->setProperty("time", QVariant::fromValue(next_time)); DetectMulticamNode(next_time); connect(watcher, &RenderTicketWatcher::Finished, this, @@ -1283,6 +1284,10 @@ RenderTicketWatcher *ViewerWidget::RequestNextFrameForQueue(bool increment) RenderTicketPtr ViewerWidget::GetFrame(const rational &t) { + if (IsPlaying() || prequeuing_video_) { + return GetSingleFrame(t); + } + QString cache_fn = GetConnectedNode()->video_frame_cache()->GetValidCacheFilename(t); @@ -1447,21 +1452,45 @@ void ViewerWidget::RendererGeneratedFrameForQueue() if (watcher->HasResult()) { QVariant frame = watcher->Get(); + bool drop_frame = false; // Ignore this signal if we've paused now if (IsPlaying() || prequeuing_video_) { + const qint64 start_ms = + watcher->property("start").toLongLong(); + const qint64 now_ms = + QDateTime::currentMSecsSinceEpoch(); + const int playback_step = qMax(1, qAbs(playback_speed_)); + const double frame_interval_ms = qMax( + 1.0, + timebase().toDouble() * 1000.0 / + static_cast(playback_step)); + if (start_ms > 0 && + (now_ms - start_ms) > frame_interval_ms) { + drop_frame = true; + } + rational ts = watcher->property("time").value(); - foreach (ViewerDisplayWidget *dw, playback_devices_) { - QVariant push; - if (dynamic_cast(dw)) { - push = - watcher->GetTicket()->property("multicam_output"); - } else { - push = frame; - } + if (!drop_frame) { + foreach (ViewerDisplayWidget *dw, playback_devices_) { + const bool is_multicam = + dynamic_cast(dw); + QVariant push; + if (is_multicam) { + push = watcher->GetTicket()->property( + "multicam_output"); + if (!push.isValid() || push.isNull()) { + // Fall back to the primary frame when multicam isn't available. + push = frame; + } + } else { + push = frame; + } - dw->queue()->AppendTimewise({ ts, push }, playback_speed_); + dw->queue()->AppendTimewise({ ts, push }, + playback_speed_); + } } if (prequeuing_video_) { diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index aac805d4b..bc17e2639 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -38,6 +38,7 @@ #include "config/config.h" #include "core.h" #include "node/block/subtitle/subtitle.h" +#include "codec/frame.h" #include "node/gizmo/path.h" #include "node/gizmo/point.h" #include "node/gizmo/polygon.h" @@ -401,8 +402,35 @@ void ViewerDisplayWidget::OnPaint() texture_->Upload(frame->data(), frame->linesize_pixels()); } } else if (TexturePtr texture = load_frame_.value()) { - // This is a GPU texture, switch to it directly - texture_ = texture; + // This is a GPU texture, switch to it directly when possible. + if (texture && texture->renderer() && + texture->renderer() != renderer()) { + bool copied = false; + QOpenGLContext *ctx = QOpenGLContext::currentContext(); + if (ctx) { + QOpenGLFunctions *funcs = ctx->functions(); + GLuint tex_id = texture->id().value(); + if (funcs && tex_id && funcs->glIsTexture(tex_id)) { + FramePtr frame = Frame::Create(); + frame->set_video_params(texture->params()); + if (frame->allocate()) { + renderer()->DownloadFromTexture( + texture->id(), texture->params(), + frame->data(), frame->linesize_pixels()); + texture_ = renderer()->CreateTexture( + frame->video_params(), frame->data(), + frame->linesize_pixels()); + copied = true; + } + } + } + + if (!copied) { + texture_ = texture; + } + } else { + texture_ = texture; + } } else { texture_ = LoadCustomTextureFromFrame(load_frame_); } diff --git a/docs/ofx-pluginrenderer-functions-zh.md b/docs/ofx-pluginrenderer-functions-zh.md new file mode 100644 index 000000000..2a1dcc418 --- /dev/null +++ b/docs/ofx-pluginrenderer-functions-zh.md @@ -0,0 +1,44 @@ +# OFX PluginRenderer 函数说明(中文) + +日期:2026-01-11 +执行者:Codex + +## 说明 +本文档概述 `app/render/plugin/pluginrenderer.cpp` 与 `app/render/plugin/pluginrenderer.h` 中函数的职责,用于排查 OFX 渲染链路问题。 + +## 头文件(pluginrenderer.h) +- `olive::plugin::detail::BytesToPixels`:将字节行跨度转换为像素行跨度,供纹理读写使用。 +- `olive::plugin::PluginRenderer`:OFX 插件渲染器,负责插件调用与 GL/CPU 纹理桥接。 +- `PluginRenderer::AttachOutputTexture`:绑定输出纹理到 OFX 的 GL 输出路径。 +- `PluginRenderer::DetachOutputTexture`:解除 OFX 的 GL 输出绑定。 +- `PluginRenderer::RenderPlugin`:执行完整的 OFX 渲染流程(输入/输出准备、动作调用、结果处理)。 + +## 源文件(pluginrenderer.cpp) +- `GetOfxAVPixelFormat`:根据 OFX Image 的属性推导 FFmpeg 像素格式,并返回每像素字节数。 +- `ApplyClipPreferencesToParams`:读取 clip 偏好(深度/组件)并更新 VideoParams。 +- `PixelFormatFromOfxDepth`:OFX bit depth 字符串 → 内部 PixelFormat。 +- `OfxDepthFromPixelFormat`:内部 PixelFormat → OFX bit depth 字符串。 +- `ChannelCountFromOfxComponent`:OFX components 字符串 → 通道数。 +- `OfxComponentsFromChannels`:通道数 → OFX components 字符串。 +- `EffectSupportsPixelDepth`:检查插件是否支持指定像素深度。 +- `ClipSupportsComponents`:检查 clip 是否支持指定组件格式。 +- `ConversionCost`:估算源参数到目标参数的转换代价,用于排序。 +- `ParamsConvertible`:判断目标参数能否映射为可用的 AVPixelFormat。 +- `ConvertTextureForClip`:结合插件能力选择输入格式并执行转换。 +- `create_avframe_from_ofx_image`:从 OFX Image 复制数据到 AVFrame(按图像属性推导格式)。 +- `create_avframe_from_ofx_image_with_params`:按指定 VideoParams 复制 OFX Image 到 AVFrame。 +- `GetDestinationAVPixelFormat`:将 VideoParams 映射为最终输出 AVPixelFormat。 +- `GetRenderFieldForParams`:根据交错设置返回 OFX render field 字符串。 +- `ReadbackTextureToFrame`:从 GPU 纹理回读到 AVFrame(必要时做格式转换)。 +- `olive::plugin::detail::BytesToPixels`:字节行跨度 → 像素行跨度。 +- `ConvertFrameIfNeeded`:必要时将 AVFrame 转换为目标 VideoParams 对应格式。 +- `LinesizeToPixels`:字节行跨度 → 像素行跨度。 +- `ConvertTextureForParams`:将纹理转换为指定 VideoParams(CPU 路径,必要时回读)。 +- `PluginIdForInstance`:安全获取插件标识符,便于日志输出。 +- `LogOfxFailure`:统一 OFX 调用失败日志输出。 +- `LogClipState`:输出 clip 声明属性与 VideoParams,用于定位格式不一致。 +- `LogImageProps`:输出 OFX Image 属性(深度/组件/行跨度/边界)。 +- `MarkRenderFailure`:渲染失败时标记目标画面(紫色)。 +- `PluginRenderer::RenderPlugin`:执行 OFX 插件渲染全流程。 +- `PluginRenderer::AttachOutputTexture`:绑定输出纹理到 OFX GL 输出路径。 +- `PluginRenderer::DetachOutputTexture`:解除 OFX GL 输出绑定。 diff --git a/ext/core b/ext/core index c42db4a85..da9668f48 160000 --- a/ext/core +++ b/ext/core @@ -1 +1 @@ -Subproject commit c42db4a854d022ef087595de79e64b24f3638044 +Subproject commit da9668f48c8aeb402f933890924bff9bd4da9473 diff --git a/third_party/openfx/HostSupport/include/ofxhUtilities.h b/third_party/openfx/HostSupport/include/ofxhUtilities.h index b71e2dd18..30d84c024 100644 --- a/third_party/openfx/HostSupport/include/ofxhUtilities.h +++ b/third_party/openfx/HostSupport/include/ofxhUtilities.h @@ -10,6 +10,7 @@ #include #include #include "ofxCore.h" +#include "ofxImageEffect.h" // macro that intercepts any exception that passes through a plugin's entry point, and transforms it into a message on the host using Host::vmessage() #define CatchAllSetStatus(stat,host,plugin,msg) \ @@ -138,6 +139,8 @@ namespace OFX { return "kOfxStatErrBadIndex"; case kOfxStatErrValue: return "kOfxStatErrValue"; + case kOfxStatErrImageFormat: + return "kOfxStatErrImageFormat"; case kOfxStatReplyYes: return "kOfxStatReplyYes"; case kOfxStatReplyNo: @@ -151,4 +154,3 @@ namespace OFX { } #endif -