From f191486375a755e8dd0525e9d1eefb788cb22201 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Sun, 4 Jan 2026 22:30:41 +0800 Subject: [PATCH] Implement multi-input OFX clip wiring and texture handling Store PluginJob input values for lookup Add per-clip texture inputs on plugin nodes Map input clips to textures during render (with Source fallback) --- .clangd | 6 + README.md | 13 +- TODO-zh.md | 47 +++ app/node/node.h | 2 + app/node/plugins/Plugin.cpp | 26 +- app/node/plugins/Plugin.h | 1 + app/node/traverser.cpp | 10 +- app/node/traverser.h | 2 +- app/pluginSupport/CMakeLists.txt | 2 + app/pluginSupport/OliveClip.cpp | 167 +++++++++- app/pluginSupport/OliveClip.h | 20 +- app/pluginSupport/OliveHost.cpp | 73 ++++- app/pluginSupport/OliveHost.h | 31 +- app/pluginSupport/OlivePluginInstance.cpp | 6 + app/pluginSupport/OlivePluginInstance.h | 15 +- app/pluginSupport/image.cpp | 245 +++++++++++++++ app/pluginSupport/image.h | 86 ++++++ app/render/job/pluginjob.h | 20 +- app/render/plugin/pluginrenderer.cpp | 291 +++++++++++++++++- app/render/plugin/pluginrenderer.h | 4 +- app/render/renderprocessor.cpp | 78 +---- app/render/texture.h | 3 + .../nodeparamview/nodeparamviewitem.cpp | 63 +++- app/widget/nodeparamview/nodeparamviewitem.h | 3 + app/widget/nodeview/nodeview.cpp | 10 + app/widget/nodeview/nodeviewitem.cpp | 38 +++ ext/KDDockWidgets | 2 +- .../HostSupport/include/ofxhImageEffect.h | 9 +- .../HostSupport/include/ofxhImageEffectAPI.h | 2 + .../openfx/HostSupport/src/ofxhClip.cpp | 1 + .../openfx/HostSupport/src/ofxhInteract.cpp | 1 - 31 files changed, 1147 insertions(+), 130 deletions(-) create mode 100644 .clangd create mode 100644 TODO-zh.md create mode 100644 app/pluginSupport/image.cpp create mode 100644 app/pluginSupport/image.h diff --git a/.clangd b/.clangd new file mode 100644 index 000000000..579419f34 --- /dev/null +++ b/.clangd @@ -0,0 +1,6 @@ +CompileFlags: + Add: [] + Remove: [-mlongcalls, -fstrict-volatile-bitfields, -fno-shrink-wrap, -fno-tree-switch-conversion, -mno-direct-extern-access] + +Diagnostics: + Suppress: ['drv_unknown_argument', 'unused-includes', 'pp_file_not_found'] \ No newline at end of file diff --git a/README.md b/README.md index 9b5b5e3b1..d9012c771 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,15 @@ Now, we are maintaining a community edition for this project. ## Binaries The original author compiled following binaries: - [0.1.0 alpha](https://github.com/olive-editor/olive/releases/tag/0.1.0) -- [0.2.0 unstable development build](https://github.com/olive-editor/olive/releases/tag/0.2.0-nightly) \ No newline at end of file +- [0.2.0 unstable development build](https://github.com/olive-editor/olive/releases/tag/0.2.0-nightly) + +## OpenFX Support TODO +- Implement plugin discovery/loading from a given path and populate the cache (currently creates host/cache only). `app/pluginSupport/OliveHost.cpp` +- Wire output clip image storage: allocate a backing buffer, set `kOfxImagePropData`, and update bounds/rowBytes before render. `app/pluginSupport/OliveClip.cpp` +- Provide real input clip image fetches (currently returns an empty `Image` for inputs). `app/pluginSupport/OliveClip.cpp` +- Ensure render path sets per-frame output data and handles ROD/bounds correctly. `app/render/plugin/pluginrenderer.cpp` +- Add missing param instance types (String, Double3D/Integer3D, Group/Page, Custom/Bytes) and mapping to node inputs. `app/pluginSupport/OlivePluginInstance.cpp`, `app/node/plugins/Plugin.cpp` +- Implement `editBegin`/`editEnd`, progress, and timeline hooks instead of stubs. `app/pluginSupport/OlivePluginInstance.cpp`, `app/pluginSupport/OlivePluginInstance.h` +- Integrate persistent message handling with the app UI (currently TODO placeholders). `app/pluginSupport/OlivePluginInstance.cpp` +- Decide and enforce project extent/fielding behavior instead of the current placeholder comment. `app/pluginSupport/OlivePluginInstance.cpp` +- Add OpenGL texture render suite support or explicitly disable it (currently `loadTexture` returns null). `app/pluginSupport/OliveClip.h` diff --git a/TODO-zh.md b/TODO-zh.md new file mode 100644 index 000000000..ee834e19c --- /dev/null +++ b/TODO-zh.md @@ -0,0 +1,47 @@ +# OpenFX 支持 TODO(中文说明) + +下面是 README 里 OpenFX TODO 的中文翻译与详细说明。每条都尽量解释“是什么、为什么需要、该往哪里改”。 + +1) 实现插件发现与加载流程 +- 现状:`app/pluginSupport/OliveHost.cpp` 里只创建了 Host 和 PluginCache,但没有扫描路径/加载 OFX 插件。 +- 为什么需要:没有完整的发现/加载,就无法让用户看到插件或实例化插件。 +- 可能改动:补全 `loadPlugins()`,遍历指定目录(如 OFX 标准路径),调用 OpenFX 的插件缓存加载逻辑,注册可用插件并让 UI/节点系统可用。 + +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 与 UI 进度条连接;timelineGetTime/GotoTime/Bounds 与工程时间轴连接。 + +8) Project Extent / Fielding 行为待确认 +- 现状:`OlivePluginInstance::getProjectExtent()` 有 “TODO” 注释。 +- 为什么需要:OFX 插件会根据项目尺寸、扫描线场信息做渲染决策。 +- 可能改动:明确工程是否支持不同 extent/fielding,确保返回值与项目设置一致。 + +9) OpenGL Render Suite 支持 +- 现状:`OliveClipInstance::loadTexture()` 返回 null,OpenGL 渲染路径未实现。 +- 为什么需要:有些 OFX 插件只支持 OpenGL 渲染,不支持 CPU 渲染。 +- 可能改动:要么实现 OpenGL texture 的加载与生命周期,要么在 host capability 中明确禁用 OpenGL render。 + +--- + +如果你希望,我可以把这些 TODO 拆成“优先级 + 预计工作量 + 依赖关系”的形式,方便你逐条推进。 diff --git a/app/node/node.h b/app/node/node.h index 8476eb05c..b8e322791 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -1303,6 +1303,8 @@ signals: void KeyframeTimeChanged(NodeKeyframe *key); + void MessageCountChanged(); + void KeyframeTypeChanged(NodeKeyframe *key); void KeyframeValueChanged(NodeKeyframe *key); diff --git a/app/node/plugins/Plugin.cpp b/app/node/plugins/Plugin.cpp index 25a943655..fcfb5d88e 100644 --- a/app/node/plugins/Plugin.cpp +++ b/app/node/plugins/Plugin.cpp @@ -24,6 +24,7 @@ olive::plugin::PluginNode::PluginNode( OFX::Host::ImageEffect::Instance *plugin) { plugin_instance_=plugin; + bool has_texture_input = false; auto params=plugin_instance_->getParams(); for (auto param: params) { @@ -70,6 +71,17 @@ olive::plugin::PluginNode::PluginNode( AddInput(param.second->getName().data(), type); } + const auto &clips = plugin_instance_->getDescriptor().getClips(); + for (const auto &entry : clips) { + if (entry.first == kOfxImageEffectOutputClipName) { + continue; + } + AddInput(entry.first.data(), NodeValue::kTexture); + has_texture_input = true; + } + if (!has_texture_input) { + AddInput(kTextureInput, NodeValue::kTexture); + } } QString olive::plugin::PluginNode::Name() const { @@ -94,9 +106,19 @@ void olive::plugin::PluginNode::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const { - TexturePtr tex = value[kTextureInput].toTexture(); + TexturePtr tex = value.value(kTextureInput).toTexture(); + if (!tex) { + for (auto it = value.cbegin(); it != value.cend(); ++it) { + if (it.value().type() == NodeValue::kTexture) { + tex = it.value().toTexture(); + if (tex) { + break; + } + } + } + } if (tex && plugin_instance_) { - PluginJob job(plugin_instance_, value); + PluginJob job(plugin_instance_, this, value); table->Push(NodeValue::kTexture, tex->toJob(job), this); } } diff --git a/app/node/plugins/Plugin.h b/app/node/plugins/Plugin.h index dcb97cae8..6c52e9274 100644 --- a/app/node/plugins/Plugin.h +++ b/app/node/plugins/Plugin.h @@ -20,6 +20,7 @@ #define PLUGIN_NODES_H #include "ofxhImageEffectAPI.h" #include "ofxhPluginCache.h" +#include "ofxImageEffect.h" #include "node/node.h" namespace olive diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index fbd58cca6..e2253276a 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -382,7 +382,7 @@ TexturePtr NodeTraverser::ProcessVideoCacheJob(const CacheJob *val) return nullptr; } -TexturePtr NodeTraverser::ProcessPluginJob(plugin::PluginJob *job) +TexturePtr NodeTraverser::ProcessPluginJob(TexturePtr texture, TexturePtr destination, const Node *node,) { // TODO } @@ -496,7 +496,13 @@ void NodeTraverser::ResolveJobs(NodeValue &val) val.set_value(tex); } else if (plugin::PluginJob* plugin_job=dynamic_cast(base_job)) { - ProcessPluginJob(plugin_job); + VideoParams tex_params = job_tex->params(); + + TexturePtr tex = CreateTexture(tex_params); + + ProcessPluginJob(job_tex, tex, val.source()); + val.set_value(tex); + } // Cache resolved value diff --git a/app/node/traverser.h b/app/node/traverser.h index fdc675b12..3e46e7f34 100644 --- a/app/node/traverser.h +++ b/app/node/traverser.h @@ -144,7 +144,7 @@ protected: return SampleBuffer(); } - virtual TexturePtr ProcessPluginJob(plugin::PluginJob *job); + virtual TexturePtr ProcessPluginJob(TexturePtr texture, TexturePtr destination, const Node *node); SampleBuffer CreateSampleBuffer(const AudioParams ¶ms, const rational &length) { diff --git a/app/pluginSupport/CMakeLists.txt b/app/pluginSupport/CMakeLists.txt index 23adfb8ad..ce057b228 100644 --- a/app/pluginSupport/CMakeLists.txt +++ b/app/pluginSupport/CMakeLists.txt @@ -7,4 +7,6 @@ target_sources(libolive-editor PRIVATE OliveClip.h paraminstance.cpp paraminstance.h + image.cpp + image.h ) \ No newline at end of file diff --git a/app/pluginSupport/OliveClip.cpp b/app/pluginSupport/OliveClip.cpp index b41ec7cc7..9a9162422 100644 --- a/app/pluginSupport/OliveClip.cpp +++ b/app/pluginSupport/OliveClip.cpp @@ -24,6 +24,18 @@ #include "OliveClip.h" #include "common/Current.h" +#include "common/ffmpegutils.h" +#include "ofxCore.h" +#include "ofxhClip.h" +#include "pluginSupport/image.h" +#include +#include +#include +#include + +extern "C" { +#include +} const std::string &olive::plugin::OliveClipInstance::getUnmappedBitDepth() const { switch (params_.format()) { @@ -65,9 +77,7 @@ const std::string &olive::plugin::OliveClipInstance::getPremult() const } double olive::plugin::OliveClipInstance::getAspectRatio() const { - return params_ - .pixel_aspect_ratio() - .toDouble(); + return params_.pixel_aspect_ratio().toDouble(); } double olive::plugin::OliveClipInstance::getFrameRate() const { @@ -76,13 +86,9 @@ double olive::plugin::OliveClipInstance::getFrameRate() const void olive::plugin::OliveClipInstance::getFrameRange(double &startFrame, double &endFrame) const { - startFrame = - params_.frame_rate().toDouble() * - params_.start_time(); + startFrame = params_.frame_rate().toDouble() * params_.start_time(); endFrame = - startFrame + - params_.frame_rate().toDouble() * - params_.duration(); + startFrame + params_.frame_rate().toDouble() * params_.duration(); } const std::string &olive::plugin::OliveClipInstance::getFieldOrder() const { @@ -98,8 +104,7 @@ const std::string &olive::plugin::OliveClipInstance::getFieldOrder() const } bool olive::plugin::OliveClipInstance::getConnected() const { - return params_.format() == - PixelFormat::INVALID; + return params_.format() == PixelFormat::INVALID; } double olive::plugin::OliveClipInstance::getUnmappedFrameRate() const { @@ -118,7 +123,54 @@ OFX::Host::ImageEffect::Image * olive::plugin::OliveClipInstance::getImage(OfxTime time, const OfxRectD *optionalBounds) { - return &image_; + OfxRectI bounds = {0, 0, params_.width(), params_.height()}; + 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)); + } + + OfxRectD rod_d = getRegionOfDefinition(time); + OfxRectI rod = { static_cast(std::floor(rod_d.x1)), + static_cast(std::floor(rod_d.y1)), + static_cast(std::ceil(rod_d.x2)), + static_cast(std::ceil(rod_d.y2)) }; + + if (name_ == "Output") { + if (!images_.contains(time)) { + // make a new ref counted image + images_.insert(time, std::make_shared(*const_cast(this), + params_, bounds, rod, true)); + } + + // add another reference to the member image for this fetch + // as we have a ref count of 1 due to construction, this will + // cause the output image never to delete by the plugin + // when it releases the image + images_[time]->addReference(); + + images_[time]->EnsureAllocatedFromParams(params_, bounds, rod, true); + + // return it + return images_[time].get(); + } else { + if (images_.contains(time)) { + std::shared_ptr image = images_.value(time); + image->EnsureAllocatedFromParams(params_, bounds, rod, false); + image->addReference(); + return image.get(); + } + + // Fetch on demand for the input clip. + // It does get deleted after the plugin is done with it as we + // have not incremented the auto ref + // + // You should do somewhat more sophisticated image management + // than this. + Image *image = new Image(*this, params_, bounds, rod, true); + return image; + } } OfxRectD olive::plugin::OliveClipInstance::getRegionOfDefinition(OfxTime time) const @@ -127,9 +179,9 @@ olive::plugin::OliveClipInstance::getRegionOfDefinition(OfxTime time) const return regionOfDefinitions_[time]; } OfxRectD regionOfDefinition; - regionOfDefinition.x1=regionOfDefinition.y1=0; - regionOfDefinition.x2=params_.width(); - regionOfDefinition.y2=params_.height(); + regionOfDefinition.x1 = regionOfDefinition.y1 = 0; + regionOfDefinition.x2 = params_.width(); + regionOfDefinition.y2 = params_.height(); return regionOfDefinition; } void olive::plugin::OliveClipInstance::setRegionOfDefinition( @@ -142,4 +194,87 @@ void olive::plugin::OliveClipInstance::setDefaultRegionOfDefinition( OfxRectD regionOfDefinition) { defaultRegionOfDefinitions_ = regionOfDefinition; -} \ No newline at end of file +} +void olive::plugin::OliveClipInstance::setInputTexture(TexturePtr texture, OfxTime time){ + if (!texture) { + return; + } + AVFramePtr frame = texture->frame(); + if (!frame || !frame->data[0]) { + return; + } + + this->params_=texture->params(); + AVPixelFormat expected_fmt = + FFmpegUtils::GetFFmpegPixelFormat(params_.format(), + params_.channel_count()); + if (expected_fmt == AV_PIX_FMT_NONE) { + return; + } + OfxRectI bounds = { 0, 0, params_.width(), params_.height() }; + OfxRectD rod_d = getRegionOfDefinition(time); + OfxRectI regionOfDefinition = { static_cast(std::floor(rod_d.x1)), + static_cast(std::floor(rod_d.y1)), + static_cast(std::ceil(rod_d.x2)), + static_cast(std::ceil(rod_d.y2)) }; + + std::shared_ptr image; + if (images_.contains(time)) { + image = images_.value(time); + image->EnsureAllocatedFromParams(params_, bounds, regionOfDefinition, + false); + } else { + image = std::make_shared(*this, params_, bounds, + regionOfDefinition, false); + images_.insert(time, image); + } + + uint8_t *dst = image->data(); + if (!dst) { + return; + } + + AVFramePtr src_frame = frame; + if (frame->format != expected_fmt || + frame->width != params_.width() || + frame->height != params_.height()) { + AVFramePtr converted = CreateAVFramePtr(); + converted->format = expected_fmt; + converted->width = params_.width(); + converted->height = params_.height(); + if (av_frame_get_buffer(converted.get(), 0) < 0) { + return; + } + + SwsContext *sws_ctx = sws_getContext( + frame->width, frame->height, + static_cast(frame->format), + converted->width, converted->height, + static_cast(converted->format), + SWS_POINT, nullptr, nullptr, nullptr); + if (!sws_ctx) { + return; + } + + sws_scale(sws_ctx, frame->data, frame->linesize, 0, frame->height, + converted->data, converted->linesize); + sws_freeContext(sws_ctx); + + src_frame = converted; + } + + int bytes_per_component = params_.format().byte_count(); + int bytes_per_row = params_.width() * params_.channel_count() * + bytes_per_component; + int src_row_bytes = src_frame->linesize[0]; + int dst_row_bytes = image->row_bytes(); + int copy_bytes = std::min(bytes_per_row, + std::min(src_row_bytes, dst_row_bytes)); + int copy_height = std::min(image->height(), src_frame->height); + + const uint8_t *src = src_frame->data[0]; + for (int y = 0; y < copy_height; ++y) { + std::memcpy(dst + y * dst_row_bytes, src + y * src_row_bytes, + copy_bytes); + } +} diff --git a/app/pluginSupport/OliveClip.h b/app/pluginSupport/OliveClip.h index 833402257..265648acb 100644 --- a/app/pluginSupport/OliveClip.h +++ b/app/pluginSupport/OliveClip.h @@ -23,10 +23,14 @@ #ifndef OLIVECLIP_H #define OLIVECLIP_H +#include "image.h" +#include "ofxCore.h" #include "ofxhClip.h" +#include "render/texture.h" #include "render/videoparams.h" #include +#include namespace olive { namespace plugin @@ -34,14 +38,15 @@ namespace plugin class OliveClipInstance: public OFX::Host::ImageEffect::ClipInstance { public: OliveClipInstance(OFX::Host::ImageEffect::Instance* effectInstance, - OFX::Host::ImageEffect::ClipDescriptor& desc,VideoParams params) + OFX::Host::ImageEffect::ClipDescriptor& desc,VideoParams ¶ms) : ClipInstance(effectInstance, desc) + , name_(desc.getName()) { params_ = params; } - OFX::Host::ImageEffect::Image& getOutputImage() + std::shared_ptr getOutputImage(OfxTime time) { - return image_; + return images_[time]; } const std::string &getUnmappedBitDepth() const override; @@ -59,11 +64,13 @@ public: OfxRectD getRegionOfDefinition(OfxTime time) const override; void setRegionOfDefinition(OfxRectD regionOfDefinition, OfxTime time); - void olive::plugin::OliveClipInstance::setDefaultRegionOfDefinition( - OfxRectD regionOfDefinition); + void setDefaultRegionOfDefinition(OfxRectD regionOfDefinition); # ifdef OFX_SUPPORTS_OPENGLRENDER OFX::Host::ImageEffect::Texture* loadTexture(OfxTime time, const char *format, const OfxRectD *optionalBounds) { return NULL; }; # endif + + void setInputTexture(TexturePtr texture, OfxTime time); + private: VideoParams params_; @@ -71,7 +78,8 @@ private: OfxRectD defaultRegionOfDefinitions_; - OFX::Host::ImageEffect::Image image_; + std::string name_; + QMap> images_; }; } } diff --git a/app/pluginSupport/OliveHost.cpp b/app/pluginSupport/OliveHost.cpp index d5ac1bcc1..e8f5d6a46 100644 --- a/app/pluginSupport/OliveHost.cpp +++ b/app/pluginSupport/OliveHost.cpp @@ -19,6 +19,8 @@ #include "node/project.h" #include "ofxhImageEffect.h" #include +#include +#include #include #include @@ -28,6 +30,8 @@ #include "OlivePluginInstance.h" #include "common/Current.h" +#include "ofxMessage.h" +#include using namespace OFX::Host; using namespace olive::plugin; @@ -93,15 +97,76 @@ OliveHost::makeDescriptor(const std::string &bundlePath, return desc; } -OFX::Host::ImageEffect::Instance* OliveHost::newInstance(void* clientData, - OFX::Host::ImageEffect::ImageEffectPlugin* plugin, - OFX::Host::ImageEffect::Descriptor& desc, +ImageEffect::Instance* OliveHost::newInstance(std::shared_ptr clientData, + ImageEffect::ImageEffectPlugin* plugin, + ImageEffect::Descriptor& desc, const std::string& context){ auto* instance = new OlivePluginInstance( plugin, desc, context, Current::getInstance().interactive()); if (clientData) { - instance->setNode(static_cast(clientData)); + instance->setNode(clientData); } instances_.append(instance); return instance; }; +OfxStatus olive::plugin::OliveHost::vmessage(const char *type, const char *id, const char *format, + va_list args){ + if (!type || !format) { + return kOfxStatFailed; + } + + char buffer[1024]; + buffer[0] = '\0'; + vsnprintf(buffer, sizeof(buffer), format, args); + QString message(buffer); + + if (strcmp(type, kOfxMessageQuestion) == 0) { + auto ret = QMessageBox::question(nullptr, "", message, + QMessageBox::Ok, QMessageBox::Cancel); + return (ret == QMessageBox::Ok) ? kOfxStatReplyYes : kOfxStatReplyNo; + } + + if (strcmp(type, kOfxMessageError) == 0) { + QMessageBox::critical(nullptr, "", message); + } else if (strcmp(type, kOfxMessageWarning) == 0) { + QMessageBox::warning(nullptr, "", message); + } else { + QMessageBox::information(nullptr, "", message); + } + + return kOfxStatOK; +} + +OfxStatus olive::plugin::OliveHost::setPersistentMessage( + const char *type, const char *id, const char *format, va_list args) +{ + if (!type || !format) { + return kOfxStatFailed; + } + + char buffer[1024]; + buffer[0] = '\0'; + vsnprintf(buffer, sizeof(buffer), format, args); + QString message(buffer); + + if (strcmp(type, kOfxMessageError) == 0) { + persistent_messages_.append({HostMessageType::Error, message}); + QMessageBox::critical(nullptr, "", message); + } else if (strcmp(type, kOfxMessageWarning) == 0) { + persistent_messages_.append({HostMessageType::Warning, message}); + QMessageBox::warning(nullptr, "", message); + } else if (strcmp(type, kOfxMessageMessage) == 0) { + persistent_messages_.append({HostMessageType::Message, message}); + QMessageBox::information(nullptr, "", message); + } else { + return kOfxStatFailed; + } + + return kOfxStatOK; +} + +OfxStatus olive::plugin::OliveHost::clearPersistentMessage() +{ + persistent_messages_.clear(); + return kOfxStatOK; +} diff --git a/app/pluginSupport/OliveHost.h b/app/pluginSupport/OliveHost.h index 2c8f96522..f07bfaa76 100644 --- a/app/pluginSupport/OliveHost.h +++ b/app/pluginSupport/OliveHost.h @@ -17,6 +17,7 @@ */ #ifndef OLIVE_HOST_H #define OLIVE_HOST_H +#include "node/plugins/Plugin.h" #include "ofxhHost.h" #include "ofxhImageEffectAPI.h" #include "ofxCore.h" @@ -32,6 +33,15 @@ #include namespace olive { namespace plugin { +enum class HostMessageType{ + Error, + Warning, + Message +}; +struct HostPersistentMessage{ + HostMessageType type; + QString message; +}; void loadPlugins(QString path); @@ -47,7 +57,7 @@ public: return true; }; - OFX::Host::ImageEffect::Instance* newInstance(void* clientData, + OFX::Host::ImageEffect::Instance* newInstance(std::shared_ptr ptr, OFX::Host::ImageEffect::ImageEffectPlugin* plugin, OFX::Host::ImageEffect::Descriptor& desc, const std::string& context) override; @@ -63,11 +73,28 @@ public: OFX::Host::ImageEffect::Descriptor *makeDescriptor( const std::string &bundlePath, OFX::Host::ImageEffect::ImageEffectPlugin *plugin) override; + /// vmessage + virtual OfxStatus vmessage(const char *type, const char *id, + const char *format, va_list args); + /// vmessage + virtual OfxStatus setPersistentMessage(const char *type, const char *id, + const char *format, va_list args); + /// vmessage + virtual OfxStatus clearPersistentMessage(); + +#ifdef OFX_SUPPORTS_OPENGLRENDER + /// @see OfxImageEffectOpenGLRenderSuiteV1.flushResources() + virtual OfxStatus flushOpenGLResources() const + { + return kOfxStatFailed; + }; +#endif private: QList descriptors_; QList instances_; + QList persistent_messages_; }; } } -#endif \ No newline at end of file +#endif diff --git a/app/pluginSupport/OlivePluginInstance.cpp b/app/pluginSupport/OlivePluginInstance.cpp index bfa4c9efe..a72d391ee 100644 --- a/app/pluginSupport/OlivePluginInstance.cpp +++ b/app/pluginSupport/OlivePluginInstance.cpp @@ -94,12 +94,18 @@ OfxStatus OlivePluginInstance::setPersistentMessage(const char *type, const char } else { return kOfxStatFailed; } + if (node_) { + emit node_->MessageCountChanged(); + } return kOfxStatOK; } OfxStatus OlivePluginInstance::clearPersistentMessage() { persistentErrors_.clear(); // TODO: tell the shell to remove message. + if (node_) { + emit node_->MessageCountChanged(); + } return kOfxStatOK; } void OlivePluginInstance::getProjectSize(double &xSize, double &ySize) const diff --git a/app/pluginSupport/OlivePluginInstance.h b/app/pluginSupport/OlivePluginInstance.h index 3f3cf6857..bf1ba9e71 100644 --- a/app/pluginSupport/OlivePluginInstance.h +++ b/app/pluginSupport/OlivePluginInstance.h @@ -21,6 +21,7 @@ #include "ofxImageEffect.h" #include #include "ofxhImageEffect.h" +#include "node/plugins/Plugin.h" #include "render/videoparams.h" #include @@ -71,7 +72,7 @@ public: { this->params_=params; } - void setNode(PluginNode* node) + void setNode(std::shared_ptr node) { node_ = node; } @@ -90,7 +91,15 @@ public: const char* format, va_list args) override; - OfxStatus clearPersistentMessage() override; + OfxStatus clearPersistentMessage() override; + int persistentMessageCount() const + { + return persistentErrors_.size(); + } + const QList &persistentMessages() const + { + return persistentErrors_; + } void getProjectSize(double& xSize, double& ySize) const override; void getProjectOffset(double& xOffset, double& yOffset) const override; @@ -174,6 +183,8 @@ public: /// get the first and last times available on the effect's timeline virtual void timeLineGetBounds(double &t1, double &t2); + + private: QList persistentErrors_; VideoParams params_; diff --git a/app/pluginSupport/image.cpp b/app/pluginSupport/image.cpp new file mode 100644 index 000000000..978aacebe --- /dev/null +++ b/app/pluginSupport/image.cpp @@ -0,0 +1,245 @@ +/* + * Olive Community Edition - Non-Linear Video Editor + * Copyright (C) 2026 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "image.h" + +#include "ofxImageEffect.h" + +#include + +namespace olive { +namespace plugin { + +static const char *PixelDepthToOfx(core::PixelFormat format) +{ + switch (format) { + case core::PixelFormat::U8: + return kOfxBitDepthByte; + case core::PixelFormat::U16: + return kOfxBitDepthShort; + case core::PixelFormat::F16: + return kOfxBitDepthHalf; + case core::PixelFormat::F32: + return kOfxBitDepthFloat; + case core::PixelFormat::INVALID: + case core::PixelFormat::COUNT: + break; + } + + return kOfxBitDepthNone; +} + +static const char *ComponentsToOfx(int channel_count) +{ + switch (channel_count) { + case 1: + return kOfxImageComponentAlpha; + case 3: + return kOfxImageComponentRGB; + case 4: + return kOfxImageComponentRGBA; + default: + break; + } + + return kOfxImageComponentNone; +} + +Image::Image(OFX::Host::ImageEffect::ClipInstance &clip_instance) + : OFX::Host::ImageEffect::Image(clip_instance) + , width_(0) + , height_(0) + , format_(core::PixelFormat::INVALID) + , premultiplied_alpha_(false) + , channel_count_(0) + , row_bytes_(0) + , bounds_{0, 0, 0, 0} + , rod_{0, 0, 0, 0} +{ +} + +Image::Image(OFX::Host::ImageEffect::ClipInstance &clip_instance, + const VideoParams ¶ms, + const OfxRectI &bounds, + const OfxRectI &rod, + bool clear) + : OFX::Host::ImageEffect::Image(clip_instance) + , width_(0) + , height_(0) + , format_(core::PixelFormat::INVALID) + , premultiplied_alpha_(false) + , channel_count_(0) + , row_bytes_(0) + , bounds_{0, 0, 0, 0} + , rod_{0, 0, 0, 0} +{ + AllocateFromParams(params, bounds, rod, clear); +} + +Image::~Image() +{ +} + +void Image::AllocateFromParams(const VideoParams ¶ms, + const OfxRectI &bounds, + const OfxRectI &rod, + bool clear) +{ + Allocate(bounds.x2 - bounds.x1, + bounds.y2 - bounds.y1, + params.format(), + params.channel_count(), + params.premultiplied_alpha(), + bounds, + rod, + clear); +} + +void Image::EnsureAllocatedFromParams(const VideoParams ¶ms, + const OfxRectI &bounds, + const OfxRectI &rod, + bool clear) +{ + bool same = (width_ == bounds.x2 - bounds.x1) && + (height_ == bounds.y2 - bounds.y1) && + (format_ == params.format()) && + (channel_count_ == params.channel_count()) && + (premultiplied_alpha_ == params.premultiplied_alpha()) && + (bounds_.x1 == bounds.x1) && (bounds_.y1 == bounds.y1) && + (bounds_.x2 == bounds.x2) && (bounds_.y2 == bounds.y2) && + (rod_.x1 == rod.x1) && (rod_.y1 == rod.y1) && + (rod_.x2 == rod.x2) && (rod_.y2 == rod.y2); + + if (!same) { + AllocateFromParams(params, bounds, rod, clear); + } else if (clear && !image_.empty()) { + std::fill(image_.begin(), image_.end(), 0); + } +} + +void Image::Allocate(int width, + int height, + core::PixelFormat format, + int channel_count, + bool premultiplied_alpha, + const OfxRectI &bounds, + const OfxRectI &rod, + bool clear) +{ + width_ = width; + height_ = height; + format_ = format; + channel_count_ = channel_count; + premultiplied_alpha_ = premultiplied_alpha; + bounds_ = bounds; + rod_ = rod; + + int bytes_per_component = format_.byte_count(); + row_bytes_ = width_ * channel_count_ * bytes_per_component; + int buffer_size = row_bytes_ * height_; + if (buffer_size < 0) { + buffer_size = 0; + } + + image_.resize(static_cast(buffer_size)); + if (clear && !image_.empty()) { + std::fill(image_.begin(), image_.end(), 0); + } + + setPointerProperty(kOfxImagePropData, image_.data()); + setIntProperty(kOfxImagePropRowBytes, row_bytes_); + setIntProperty(kOfxImagePropBounds, bounds.x1, 0); + setIntProperty(kOfxImagePropBounds, bounds.y1, 1); + setIntProperty(kOfxImagePropBounds, bounds.x2, 2); + setIntProperty(kOfxImagePropBounds, bounds.y2, 3); + setIntProperty(kOfxImagePropRegionOfDefinition, rod.x1, 0); + setIntProperty(kOfxImagePropRegionOfDefinition, rod.y1, 1); + setIntProperty(kOfxImagePropRegionOfDefinition, rod.x2, 2); + setIntProperty(kOfxImagePropRegionOfDefinition, rod.y2, 3); + setStringProperty(kOfxImageEffectPropComponents, + ComponentsToOfx(channel_count_)); + setStringProperty(kOfxImageEffectPropPixelDepth, + PixelDepthToOfx(format_)); + setStringProperty(kOfxImageEffectPropPreMultiplication, + premultiplied_alpha_ ? kOfxImagePreMultiplied + : kOfxImageUnPreMultiplied); +} + +core::PixelFormat Image::pixel_format() +{ + if (format_ != core::PixelFormat::INVALID) { + return format_; + } + + std::string type = getStringProperty(kOfxImageEffectPropPixelDepth); + if (type == kOfxBitDepthByte) { + format_ = core::PixelFormat::U8; + } else if (type == kOfxBitDepthShort) { + format_ = core::PixelFormat::U16; + } else if (type == kOfxBitDepthHalf) { + format_ = core::PixelFormat::F16; + } else if (type == kOfxBitDepthFloat) { + format_ = core::PixelFormat::F32; + } else { + format_ = core::PixelFormat::INVALID; + } + return format_; +} + +bool Image::premultiplied_alpha() +{ + std::string premultiplied = + getStringProperty(kOfxImageEffectPropPreMultiplication); + premultiplied_alpha_ = (premultiplied == kOfxImagePreMultiplied); + return premultiplied_alpha_; +} + +int Image::width() +{ + int bounds[4] = {0}; + getIntPropertyN(kOfxImagePropBounds, bounds, 4); + width_ = bounds[2] - bounds[0]; + return width_; +} + +int Image::height() +{ + int bounds[4] = {0}; + getIntPropertyN(kOfxImagePropBounds, bounds, 4); + height_ = bounds[3] - bounds[1]; + return height_; +} + +int Image::channel_count() +{ + std::string type = getStringProperty(kOfxImageEffectPropComponents); + if (type == kOfxImageComponentAlpha) { + channel_count_ = 1; + } else if (type == kOfxImageComponentRGBA) { + channel_count_ = 4; + } else if (type == kOfxImageComponentRGB) { + channel_count_ = 3; + } else { + channel_count_ = 0; + } + return channel_count_; +} + +} // namespace plugin +} // namespace olive diff --git a/app/pluginSupport/image.h b/app/pluginSupport/image.h new file mode 100644 index 000000000..812f2ecf8 --- /dev/null +++ b/app/pluginSupport/image.h @@ -0,0 +1,86 @@ +/* + * Olive Community Edition - Non-Linear Video Editor + * Copyright (C) 2026 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef OLIVE_EDITOR_PLUGIN_IMAGE_H +#define OLIVE_EDITOR_PLUGIN_IMAGE_H + +#include "ofxCore.h" +#include "ofxhClip.h" +#include "olive/core/render/pixelformat.h" +#include "render/loopmode.h" +#include "render/videoparams.h" +#include +#include +namespace olive +{ +namespace plugin +{ +class Image : public OFX::Host::ImageEffect::Image { +public: + Image(OFX::Host::ImageEffect::ClipInstance &clip_instance); + Image(OFX::Host::ImageEffect::ClipInstance &clip_instance, + const VideoParams ¶ms, + const OfxRectI &bounds, + const OfxRectI &rod, + bool clear = true); + ~Image(); + uint8_t *data() { + return image_.empty() ? nullptr : image_.data(); + } + int width(); + int height(); + core::PixelFormat pixel_format(); + bool premultiplied_alpha(); + int channel_count(); + + void AllocateFromParams(const VideoParams ¶ms, + const OfxRectI &bounds, + const OfxRectI &rod, + bool clear = true); + void EnsureAllocatedFromParams(const VideoParams ¶ms, + const OfxRectI &bounds, + const OfxRectI &rod, + bool clear = false); + void Allocate(int width, + int height, + core::PixelFormat format, + int channel_count, + bool premultiplied_alpha, + const OfxRectI &bounds, + const OfxRectI &rod, + bool clear = true); + int row_bytes() const + { + return row_bytes_; + } +protected: + std::vector image_; + int width_; + int height_; + core::PixelFormat format_; + bool premultiplied_alpha_; + int channel_count_; + int row_bytes_; + OfxRectI bounds_; + OfxRectI rod_; +}; +} +} + +#endif //OLIVE_EDITOR_PLUGIN_IMAGE_H diff --git a/app/render/job/pluginjob.h b/app/render/job/pluginjob.h index c9f54ab24..6d901cac7 100644 --- a/app/render/job/pluginjob.h +++ b/app/render/job/pluginjob.h @@ -30,17 +30,31 @@ namespace plugin { class PluginJob :public AcceleratedJob{ public: - explicit PluginJob(OFX::Host::ImageEffect::Instance* pluginInstance, NodeValueRow row): AcceleratedJob() + explicit PluginJob(const OFX::Host::ImageEffect::Instance* pluginInstance, + const PluginNode* node, NodeValueRow row) + : AcceleratedJob() { - this->pluginInstance = pluginInstance; + this->pluginInstance_ = pluginInstance; + this->node_=node; + Insert(row); + } + + PluginNode *node() const { + return const_cast(node_); + } + + OFX::Host::ImageEffect::Instance* pluginInstance() { + return const_cast(pluginInstance_); } private: - OFX::Host::ImageEffect::Instance *pluginInstance=nullptr; + const OFX::Host::ImageEffect::Instance *pluginInstance_=nullptr; QHash> paramsOnTime; QHash params; + + const PluginNode *node_=nullptr; }; } // plugin diff --git a/app/render/plugin/pluginrenderer.cpp b/app/render/plugin/pluginrenderer.cpp index 86528a106..6be95abba 100644 --- a/app/render/plugin/pluginrenderer.cpp +++ b/app/render/plugin/pluginrenderer.cpp @@ -20,16 +20,293 @@ // // Created by mikesolar on 25-10-19. // +#include +#include +#include #define GL_PREAMBLE //QMutexLocker __l(&global_opengl_mutex); #include "pluginrenderer.h" - +#include "pluginSupport/OliveClip.h" #include "pluginSupport/OlivePluginInstance.h" +#include "common/ffmpegutils.h" +#include "ofxImageEffect.h" +extern "C"{ +#include +#include +#include +} -void olive::plugin::PluginRenderer::Blit(QVariant shader, - olive::AcceleratedJob &job, - olive::Texture *destination, - olive::VideoParams destination_params, - bool clear_destination) +static AVPixelFormat GetOfxAVPixelFormat(const OFX::Host::ImageEffect::Image &image, + int *bytes_per_pixel) { + const std::string &depth = image.getStringProperty(kOfxImageEffectPropPixelDepth); + const std::string &components = image.getStringProperty(kOfxImageEffectPropComponents); -} \ No newline at end of file + olive::core::PixelFormat pixel_format = olive::core::PixelFormat::INVALID; + if (depth == kOfxBitDepthByte) { + pixel_format = olive::core::PixelFormat::U8; + } else if (depth == kOfxBitDepthShort) { + pixel_format = olive::core::PixelFormat::U16; + } else if (depth == kOfxBitDepthHalf) { + pixel_format = olive::core::PixelFormat::F16; + } else if (depth == kOfxBitDepthFloat) { + pixel_format = olive::core::PixelFormat::F32; + } + + int channel_count = 0; + if (components == kOfxImageComponentRGBA) { + channel_count = 4; + } else if (components == kOfxImageComponentRGB) { + channel_count = 3; + } else if (components == kOfxImageComponentAlpha) { + channel_count = 1; + } + + AVPixelFormat pix_fmt = olive::FFmpegUtils::GetFFmpegPixelFormat(pixel_format, channel_count); + if (pix_fmt == AV_PIX_FMT_NONE && channel_count == 1) { + if (pixel_format == olive::core::PixelFormat::U8) { + pix_fmt = AV_PIX_FMT_GRAY8; + } else if (pixel_format == olive::core::PixelFormat::U16) { + pix_fmt = AV_PIX_FMT_GRAY16LE; + } + } + + if (pix_fmt == AV_PIX_FMT_NONE) { + return AV_PIX_FMT_NONE; + } + + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmt); + if (!desc) { + return AV_PIX_FMT_NONE; + } + + int bits_per_pixel = av_get_bits_per_pixel(desc); + if (bits_per_pixel <= 0 || bits_per_pixel % 8 != 0) { + return AV_PIX_FMT_NONE; + } + + *bytes_per_pixel = bits_per_pixel / 8; + return pix_fmt; +} + +static olive::AVFramePtr create_avframe_from_ofx_image(OFX::Host::ImageEffect::Image &image) +{ + void *data_ptr = image.getPointerProperty(kOfxImagePropData); + if (!data_ptr) { + return nullptr; + } + + int bounds[4] = {0, 0, 0, 0}; + image.getIntPropertyN(kOfxImagePropBounds, bounds, 4); + int width = bounds[2] - bounds[0]; + int height = bounds[3] - bounds[1]; + if (width <= 0 || height <= 0) { + return nullptr; + } + + int bytes_per_pixel = 0; + AVPixelFormat pix_fmt = GetOfxAVPixelFormat(image, &bytes_per_pixel); + if (pix_fmt == AV_PIX_FMT_NONE || bytes_per_pixel <= 0) { + return nullptr; + } + + int row_bytes = image.getIntProperty(kOfxImagePropRowBytes); + if (row_bytes <= 0) { + row_bytes = width * bytes_per_pixel; + } + + uint8_t *src = static_cast(data_ptr); + src += bounds[1] * row_bytes + bounds[0] * bytes_per_pixel; + + olive::AVFramePtr frame = olive::CreateAVFramePtr(); + frame->width = width; + frame->height = height; + frame->format = pix_fmt; + + if (av_frame_get_buffer(frame.get(), 0) < 0) { + return nullptr; + } + + const int copy_bytes = width * bytes_per_pixel; + for (int y = 0; y < height; ++y) { + std::memcpy(frame->data[0] + y * frame->linesize[0], + src + y * row_bytes, + copy_bytes); + } + + return frame; +} + +static AVPixelFormat GetDestinationAVPixelFormat(const olive::VideoParams ¶ms) +{ + AVPixelFormat pix_fmt = + olive::FFmpegUtils::GetFFmpegPixelFormat(params.format(), + params.channel_count()); + if (pix_fmt == AV_PIX_FMT_NONE && params.channel_count() == 1) { + if (params.format() == olive::core::PixelFormat::U8) { + pix_fmt = AV_PIX_FMT_GRAY8; + } else if (params.format() == olive::core::PixelFormat::U16) { + pix_fmt = AV_PIX_FMT_GRAY16LE; + } + } + return pix_fmt; +} + +static olive::AVFramePtr ConvertFrameIfNeeded(olive::AVFramePtr src, + const olive::VideoParams &dst_params) +{ + if (!src) { + return nullptr; + } + + AVPixelFormat dst_fmt = GetDestinationAVPixelFormat(dst_params); + if (dst_fmt == AV_PIX_FMT_NONE) { + return src; + } + + if (src->format == dst_fmt && + src->width == dst_params.width() && + src->height == dst_params.height()) { + return src; + } + + olive::AVFramePtr dst = olive::CreateAVFramePtr(); + dst->format = dst_fmt; + dst->width = dst_params.width(); + dst->height = dst_params.height(); + if (av_frame_get_buffer(dst.get(), 0) < 0) { + return src; + } + + SwsContext *sws_ctx = sws_getContext( + src->width, src->height, static_cast(src->format), + dst->width, dst->height, dst_fmt, SWS_POINT, + nullptr, nullptr, nullptr); + if (!sws_ctx) { + return src; + } + + sws_scale(sws_ctx, src->data, src->linesize, 0, src->height, + dst->data, dst->linesize); + sws_freeContext(sws_ctx); + + return dst; +} + +void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::PluginJob& job, + olive::Texture *destination, + olive::VideoParams destination_params, + bool clear_destination, bool interactive) +{ + auto instance=job.pluginInstance(); + OfxStatus stat; + stat = instance->createInstanceAction(); + if(stat != kOfxStatOK && stat != kOfxStatReplyDefault) { + return; + } + // now we need to to call getClipPreferences on the instance so that it does the clip component/depth + // logic and caches away the components and depth on each clip. + bool ok = instance->getClipPreferences(); + if (!ok) { + return; + } + + // current render scale of 1 + 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 = src->params().width(); + renderWindow.y2 = src->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; + stat = instance->beginRenderAction(0, numFramesToRender, + 1.0, false, renderScale, true, + interactive); + if (stat != kOfxStatOK && stat != kOfxStatReplyDefault) { + return; + } + + OliveClipInstance *clip=dynamic_cast(instance->getClip("Output")); + if (!clip) { + instance->endRenderAction(0, numFramesToRender, 1.0, interactive, renderScale, true,interactive + ); + return; + } + + // call get region of interest on each of the inputs + OfxTime frame = 0; + + const NodeValueRow &values = job.GetValues(); + const auto &clips = instance->getDescriptor().getClips(); + for (const auto &entry : clips) { + if (entry.first == kOfxImageEffectOutputClipName) { + continue; + } + OliveClipInstance *input_clip = + dynamic_cast(instance->getClip(entry.first)); + if (input_clip) { + QString clip_key = QString::fromStdString(entry.first); + TexturePtr input_tex = values.value(clip_key).toTexture(); + if (!input_tex && + entry.first == kOfxImageEffectSimpleSourceClipName) { + input_tex = values.value(kTextureInput).toTexture(); + } + if (input_tex) { + input_clip->setInputTexture(input_tex, frame); + } + } + } + // get the RoI for each input clip + // the regions of interest for each input clip are returned in a std::map + // on a real host, these will be the regions of each input clip that the + // effect needs to render a given frame (clipped to the RoD). + // + // In our example we are doing full frame fetches regardless. + std::map rois; + stat = instance->getRegionOfInterestAction(frame, renderScale, + regionOfInterest, rois); + assert(stat == kOfxStatOK || stat == kOfxStatReplyDefault); + + // render a frame + stat = instance->renderAction(0,kOfxImageFieldBoth,renderWindow, renderScale, true, interactive, interactive); + assert(stat == kOfxStatOK); + + // get the output image buffer + std::shared_ptr output_image = clip->getOutputImage(frame); + if (!output_image) { + instance->endRenderAction(frame, numFramesToRender, 1.0, interactive, renderScale, true,interactive + ); + return; + } + + AVFramePtr frame_ptr = create_avframe_from_ofx_image(*output_image); + if (!frame_ptr) { + instance->endRenderAction(0, numFramesToRender, 1.0, interactive, renderScale, true,interactive + ); + return; + } + + AVFramePtr converted = ConvertFrameIfNeeded(frame_ptr, destination_params); + destination->handleFrame(converted); + + instance->endRenderAction(0, numFramesToRender, 1.0, interactive, renderScale, true,interactive + ); + +} diff --git a/app/render/plugin/pluginrenderer.h b/app/render/plugin/pluginrenderer.h index f1964baa3..13e725598 100644 --- a/app/render/plugin/pluginrenderer.h +++ b/app/render/plugin/pluginrenderer.h @@ -43,10 +43,10 @@ public: PluginRenderer(QObject *parent=nullptr):OpenGLRenderer(parent){}; virtual ~PluginRenderer() override{}; protected: - virtual void Blit(QVariant shader, olive::AcceleratedJob& job, + virtual void RenderPlugin(TexturePtr src, olive::plugin::PluginJob& job, olive::Texture *destination, olive::VideoParams destination_params, - bool clear_destination) override; + bool clear_destination, bool interactive); }; } diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 73f89d487..2f3ba888f 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -175,83 +175,7 @@ void RenderProcessor::Run() = node->getPlugin(); std::unique_ptr instance(plugin->createInstance(kOfxImageEffectContextFilter, NULL)); - OfxStatus stat; - stat = instance->createInstanceAction(); - if(stat != kOfxStatOK && stat != kOfxStatReplyDefault) { - ticket_->Finish(); - return; - } - // now we need to to call getClipPreferences on the instance so that it does the clip component/depth - // logic and caches away the components and depth on each clip. - bool ok = instance->getClipPreferences(); - if (!ok) { - ticket_->Finish(); - return; - } - // current render scale of 1 - 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 = ticket_->property("size").value().width(); - renderWindow.y2 = ticket_->property("size").value().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 = params.width(); - regionOfDefinition.y2 = params.height(); - - - int numFramesToRender=ticket_->property("time").value().toDouble() - * params.frame_rate().toDouble(); - stat = instance->beginRenderAction(0, numFramesToRender, 1.0, false, renderScale, /*sequential=#1#true, /*interactive=#1#false - ); - if (stat != kOfxStatOK && stat != kOfxStatReplyDefault) { - ticket_->Finish(); - return; - } - - plugin::OliveClipInstance *clip=dynamic_cast(instance->getClip("Output")); - for(int t = 0; t <= numFramesToRender; ++t) - { - // call get region of interest on each of the inputs - OfxTime frame = t; - - // get the RoI for each input clip - // the regions of interest for each input clip are returned in a std::map - // on a real host, these will be the regions of each input clip that the - // effect needs to render a given frame (clipped to the RoD). - // - // In our example we are doing full frame fetches regardless. - std::map rois; - stat = instance->getRegionOfInterestAction(frame, renderScale, - regionOfInterest, rois); - assert(stat == kOfxStatOK || stat == kOfxStatReplyDefault); - - // render a frame - stat = instance->renderAction(t,kOfxImageFieldBoth,renderWindow, renderScale, /*sequential=#1#true, /*interactive=#1#false, /*draft=#1#false); - assert(stat == kOfxStatOK); - - // get the output image buffer - OFX::Host::ImageEffect::Image *outputImage = clip->getOutputImage(); - - std::ostringstream ss; - ss << "Output." << t << ".ppm"; - exportToPPM(ss.str(), outputImage); - } - - instance->endRenderAction(0, numFramesToRender, 1.0, false, renderScale, /*sequential=#1#true, /*interactive=#1#false - ); } */ @@ -615,7 +539,7 @@ void RenderProcessor::ProcessShader(TexturePtr destination, const Node *node, locker.unlock(); // Run shader - render_ctx_->BlitToTexture(shader, *job, destination.get()); + render_ctx_->BlitToTexture(shader, const_cast(*job), destination.get()); } void RenderProcessor::ProcessSamples(SampleBuffer &destination, diff --git a/app/render/texture.h b/app/render/texture.h index c0481dcb4..e464faeda 100644 --- a/app/render/texture.h +++ b/app/render/texture.h @@ -154,6 +154,9 @@ public: { frame_=ptr; } + AVFramePtr frame(){ + return frame_; + } private: Renderer *renderer_; diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index 2c35c87cd..4d67b5b4c 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -27,6 +27,7 @@ #include "node/group/group.h" #include "node/nodeundo.h" #include "node/project/sequence/sequence.h" +#include "pluginSupport/OlivePluginInstance.h" namespace olive { @@ -49,6 +50,8 @@ NodeParamViewItem::NodeParamViewItem( QWidget *parent) : super(parent) , body_(nullptr) + , message_label_(nullptr) + , message_container_(nullptr) , node_(node) , create_checkboxes_(create_checkboxes) , ctx_(nullptr) @@ -62,6 +65,8 @@ NodeParamViewItem::NodeParamViewItem( connect(node_, &Node::LabelChanged, this, &NodeParamViewItem::Retranslate); connect(node_, &Node::InputArraySizeChanged, this, &NodeParamViewItem::InputArraySizeChanged); + connect(node_, &Node::MessageCountChanged, this, + &NodeParamViewItem::UpdateMessagePanel); // FIXME: Implemented to pick up when an input is set to hidden or not - DEFINITELY not a fast // way of doing this, but "fine" for now. @@ -93,6 +98,12 @@ void NodeParamViewItem::RecreateBody() body_->setParent(nullptr); body_->deleteLater(); } + if (message_container_) { + message_container_->setParent(nullptr); + message_container_->deleteLater(); + message_container_ = nullptr; + message_label_ = nullptr; + } body_ = new NodeParamViewItemBody(node_, create_checkboxes_, this); connect(body_, &NodeParamViewItemBody::RequestSelectNode, this, @@ -106,7 +117,57 @@ void NodeParamViewItem::RecreateBody() body_->Retranslate(); body_->SetTimebase(timebase_); body_->SetTimeTarget(time_target_); - SetBody(body_); + + message_container_ = new QWidget(this); + QVBoxLayout *message_layout = new QVBoxLayout(message_container_); + message_layout->setContentsMargins(0, 0, 0, 0); + message_layout->setSpacing(4); + + message_label_ = new QLabel(message_container_); + message_label_->setWordWrap(true); + message_label_->setTextInteractionFlags(Qt::TextSelectableByMouse); + message_label_->setStyleSheet( + QStringLiteral("background: rgba(0, 0, 0, 0.06); padding: 6px;")); + message_layout->addWidget(message_label_); + message_layout->addWidget(body_); + + SetBody(message_container_); + UpdateMessagePanel(); +} + +void NodeParamViewItem::UpdateMessagePanel() +{ + if (!message_label_) { + return; + } + + auto *instance = node_->getPluginInstance(); + auto *olive_instance = + dynamic_cast(instance); + if (!olive_instance || olive_instance->persistentMessageCount() == 0) { + message_label_->setVisible(false); + return; + } + + QStringList lines; + for (const auto &msg : olive_instance->persistentMessages()) { + QString prefix; + switch (msg.type) { + case plugin::ErrorType::Error: + prefix = QStringLiteral("Error"); + break; + case plugin::ErrorType::Warning: + prefix = QStringLiteral("Warning"); + break; + case plugin::ErrorType::Message: + prefix = QStringLiteral("Message"); + break; + } + lines.append(QStringLiteral("%1: %2").arg(prefix, msg.message)); + } + + message_label_->setText(lines.join('\n')); + message_label_->setVisible(true); } int NodeParamViewItem::GetElementY(const NodeInput &c) const diff --git a/app/widget/nodeparamview/nodeparamviewitem.h b/app/widget/nodeparamview/nodeparamviewitem.h index 787178c1b..9b74b36f9 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.h +++ b/app/widget/nodeparamview/nodeparamviewitem.h @@ -232,6 +232,8 @@ protected slots: private: NodeParamViewItemBody *body_; + QLabel *message_label_; + QWidget *message_container_; Node *node_; @@ -247,6 +249,7 @@ private: private slots: void RecreateBody(); + void UpdateMessagePanel(); }; } diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index fddd30c3d..8c5763b91 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -32,6 +32,7 @@ #include "node/group/group.h" #include "node/nodeundo.h" #include "node/project/serializer/serializer.h" +#include "panel/panelmanager.h" #include "node/traverser.h" #include "ui/icons/icons.h" #include "widget/menu/menushared.h" @@ -639,6 +640,15 @@ void NodeView::mouseDoubleClickEvent(QMouseEvent *event) dynamic_cast(itemAt(event->pos())); if (item_at_cursor) { item_at_cursor->ToggleExpanded(); + if (PanelManager::instance()) { + if (PanelWidget *panel = PanelManager::instance() + ->GetPanelWithName( + QStringLiteral("ParamPanel"))) { + panel->show(); + panel->raise(); + panel->setFocus(); + } + } } } } diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index d25098fed..93e55f9c3 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -28,6 +28,7 @@ #include "config/config.h" #include "core.h" #include "node/nodeundo.h" +#include "pluginSupport/OlivePluginInstance.h" #include "nodeview.h" #include "nodeviewscene.h" #include "ui/colorcoding.h" @@ -68,6 +69,8 @@ NodeViewItem::NodeViewItem(Node *node, const QString &input, int element, &NodeViewItem::NodeAppearanceChanged); connect(node_, &Node::ColorChanged, this, &NodeViewItem::NodeAppearanceChanged); + connect(node_, &Node::MessageCountChanged, this, + &NodeViewItem::NodeAppearanceChanged); if (IsOutputItem()) { connect(node_, &Node::InputAdded, this, @@ -428,6 +431,41 @@ void NodeViewItem::paint(QPainter *painter, arrow_size); } + if (IsOutputItem()) { + auto *instance = node_->getPluginInstance(); + auto *olive_instance = + dynamic_cast(instance); + int message_count = + olive_instance ? olive_instance->persistentMessageCount() : 0; + + if (message_count > 0) { + QString badge_text = QString::number(message_count); + QFont badge_font = painter->font(); + badge_font.setPointSizeF(badge_font.pointSizeF() * 0.7); + painter->setFont(badge_font); + + QFontMetrics badge_metrics(badge_font); + int text_width = badge_metrics.horizontalAdvance(badge_text); + int text_height = badge_metrics.height(); + int pad = text_height / 3; + int badge_width = qMax(text_width + pad * 2, text_height + pad); + int badge_height = text_height + pad; + + QRectF badge_rect( + single_unit_rect.right() - badge_width - 4, + single_unit_rect.top() + 4, + badge_width, badge_height); + + painter->setPen(Qt::NoPen); + painter->setBrush(QColor(220, 50, 47)); + painter->drawRoundedRect(badge_rect, badge_height / 2, + badge_height / 2); + + painter->setPen(Qt::white); + painter->drawText(badge_rect, Qt::AlignCenter, badge_text); + } + } + // Draw final border (output only) if (IsOutputItem()) { QPen border_pen; diff --git a/ext/KDDockWidgets b/ext/KDDockWidgets index 6b0ef44eb..d24ba267d 160000 --- a/ext/KDDockWidgets +++ b/ext/KDDockWidgets @@ -1 +1 @@ -Subproject commit 6b0ef44eb189411d36c739ccde8a081a3e62034a +Subproject commit d24ba267da1a8e7e1f34d69db5e07336d68f6886 diff --git a/third_party/openfx/HostSupport/include/ofxhImageEffect.h b/third_party/openfx/HostSupport/include/ofxhImageEffect.h index 075ef0f22..bf0f7b980 100755 --- a/third_party/openfx/HostSupport/include/ofxhImageEffect.h +++ b/third_party/openfx/HostSupport/include/ofxhImageEffect.h @@ -14,7 +14,12 @@ #include "ofxhParam.h" #include "ofxhMemory.h" #include "ofxhInteract.h" - +#include +namespace olive { +namespace plugin { +class PluginNode; +} +} #ifdef _MSC_VER //Use visual studio extension #define __PRETTY_FUNCTION__ __FUNCSIG__ @@ -56,7 +61,7 @@ namespace OFX { /// \arg plugin - the plugin being created /// \arg desc - the descriptor for that plugin /// \arg context - the context to be created in - virtual Instance* newInstance(void* clientData, + virtual Instance* newInstance(std::shared_ptr clientData, ImageEffectPlugin* plugin, Descriptor& desc, const std::string& context) = 0; diff --git a/third_party/openfx/HostSupport/include/ofxhImageEffectAPI.h b/third_party/openfx/HostSupport/include/ofxhImageEffectAPI.h index ed762e771..e5b593fdc 100644 --- a/third_party/openfx/HostSupport/include/ofxhImageEffectAPI.h +++ b/third_party/openfx/HostSupport/include/ofxhImageEffectAPI.h @@ -4,6 +4,8 @@ #ifndef OFXH_IMAGE_EFFECT_API_H #define OFXH_IMAGE_EFFECT_API_H + + #include "ofxhPluginAPICache.h" #include "ofxhPluginCache.h" #include diff --git a/third_party/openfx/HostSupport/src/ofxhClip.cpp b/third_party/openfx/HostSupport/src/ofxhClip.cpp index fdc900389..aea7f7610 100755 --- a/third_party/openfx/HostSupport/src/ofxhClip.cpp +++ b/third_party/openfx/HostSupport/src/ofxhClip.cpp @@ -9,6 +9,7 @@ #include "ofxhBinary.h" #include "ofxhPropertySuite.h" #include "ofxhClip.h" +#include "ofxhPropertySuite.h" #include "ofxhImageEffect.h" #ifdef OFX_SUPPORTS_OPENGLRENDER #include "ofxGPURender.h" diff --git a/third_party/openfx/HostSupport/src/ofxhInteract.cpp b/third_party/openfx/HostSupport/src/ofxhInteract.cpp index 22ebc29d1..d37d2df22 100755 --- a/third_party/openfx/HostSupport/src/ofxhInteract.cpp +++ b/third_party/openfx/HostSupport/src/ofxhInteract.cpp @@ -7,7 +7,6 @@ // ofx host #include "ofxhBinary.h" -#include "ofxhPropertySuite.h" #include "ofxhClip.h" #include "ofxhParam.h" #include "ofxhMemory.h"