diff --git a/docs/zh/plans/riir/M9-oakplugin.md b/docs/zh/plans/riir/M9-oakplugin.md
index 74fe7bba0..38642e323 100644
--- a/docs/zh/plans/riir/M9-oakplugin.md
+++ b/docs/zh/plans/riir/M9-oakplugin.md
@@ -98,3 +98,28 @@ ldd liboakengine.so | grep oak # 只见 oak* 模
- instance:Shadertoy 类插件(CI 里可用的)创建/参数/渲染一帧非空。
- progress:回调触发与 cancel。
- `oakplugin_debug_alive_count()` 泄漏断言。
+
+## 6. 实施现状(2026-08-05)
+
+已完成。src/plugin/src/ 六个类(olivehost/oliveplugininstance/
+oliveclip/paraminstance/image/pluginprogressreporter)全部去Qt,
+QMessageBox/QApplication 依赖删除,改为 §4 裁决的 facade 回调
+装配。C ABI 落在 include/plugin/{error,host,instance}.h,
+OakPluginInstance 为引用计数值句柄(ctx/addref/release/
+abi_version)。
+
+偏离手册之处:
+
+- paraminstance 不直接持有 Node*,改持 OakNodeNode 值句柄,参数
+ 读写全部经 oaknode C ABI;node↔instance 的身份路由用新增的
+ oaknode_node_identity() 注册表实现。
+- oliveclip 的纹理为 OakRenderTexture 值句柄,oakrender 因此补了
+ texture/copier/ticket 一族 C API。
+- oaknode 的 transition stub(src/node/transition/pluginSupport/)
+ 桥接 oakplugin 真身头,liboaknode 运行期引用 oakplugin 符号
+ (dynamic_lookup);所有构建 oaknode 的 standalone 树都要把
+ src/plugin 加进构建,测试二进制必须链接 oakplugin。
+
+验证:build-oakplugin 100/100;codec 22/22、audio 40/40、
+task 110/110、render 49/49、timeline 121/121(均含 oakplugin
+接入后的全量回归)。
diff --git a/docs/zh/plans/riir/notes.md b/docs/zh/plans/riir/notes.md
index dcc53427e..8531f71f7 100644
--- a/docs/zh/plans/riir/notes.md
+++ b/docs/zh/plans/riir/notes.md
@@ -285,3 +285,35 @@ ColorManager)去Qt化过程中的删除与语义变化,迁移调用方时需
必须同时链两个库(各 standalone 驱动已接线)。
- oakcommon xml C API 新增 get_native 借用访问器(C++ only),
oaktimeline 的 load/save 经它取回 XmlStreamReader/Writer。
+
+## oakplugin 去Qt化的删除与语义变更(2026-08-05)
+
+- olivehost/oliveplugininstance/oliveclip/paraminstance/image/
+ pluginprogressreporter 六个类去Qt:QMessageBox/QApplication 全部
+ 删除,宿主消息与 undo 提交改为 facade 回调
+ (set_host_message_handler / set_undo_submit_callback /
+ ActiveViewerProvider / set_main_thread_id)。
+- paraminstance 的参数桥改走 oaknode C API(node_ 为 OakNodeNode
+ 值句柄);undo 命令走 oakundo C API;node↔instance 路由用新增的
+ oaknode_node_identity() 注册表。
+- oliveclip 的纹理改 OakRenderTexture 值句柄;为此 oakrender C API
+ 新增 texture is_dummy/get_frame/width/height/fb_format/
+ wrap_native 与 copier/ticket 族。
+- oaknode C API 新增 get_input_at_time/set_input_at_time_undoable
+ (_into)/node_identity/sequence_from_node/
+ sequence_set_default_parameters/find_input_footage。
+- avframeptr.h 从 src/codec/src/ffmpeg/ 上移到 src/common/src/
+ (codec 与 render 共用),transition 里的旧拷贝已删。
+- 新 C ABI:include/plugin/{error,host,instance}.h,
+ OakPluginInstance 为引用计数值句柄。
+- oaknode 的 transition stub(src/node/transition/pluginSupport/
+ {oliveplugininstance,olivehost}.h)改为桥接 oakplugin 真身头,
+ liboaknode 因此引用 oakplugin 符号;凡构建 oaknode 的 standalone
+ 树(codec/audio/task/render/timeline)都必须
+ add_subdirectory(src/plugin),且测试二进制要链接 oakplugin,
+ 否则 dynamic_lookup 悬空符号 jump-to-0(EXC_BAD_ACCESS
+ address=0x0)。
+- 修复 ffmpegdecoder.cpp 被 3d004c081 误删出
+ src/codec/src/ffmpeg/CMakeLists.txt 的回归:FFmpegDecoder 构造
+ 函数在 flat namespace 悬空,decoder probe/decode/encoder
+ roundtrip 三个测试 jump-to-0。
diff --git a/include/node/node.h b/include/node/node.h
index a132b5c86..e1bf12895 100644
--- a/include/node/node.h
+++ b/include/node/node.h
@@ -609,6 +609,39 @@ int oaknode_viewer_set_audio_params(OakNodeNode viewer,
*/
int oaknode_node_find_input_footage(OakNodeNode node, OakNodeFootage *out);
+/**
+ * @brief Value of an input at a specific time (Node::get_value_at_time(),
+ * element -1). Same POD rules as oaknode_node_get_input().
+ */
+int oaknode_node_get_input_at_time(OakNodeNode node,
+ const char *input_id, int64_t time_num,
+ int64_t time_den, oaknode_value *out);
+
+/**
+ * @brief Set an input's value at a specific time with keyframe logic
+ * (Node::set_value_at_time(), element -1, track 0,
+ * insert_on_all_tracks_if_no_key = true). `*out_command` receives
+ * an owned undo command handle.
+ */
+int oaknode_node_set_input_at_time_undoable(OakNodeNode node,
+ const char *input_id, int64_t time_num, int64_t time_den,
+ const oaknode_value *v, int track, OakUndoCommand *out_command);
+
+/**
+ * @brief Identity of the underlying node object as an opaque integer
+ * (address-cast; for registry keys only, never dereference).
+ */
+uintptr_t oaknode_node_identity(OakNodeNode node);
+
+/**
+ * @brief Append a value-at-time set into an existing multi command
+ * (same semantics as oaknode_node_set_input_at_time_undoable but
+ * batches into `multi_command` from oakundo_command_init_multi()).
+ */
+int oaknode_node_set_input_at_time_into(OakNodeNode node,
+ const char *input_id, int64_t time_num, int64_t time_den,
+ const oaknode_value *v, int track, OakUndoCommand multi_command);
+
/**
* @brief Create a command that removes a node from its graph together
* with its exclusive dependencies and disconnects its edges
diff --git a/include/node/sequence.h b/include/node/sequence.h
index ee76ed628..e040a785c 100644
--- a/include/node/sequence.h
+++ b/include/node/sequence.h
@@ -111,6 +111,12 @@ int oaknode_sequence_set_default_parameters(OakNodeSequence sequence);
*/
OakNodeNode oaknode_sequence_as_node(OakNodeSequence sequence);
+/**
+ * @brief Non-owning cast from a node handle to a sequence handle (empty
+ * ctx when the node is not a Sequence).
+ */
+OakNodeSequence oaknode_sequence_from_node(OakNodeNode node);
+
/**
* @brief Borrowed handle to the per-type track list.
*
diff --git a/include/plugin/error.h b/include/plugin/error.h
new file mode 100644
index 000000000..143493183
--- /dev/null
+++ b/include/plugin/error.h
@@ -0,0 +1,40 @@
+/***
+
+ Oak Video Editor - Non-Linear Video Editor
+ Copyright (C) 2026 Oak 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 OAK_EDITOR_PLUGIN_ERROR_H
+#define OAK_EDITOR_PLUGIN_ERROR_H
+
+#include
+
+/**
+ * @brief Status and error codes shared by all oakplugin C API families.
+ */
+#define OAKPLUGIN_OK 0 /**< Success. */
+#define OAKPLUGIN_E_INVALID (-1) /**< NULL handle or invalid argument. */
+#define OAKPLUGIN_E_STATE (-2) /**< Call not valid in the current state. */
+#define OAKPLUGIN_E_FAILED (-3) /**< The underlying operation failed. */
+#define OAKPLUGIN_E_NOT_FOUND (-4) /**< Entry not found. */
+#define OAKPLUGIN_E_NOMEM (-5) /**< Allocation failed. */
+#define OAKPLUGIN_E_CANCELLED (-6) /**< The operation was cancelled. */
+
+/** @brief ABI version stamped into every oakplugin handle. */
+#define OAKPLUGIN_ABI_VERSION 1
+
+#endif //OAK_EDITOR_PLUGIN_ERROR_H
diff --git a/include/plugin/host.h b/include/plugin/host.h
new file mode 100644
index 000000000..644fc166f
--- /dev/null
+++ b/include/plugin/host.h
@@ -0,0 +1,69 @@
+/***
+
+ Oak Video Editor - Non-Linear Video Editor
+ Copyright (C) 2026 Oak 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 OAK_EDITOR_PLUGIN_HOST_H
+#define OAK_EDITOR_PLUGIN_HOST_H
+
+#include "plugin/error.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Initialize the OFX host (olive::plugin::load_plugins() with the
+ * default search paths). Idempotent.
+ */
+int oakplugin_host_init(void);
+
+/** @brief Shut the host down (persistent messages cleared). */
+void oakplugin_host_shutdown(void);
+
+/** @brief Scan additional bundle directories. */
+int oakplugin_host_scan(const char *const *bundle_dirs, int dir_count);
+
+/** @brief Number of discovered plugins (>= 0), or a negative error. */
+int oakplugin_host_plugin_count(void);
+
+/** @brief Plugin identifier at index (two-stage string getter). */
+int oakplugin_host_plugin_id_at(int index, char *buf, int buf_size);
+
+/** @brief Plugin label for an identifier (two-stage; currently the
+ * identifier itself). OAKPLUGIN_E_NOT_FOUND for unknown ids. */
+int oakplugin_host_plugin_label(const char *plugin_id, char *buf,
+ int buf_size);
+
+/**
+ * @brief UI message handler for OFX host messages (question replies use
+ * OAKPLUGIN_MESSAGE_ANSWER_YES/NO). Without a handler, messages
+ * are logged and questions get "no".
+ */
+#define OAKPLUGIN_MESSAGE_ANSWER_NO 0
+#define OAKPLUGIN_MESSAGE_ANSWER_YES 1
+typedef int (*oakplugin_message_fn)(const char *type, const char *message,
+ void *userdata);
+void oakplugin_host_set_message_handler(oakplugin_message_fn fn,
+ void *userdata);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_PLUGIN_HOST_H
diff --git a/include/plugin/instance.h b/include/plugin/instance.h
new file mode 100644
index 000000000..52cd3a39d
--- /dev/null
+++ b/include/plugin/instance.h
@@ -0,0 +1,103 @@
+/***
+
+ Oak Video Editor - Non-Linear Video Editor
+ Copyright (C) 2026 Oak 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 OAK_EDITOR_PLUGIN_INSTANCE_H
+#define OAK_EDITOR_PLUGIN_INSTANCE_H
+
+#include
+
+#include "node/node.h"
+#include "plugin/error.h"
+#include "render/renderer.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Reference-counted handle to an OFX plugin instance
+ * (olive::plugin::OlivePluginInstance).
+ *
+ * Ownership/count semantics follow include/common/handle.h: create
+ * returns count 1, addref/release adjust it, release destroys at zero.
+ */
+typedef struct OakPluginInstance {
+ void *ctx;
+ void (*addref)(void *ctx);
+ void (*release)(void *ctx);
+ uint32_t abi_version; /**< OAKPLUGIN_ABI_VERSION. */
+} OakPluginInstance;
+
+/**
+ * @brief Create an instance of a discovered plugin (filter context).
+ * Returns an empty handle (ctx == NULL) for unknown ids/failure.
+ */
+OakPluginInstance oakplugin_instance_create(const char *plugin_id);
+
+/** @brief Release one reference. NULL/empty no-op; clears ctx. */
+void oakplugin_instance_free(OakPluginInstance *instance);
+
+/**
+ * @brief Set/get a parameter as an oaknode_value POD (type rules from
+ * node/node.h). String-typed params use
+ * oakplugin_instance_set_param_string()/get_param_string().
+ */
+int oakplugin_instance_set_param(OakPluginInstance instance,
+ const char *param_id,
+ const oaknode_value *value);
+int oakplugin_instance_get_param(OakPluginInstance instance,
+ const char *param_id, oaknode_value *out);
+int oakplugin_instance_set_param_string(OakPluginInstance instance,
+ const char *param_id,
+ const char *value);
+int oakplugin_instance_get_param_string(OakPluginInstance instance,
+ const char *param_id, char *buf,
+ int buf_size);
+
+/**
+ * @brief Render one frame through the instance (renderAction).
+ *
+ * `src` may be an empty handle for generator plugins. Textures stay
+ * owned by the caller (borrowed for the call).
+ */
+int oakplugin_instance_render(OakPluginInstance instance,
+ OakRenderTexture dst, OakRenderTexture src,
+ double time_seconds);
+
+/**
+ * @brief Progress callback for long renders (async return channel,
+ * 01 §4 exception). Return non-zero to abort processing.
+ */
+typedef int (*oakplugin_progress_fn)(double progress, void *userdata);
+int oakplugin_instance_set_progress_cb(OakPluginInstance instance,
+ oakplugin_progress_fn fn,
+ void *userdata);
+
+/** @brief Cancel any in-progress render/progress reporting. */
+int oakplugin_instance_cancel(OakPluginInstance instance);
+
+/** @brief Alive-count for leak assertions in tests. */
+int oakplugin_debug_alive_count(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_PLUGIN_INSTANCE_H
diff --git a/include/render/renderer.h b/include/render/renderer.h
index 9cd152f7c..7ab78f647 100644
--- a/include/render/renderer.h
+++ b/include/render/renderer.h
@@ -23,6 +23,10 @@
#include
+#ifdef __cplusplus
+#include
+#endif
+
#include "error.h"
#ifdef __cplusplus
@@ -229,9 +233,42 @@ int oakrender_display_texture_download(OakRenderTexture texture, void *pixels,
int oakrender_display_texture_get_params(OakRenderTexture texture,
oakrender_video_params *out);
+/** @brief Frame width/height in pixels (0 on empty). */
+int oakrender_codec_frame_width(OakCodecFrame frame);
+int oakrender_codec_frame_height(OakCodecFrame frame);
+
+/** @brief ffmpeg_bridge pixel format when the frame wraps a texture's
+ * CPU copy (an AVFramePtr); -1 otherwise. */
+int oakrender_codec_frame_fb_format(OakCodecFrame frame);
+
/** @brief Native texture id (0 on empty or a dummy/id-less texture). */
int oakrender_display_texture_id(OakRenderTexture texture);
+/** @brief 1 when the texture is a placeholder dummy (Texture::is_dummy()). */
+int oakrender_display_texture_is_dummy(OakRenderTexture texture);
+
+/**
+ * @brief The CPU frame stored in the texture, if any (Texture::frame()).
+ * *out receives a retained frame handle (empty when none).
+ */
+int oakrender_display_texture_get_frame(OakRenderTexture texture,
+ OakCodecFrame *out);
+
+#ifdef __cplusplus
+} /* extern "C" */
+
+namespace olive { class Texture; using TexturePtr = std::shared_ptr; }
+
+/**
+ * @brief Wrap a native TexturePtr in a retained handle (C++ only; used
+ * by oakrender internals when handing textures across the C ABI).
+ */
+OakRenderTexture oakrender_display_texture_wrap_native(
+ const olive::TexturePtr &texture);
+
+extern "C" {
+#endif
+
/* ---- Frame handle -------------------------------------------------------- */
/** @brief Create an empty CPU frame. Returns a handle with count 1. */
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 7a04522d3..840242adb 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -6,3 +6,4 @@ add_subdirectory(codec)
add_subdirectory(audio)
add_subdirectory(timeline)
add_subdirectory(task)
+add_subdirectory(plugin)
diff --git a/src/audio/standalone/CMakeLists.txt b/src/audio/standalone/CMakeLists.txt
index d798f7157..bdc37813a 100644
--- a/src/audio/standalone/CMakeLists.txt
+++ b/src/audio/standalone/CMakeLists.txt
@@ -156,3 +156,4 @@ if(BUILD_TESTS)
enable_testing()
add_subdirectory(${OAK_REPO_ROOT}/src/audio/tests ${CMAKE_BINARY_DIR}/audio_tests)
endif()
+add_subdirectory(${OAK_REPO_ROOT}/src/plugin ${CMAKE_BINARY_DIR}/plugin)
diff --git a/src/audio/tests/CMakeLists.txt b/src/audio/tests/CMakeLists.txt
index 27ac8b9f7..f8053c1d9 100644
--- a/src/audio/tests/CMakeLists.txt
+++ b/src/audio/tests/CMakeLists.txt
@@ -36,6 +36,7 @@ target_link_libraries(oakaudio-gtest PRIVATE
oakcodec
oakrender
oaknode
+ oakplugin
oakcommon
oakundo
olivecore
diff --git a/src/codec/src/ffmpeg/CMakeLists.txt b/src/codec/src/ffmpeg/CMakeLists.txt
index 90324c48c..2379115a3 100644
--- a/src/codec/src/ffmpeg/CMakeLists.txt
+++ b/src/codec/src/ffmpeg/CMakeLists.txt
@@ -14,7 +14,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-target_sources(oakcodec PRIVATE avframeptr.h ffmpegdecoder.cpp
+target_sources(oakcodec PRIVATE
+ ffmpegdecoder.cpp
ffmpegdecoder.h
ffmpegencoder.cpp
ffmpegencoder.h
diff --git a/src/codec/src/ffmpeg/ffmpegdecoder.h b/src/codec/src/ffmpeg/ffmpegdecoder.h
index db5ccf131..e618352b7 100644
--- a/src/codec/src/ffmpeg/ffmpegdecoder.h
+++ b/src/codec/src/ffmpeg/ffmpegdecoder.h
@@ -31,7 +31,7 @@
#include
#include "decoder.h"
-#include "ffmpeg/avframeptr.h"
+#include "avframeptr.h"
namespace olive
{
diff --git a/src/codec/standalone/CMakeLists.txt b/src/codec/standalone/CMakeLists.txt
index c6097aee1..1db39ca08 100644
--- a/src/codec/standalone/CMakeLists.txt
+++ b/src/codec/standalone/CMakeLists.txt
@@ -153,3 +153,4 @@ if(BUILD_TESTS)
enable_testing()
add_subdirectory(${OAK_REPO_ROOT}/src/codec/tests ${CMAKE_BINARY_DIR}/codec_tests)
endif()
+add_subdirectory(${OAK_REPO_ROOT}/src/plugin ${CMAKE_BINARY_DIR}/plugin)
diff --git a/src/codec/tests/CMakeLists.txt b/src/codec/tests/CMakeLists.txt
index 3ffea577c..40405574b 100644
--- a/src/codec/tests/CMakeLists.txt
+++ b/src/codec/tests/CMakeLists.txt
@@ -34,6 +34,7 @@ target_link_libraries(oakcodec-gtest PRIVATE
oakcodec
oakrender
oaknode
+ oakplugin
oakcommon
oakundo
olivecore
diff --git a/src/common/src/CMakeLists.txt b/src/common/src/CMakeLists.txt
index 25d248ca2..aae9830c6 100644
--- a/src/common/src/CMakeLists.txt
+++ b/src/common/src/CMakeLists.txt
@@ -16,6 +16,7 @@
# along with this program. If not, see .
add_library(oakcommon SHARED
+ avframeptr.h
commandlineparser.cpp
commandlineparser.h
colortransform.h
diff --git a/src/codec/src/ffmpeg/avframeptr.h b/src/common/src/avframeptr.h
similarity index 100%
rename from src/codec/src/ffmpeg/avframeptr.h
rename to src/common/src/avframeptr.h
diff --git a/src/node/c_api/node.cpp b/src/node/c_api/node.cpp
index cda4e4f2e..a62247c3a 100644
--- a/src/node/c_api/node.cpp
+++ b/src/node/c_api/node.cpp
@@ -1429,3 +1429,97 @@ int oaknode_node_find_input_footage(OakNodeNode node, OakNodeFootage *out)
return OAKNODE_E_FAILED;
}
}
+
+int oaknode_node_get_input_at_time(OakNodeNode node,
+ const char *input_id, int64_t time_num,
+ int64_t time_den, oaknode_value *out)
+{
+ if (!node.ctx || !input_id || !out) {
+ return OAKNODE_E_INVALID;
+ }
+ if (!has_input(oaknode_c_api::to_native(node), input_id)) {
+ return OAKNODE_E_NOT_FOUND;
+ }
+
+ try {
+ olive::Node *n = oaknode_c_api::to_native(node);
+ olive::Variant v = n->get_value_at_time(
+ input_id,
+ olive::core::Rational(int(time_num), int(time_den)));
+
+ olive::NodeValue::Type type = n->get_input_data_type(input_id);
+ return oaknode_c_api::value_from_variant(type, v, out);
+ } catch (...) {
+ return OAKNODE_E_FAILED;
+ }
+}
+
+int oaknode_node_set_input_at_time_undoable(OakNodeNode node,
+ const char *input_id, int64_t time_num, int64_t time_den,
+ const oaknode_value *v, int track, OakUndoCommand *out_command)
+{
+ if (!node.ctx || !input_id || !v || !out_command) {
+ return OAKNODE_E_INVALID;
+ }
+ if (!has_input(oaknode_c_api::to_native(node), input_id)) {
+ return OAKNODE_E_NOT_FOUND;
+ }
+
+ try {
+ olive::Variant value;
+ if (!oaknode_c_api::variant_from_value(v, &value)) {
+ return OAKNODE_E_INVALID;
+ }
+
+ auto *multi = new olive::MultiUndoCommand();
+ olive::Node::set_value_at_time(
+ olive::NodeInput(oaknode_c_api::to_native(node),
+ input_id),
+ olive::core::Rational(int(time_num), int(time_den)), value, track,
+ multi, true);
+
+ *out_command = oaknode_c_api::wrap_command(multi);
+ return out_command->ctx ? OAKNODE_OK : OAKNODE_E_NOMEM;
+ } catch (...) {
+ return OAKNODE_E_FAILED;
+ }
+}
+
+uintptr_t oaknode_node_identity(OakNodeNode node)
+{
+ return reinterpret_cast(
+ oaknode_c_api::to_native(node));
+}
+
+int oaknode_node_set_input_at_time_into(OakNodeNode node,
+ const char *input_id, int64_t time_num, int64_t time_den,
+ const oaknode_value *v, int track, OakUndoCommand multi_command)
+{
+ if (!node.ctx || !input_id || !v || !multi_command.ctx) {
+ return OAKNODE_E_INVALID;
+ }
+ if (!has_input(oaknode_c_api::to_native(node), input_id)) {
+ return OAKNODE_E_NOT_FOUND;
+ }
+
+ olive::UndoCommand *base = oakundo_capi::to_command(multi_command);
+ auto *multi = dynamic_cast(base);
+ if (!multi) {
+ return OAKNODE_E_INVALID;
+ }
+
+ try {
+ olive::Variant value;
+ if (!oaknode_c_api::variant_from_value(v, &value)) {
+ return OAKNODE_E_INVALID;
+ }
+ olive::Node::set_value_at_time(
+ olive::NodeInput(oaknode_c_api::to_native(node),
+ input_id),
+ olive::core::Rational(int(time_num), int(time_den)), value, track,
+ multi, true);
+ return OAKNODE_OK;
+ } catch (...) {
+ return OAKNODE_E_FAILED;
+ }
+}
diff --git a/src/node/c_api/nodehandle.h b/src/node/c_api/nodehandle.h
index 685ccc85d..7f0b0e56d 100644
--- a/src/node/c_api/nodehandle.h
+++ b/src/node/c_api/nodehandle.h
@@ -151,6 +151,14 @@ inline void mark_container_owned(Handle h)
}
}
+/**
+ * @brief Deleter for non-owning (borrowed) boxes: never deletes.
+ */
+inline void delete_noop(void *object)
+{
+ (void)object;
+}
+
/**
* @brief Deleter callback stamping the concrete C++ type.
*/
diff --git a/src/node/c_api/sequence.cpp b/src/node/c_api/sequence.cpp
index 5b3b27873..bd0305d7a 100644
--- a/src/node/c_api/sequence.cpp
+++ b/src/node/c_api/sequence.cpp
@@ -353,3 +353,17 @@ int oaknode_sequence_set_default_parameters(OakNodeSequence sequence)
return OAKNODE_E_FAILED;
}
}
+
+OakNodeSequence oaknode_sequence_from_node(OakNodeNode node)
+{
+ OakNodeSequence empty = {};
+ if (!node.ctx) {
+ return empty;
+ }
+ olive::Node *n = oaknode_c_api::to_native(node);
+ if (!n || !dynamic_cast(n)) {
+ return empty;
+ }
+ return make_handle(n, false,
+ &oaknode_c_api::delete_noop);
+}
diff --git a/src/node/standalone/CMakeLists.txt b/src/node/standalone/CMakeLists.txt
index 83494718f..c0cc28653 100644
--- a/src/node/standalone/CMakeLists.txt
+++ b/src/node/standalone/CMakeLists.txt
@@ -111,6 +111,8 @@ target_link_options(oaknode PRIVATE
# 把 oakrender 构建进来并链进测试二进制;未拆分模块符号同样悬置。
set(BUILD_TESTS OFF)
add_subdirectory(${OAK_REPO_ROOT}/src/render/src ${CMAKE_BINARY_DIR}/render)
+add_subdirectory(${OAK_REPO_ROOT}/src/render/c_api ${CMAKE_BINARY_DIR}/render_c_api)
+add_subdirectory(${OAK_REPO_ROOT}/src/plugin ${CMAKE_BINARY_DIR}/plugin)
set(BUILD_TESTS ON)
target_include_directories(oakrender BEFORE PUBLIC
@@ -148,7 +150,7 @@ target_link_libraries(oakrender PRIVATE
"-framework QuartzCore"
)
-target_link_libraries(oaknode-gtest PRIVATE oakrender oaktimeline)
+target_link_libraries(oaknode-gtest PRIVATE oakrender oaktimeline oakplugin)
# liboakrender references the oakengine_ipc_* C ABI (worker IPC) via
# dynamic_lookup; the real implementation (engine/src/capi/ipc.cpp) is still
diff --git a/src/node/transition/common/avframeptr.h b/src/node/transition/common/avframeptr.h
deleted file mode 100644
index eccb31423..000000000
--- a/src/node/transition/common/avframeptr.h
+++ /dev/null
@@ -1,140 +0,0 @@
-// Transitional copy of engine/common/avframeptr.h(本身已无 Qt,待下沉 oakcommon)。只增不删。
-/***
-
- Olive - Non-Linear Video Editor
- Copyright (C) 2022 Olive Team
- Modifications Copyright (C) 2025 mikesolar
-
- 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 OAK_AVFRAMEPTR_H
-#define OAK_AVFRAMEPTR_H
-
-#include
-
-#include
-
-#include
-
-namespace olive
-{
-
-/**
- * @brief C++ adapter around the ffmpeg_bridge frame handle
- *
- * Mirrors the AVFrame field access the codebase used to perform directly,
- * but every operation goes through the pure C bridge API so the editor
- * never touches FFmpeg itself. The underlying frame object always lives
- * inside the bridge library.
- */
-class AVFrame {
-public:
- AVFrame() :
- handle_(fb_frame_alloc())
- {
- }
-
- explicit AVFrame(FBFrame *handle) :
- handle_(handle)
- {
- }
-
- ~AVFrame()
- {
- if (handle_) {
- fb_frame_free(&handle_);
- }
- }
-
- AVFrame(const AVFrame &) = delete;
- AVFrame &operator=(const AVFrame &) = delete;
-
- FBFrame *handle() const { return handle_; }
-
- int width() const { return fb_frame_get_width(handle_); }
- void set_width(int w) { fb_frame_set_width(handle_, w); }
- int height() const { return fb_frame_get_height(handle_); }
- void set_height(int h) { fb_frame_set_height(handle_, h); }
- int format() const { return fb_frame_get_format(handle_); }
- void set_format(int f) { fb_frame_set_format(handle_, f); }
- int64_t pts() const { return fb_frame_get_pts(handle_); }
- void set_pts(int64_t p) { fb_frame_set_pts(handle_, p); }
- int64_t best_effort_timestamp() const
- {
- return fb_frame_get_best_effort_timestamp(handle_);
- }
- int nb_samples() const { return fb_frame_get_nb_samples(handle_); }
- void set_nb_samples(int n) { fb_frame_set_nb_samples(handle_, n); }
- int sample_rate() const { return fb_frame_get_sample_rate(handle_); }
- void set_sample_rate(int r) { fb_frame_set_sample_rate(handle_, r); }
- int color_range() const { return fb_frame_get_color_range(handle_); }
- void set_color_range(int r) { fb_frame_set_color_range(handle_, r); }
- int colorspace() const { return fb_frame_get_colorspace(handle_); }
- void set_colorspace(int cs) { fb_frame_set_colorspace(handle_, cs); }
- uint64_t channel_layout_mask() const
- {
- return fb_frame_get_channel_layout_mask(handle_);
- }
- void set_channel_layout_mask(uint64_t m)
- {
- fb_frame_set_channel_layout_mask(handle_, m);
- }
-
- bool is_hw() const { return fb_frame_is_hw(handle_) != 0; }
- int hw_transfer_data(const AVFrame *src)
- {
- return fb_frame_hw_transfer_data(handle_, src->handle_);
- }
- int get_buffer(int align) { return fb_frame_get_buffer(handle_, align); }
- int make_writable() { return fb_frame_make_writable(handle_); }
-
- uint8_t *data(int plane) { return fb_frame_get_data(handle_, plane); }
- const uint8_t *data(int plane) const
- {
- return fb_frame_get_data_const(handle_, plane);
- }
- void set_data(int plane, uint8_t *d)
- {
- fb_frame_set_data(handle_, plane, d);
- }
- int linesize(int plane) const
- {
- return fb_frame_get_linesize(handle_, plane);
- }
- void set_linesize(int plane, int l)
- {
- fb_frame_set_linesize(handle_, plane, l);
- }
-
-private:
- FBFrame *handle_;
-};
-
-using AVFramePtr = std::shared_ptr;
-
-inline AVFramePtr create_av_frame_ptr(FBFrame *f)
-{
- return std::make_shared(f);
-}
-
-inline AVFramePtr create_av_frame_ptr()
-{
- return std::make_shared();
-}
-
-}
-
-#endif // OAK_AVFRAMEPTR_H
diff --git a/src/node/transition/pluginSupport/olivehost.h b/src/node/transition/pluginSupport/olivehost.h
index 566ec65e9..947b8c14c 100644
--- a/src/node/transition/pluginSupport/olivehost.h
+++ b/src/node/transition/pluginSupport/olivehost.h
@@ -1,12 +1,3 @@
#pragma once
-// Transitional stub for engine/pluginSupport/olivehost.h (still Qt-based).
-// Only the surface oaknode uses. M9 replaces this with the real plugin
-// boundary.
-#include
-#include "ofxhImageEffectAPI.h"
-#include "ofxhPluginCache.h"
-namespace olive {
-namespace plugin {
-inline void load_plugins(const std::string &) {}
-}
-}
+// M9 收尾:桥接到 oakplugin 真身
+#include "../../../plugin/src/olivehost.h"
diff --git a/src/node/transition/pluginSupport/oliveplugininstance.h b/src/node/transition/pluginSupport/oliveplugininstance.h
index 60bbf2d7d..3092f4c69 100644
--- a/src/node/transition/pluginSupport/oliveplugininstance.h
+++ b/src/node/transition/pluginSupport/oliveplugininstance.h
@@ -1,30 +1,3 @@
#pragma once
-// Minimal syntax-check stub for engine/pluginSupport/oliveplugininstance.h
-// (real header is Qt-based, handled in a later milestone). Not in repo.
-#include
-
-#include "ofxhImageEffect.h"
-
-namespace olive
-{
-namespace plugin
-{
-class PluginNode;
-}
-
-class OlivePluginInstance : public OFX::Host::ImageEffect::Instance {
-public:
- void setNode(std::shared_ptr node)
- {
- node_ = node;
- }
- std::shared_ptr node() const
- {
- return node_;
- }
-
-private:
- std::shared_ptr node_;
-};
-
-}
+// M9 收尾:桥接到 oakplugin 真身
+#include "../../../plugin/src/oliveplugininstance.h"
diff --git a/src/plugin/CMakeLists.txt b/src/plugin/CMakeLists.txt
new file mode 100644
index 000000000..f41570121
--- /dev/null
+++ b/src/plugin/CMakeLists.txt
@@ -0,0 +1,6 @@
+add_subdirectory(src)
+add_subdirectory(c_api)
+
+if(BUILD_TESTS)
+ add_subdirectory(tests)
+endif()
diff --git a/src/plugin/c_api/CMakeLists.txt b/src/plugin/c_api/CMakeLists.txt
new file mode 100644
index 000000000..5c7f105b3
--- /dev/null
+++ b/src/plugin/c_api/CMakeLists.txt
@@ -0,0 +1,4 @@
+target_sources(oakplugin PRIVATE
+ host.cpp
+ instance.cpp
+)
diff --git a/src/plugin/c_api/host.cpp b/src/plugin/c_api/host.cpp
new file mode 100644
index 000000000..2be3f8863
--- /dev/null
+++ b/src/plugin/c_api/host.cpp
@@ -0,0 +1,155 @@
+/***
+
+ Oak Video Editor - Non-Linear Video Editor
+ Copyright (C) 2026 Oak 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 "plugin/host.h"
+
+#include
+#include
+#include
+
+#include "../src/olivehost.h"
+
+namespace
+{
+
+olive::plugin::HostMessageHandler forwarder_fn;
+oakplugin_message_fn user_fn;
+void *user_userdata;
+
+int copy_string(const std::string &value, char *buf, int buf_size)
+{
+ int needed = int(value.size()) + 1;
+ if (buf && buf_size >= needed) {
+ memcpy(buf, value.c_str(), needed);
+ }
+ return needed;
+}
+
+std::vector plugin_ids()
+{
+ std::vector ids;
+ OFX::Host::PluginCache *cache =
+ OFX::Host::PluginCache::getPluginCache();
+ if (!cache) {
+ return ids;
+ }
+ for (const auto &plugin : cache->getPlugins()) {
+ if (plugin) {
+ ids.push_back(plugin->getIdentifier());
+ }
+ }
+ return ids;
+}
+
+} // namespace
+
+int oakplugin_host_init(void)
+{
+ try {
+ olive::plugin::load_plugins();
+ return OAKPLUGIN_OK;
+ } catch (...) {
+ return OAKPLUGIN_E_FAILED;
+ }
+}
+
+void oakplugin_host_shutdown(void)
+{
+ // The OFX plugin cache is process-global; nothing to tear down here
+ // (the Current slots release their references on their own).
+}
+
+int oakplugin_host_scan(const char *const *bundle_dirs, int dir_count)
+{
+ if (!bundle_dirs || dir_count < 0) {
+ return OAKPLUGIN_E_INVALID;
+ }
+
+ try {
+ for (int i = 0; i < dir_count; i++) {
+ if (bundle_dirs[i]) {
+ olive::plugin::load_plugins(bundle_dirs[i]);
+ }
+ }
+ return OAKPLUGIN_OK;
+ } catch (...) {
+ return OAKPLUGIN_E_FAILED;
+ }
+}
+
+int oakplugin_host_plugin_count(void)
+{
+ try {
+ return int(plugin_ids().size());
+ } catch (...) {
+ return OAKPLUGIN_E_FAILED;
+ }
+}
+
+int oakplugin_host_plugin_id_at(int index, char *buf, int buf_size)
+{
+ try {
+ std::vector ids = plugin_ids();
+ if (index < 0 || index >= int(ids.size())) {
+ return OAKPLUGIN_E_NOT_FOUND;
+ }
+ return copy_string(ids[size_t(index)], buf, buf_size);
+ } catch (...) {
+ return OAKPLUGIN_E_FAILED;
+ }
+}
+
+int oakplugin_host_plugin_label(const char *plugin_id, char *buf,
+ int buf_size)
+{
+ if (!plugin_id) {
+ return OAKPLUGIN_E_INVALID;
+ }
+
+ try {
+ for (const std::string &id : plugin_ids()) {
+ if (id == plugin_id) {
+ return copy_string(id, buf, buf_size);
+ }
+ }
+ return OAKPLUGIN_E_NOT_FOUND;
+ } catch (...) {
+ return OAKPLUGIN_E_FAILED;
+ }
+}
+
+void oakplugin_host_set_message_handler(oakplugin_message_fn fn,
+ void *userdata)
+{
+ user_fn = fn;
+ user_userdata = userdata;
+
+ if (!fn) {
+ olive::plugin::set_host_message_handler(nullptr);
+ return;
+ }
+
+ olive::plugin::set_host_message_handler(
+ [](const char *type, const std::string &message) -> OfxStatus {
+ int answer = user_fn(type, message.c_str(), user_userdata);
+ return answer == OAKPLUGIN_MESSAGE_ANSWER_YES ? kOfxStatReplyYes
+ : kOfxStatReplyNo;
+ });
+}
diff --git a/src/plugin/c_api/instance.cpp b/src/plugin/c_api/instance.cpp
new file mode 100644
index 000000000..a2b3a5ea3
--- /dev/null
+++ b/src/plugin/c_api/instance.cpp
@@ -0,0 +1,399 @@
+/***
+
+ Oak Video Editor - Non-Linear Video Editor
+ Copyright (C) 2026 Oak 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 "plugin/instance.h"
+
+#include
+#include
+
+#include
+#include
+
+#include "../src/oliveclip.h"
+#include "../src/oliveplugininstance.h"
+
+namespace
+{
+
+struct InstanceBox {
+ olive::plugin::OlivePluginInstance *instance;
+ std::atomic refs;
+ oakplugin_progress_fn progress_fn;
+ void *progress_userdata;
+
+ InstanceBox(olive::plugin::OlivePluginInstance *i)
+ : instance(i)
+ , refs(1)
+ , progress_fn(nullptr)
+ , progress_userdata(nullptr)
+ {
+ }
+};
+
+std::atomic g_alive{ 0 };
+
+void box_addref(void *ctx)
+{
+ if (ctx) {
+ static_cast(ctx)->refs.fetch_add(1);
+ }
+}
+
+void box_release(void *ctx)
+{
+ if (!ctx) {
+ return;
+ }
+ InstanceBox *box = static_cast(ctx);
+ if (box->refs.fetch_sub(1) == 1) {
+ delete box->instance;
+ delete box;
+ g_alive--;
+ }
+}
+
+OakPluginInstance make_handle(olive::plugin::OlivePluginInstance *instance)
+{
+ OakPluginInstance handle = {};
+ if (!instance) {
+ return handle;
+ }
+
+ InstanceBox *box = new (std::nothrow) InstanceBox(instance);
+ if (!box) {
+ delete instance;
+ return handle;
+ }
+
+ handle.ctx = box;
+ handle.addref = box_addref;
+ handle.release = box_release;
+ handle.abi_version = OAKPLUGIN_ABI_VERSION;
+ g_alive++;
+ return handle;
+}
+
+InstanceBox *impl(OakPluginInstance h)
+{
+ if (!h.ctx) {
+ return nullptr;
+ }
+ return static_cast(h.ctx);
+}
+
+} // namespace
+
+OakPluginInstance oakplugin_instance_create(const char *plugin_id)
+{
+ if (!plugin_id) {
+ return OakPluginInstance{};
+ }
+
+ try {
+ OFX::Host::PluginCache *cache =
+ OFX::Host::PluginCache::getPluginCache();
+ if (!cache) {
+ return OakPluginInstance{};
+ }
+
+ for (const auto &plugin : cache->getPlugins()) {
+ if (plugin && plugin->getIdentifier() == plugin_id) {
+ auto *effect_plugin =
+ dynamic_cast(
+ plugin);
+ if (!effect_plugin) {
+ return OakPluginInstance{};
+ }
+ auto *instance = effect_plugin->createInstance(
+ kOfxImageEffectContextFilter, nullptr);
+ auto *olive_instance =
+ dynamic_cast(
+ instance);
+ if (!olive_instance) {
+ delete instance;
+ return OakPluginInstance{};
+ }
+ return make_handle(olive_instance);
+ }
+ }
+ return OakPluginInstance{};
+ } catch (...) {
+ return OakPluginInstance{};
+ }
+}
+
+void oakplugin_instance_free(OakPluginInstance *instance)
+{
+ if (!instance || !instance->ctx) {
+ return;
+ }
+ instance->release(instance->ctx);
+ instance->ctx = NULL;
+}
+
+int oakplugin_instance_set_param(OakPluginInstance instance,
+ const char *param_id,
+ const oaknode_value *value)
+{
+ InstanceBox *box = impl(instance);
+ if (!box || !param_id || !value) {
+ return OAKPLUGIN_E_INVALID;
+ }
+
+ try {
+ const std::map ¶ms_map =
+ box->instance->getParams();
+ auto it = params_map.find(param_id);
+ if (it == params_map.end() || !it->second) {
+ return OAKPLUGIN_E_NOT_FOUND;
+ }
+ OFX::Host::Param::Instance *param = it->second;
+
+ switch (value->type) {
+ case OAKNODE_VALUE_INT:
+ case OAKNODE_VALUE_COMBO:
+ return static_cast(param)
+ ->set(int(value->num)) == kOfxStatOK
+ ? OAKPLUGIN_OK
+ : OAKPLUGIN_E_FAILED;
+ case OAKNODE_VALUE_BOOL:
+ return static_cast(param)
+ ->set(value->num != 0) == kOfxStatOK
+ ? OAKPLUGIN_OK
+ : OAKPLUGIN_E_FAILED;
+ case OAKNODE_VALUE_FLOAT:
+ return static_cast(param)
+ ->set(value->f[0]) == kOfxStatOK
+ ? OAKPLUGIN_OK
+ : OAKPLUGIN_E_FAILED;
+ case OAKNODE_VALUE_VEC2:
+ return static_cast(param)
+ ->set(value->f[0], value->f[1]) == kOfxStatOK
+ ? OAKPLUGIN_OK
+ : OAKPLUGIN_E_FAILED;
+ case OAKNODE_VALUE_VEC3:
+ return static_cast(param)
+ ->set(value->f[0], value->f[1],
+ value->f[2]) == kOfxStatOK
+ ? OAKPLUGIN_OK
+ : OAKPLUGIN_E_FAILED;
+ case OAKNODE_VALUE_COLOR:
+ case OAKNODE_VALUE_VEC4:
+ return static_cast(param)
+ ->set(value->f[0], value->f[1], value->f[2],
+ value->f[3]) == kOfxStatOK
+ ? OAKPLUGIN_OK
+ : OAKPLUGIN_E_FAILED;
+ default:
+ return OAKPLUGIN_E_INVALID;
+ }
+ } catch (...) {
+ return OAKPLUGIN_E_FAILED;
+ }
+}
+
+int oakplugin_instance_get_param(OakPluginInstance instance,
+ const char *param_id, oaknode_value *out)
+{
+ InstanceBox *box = impl(instance);
+ if (!box || !param_id || !out) {
+ return OAKPLUGIN_E_INVALID;
+ }
+
+ try {
+ const std::map ¶ms_map =
+ box->instance->getParams();
+ auto it = params_map.find(param_id);
+ if (it == params_map.end() || !it->second) {
+ return OAKPLUGIN_E_NOT_FOUND;
+ }
+ OFX::Host::Param::Instance *param = it->second;
+
+ out->type = OAKNODE_VALUE_NONE;
+ const std::string &type = param->getType();
+ if (type == kOfxParamTypeInteger || type == kOfxParamTypeChoice) {
+ int v = 0;
+ static_cast(param)->get(v);
+ out->type = OAKNODE_VALUE_INT;
+ out->num = v;
+ } else if (type == kOfxParamTypeBoolean) {
+ bool v = false;
+ static_cast(param)->get(v);
+ out->type = OAKNODE_VALUE_BOOL;
+ out->num = v ? 1 : 0;
+ } else if (type == kOfxParamTypeDouble) {
+ double v = 0;
+ static_cast(param)->get(v);
+ out->type = OAKNODE_VALUE_FLOAT;
+ out->f[0] = v;
+ } else if (type == kOfxParamTypeDouble2D) {
+ double x = 0, y = 0;
+ static_cast(param)->get(x,
+ y);
+ out->type = OAKNODE_VALUE_VEC2;
+ out->f[0] = x;
+ out->f[1] = y;
+ } else if (type == kOfxParamTypeDouble3D) {
+ double x = 0, y = 0, z = 0;
+ static_cast(param)->get(x,
+ y,
+ z);
+ out->type = OAKNODE_VALUE_VEC3;
+ out->f[0] = x;
+ out->f[1] = y;
+ out->f[2] = z;
+ } else if (type == kOfxParamTypeRGBA) {
+ double r = 0, g = 0, b = 0, a = 0;
+ static_cast(param)->get(r, g,
+ b, a);
+ out->type = OAKNODE_VALUE_COLOR;
+ out->f[0] = r;
+ out->f[1] = g;
+ out->f[2] = b;
+ out->f[3] = a;
+ } else {
+ return OAKPLUGIN_E_INVALID;
+ }
+ return OAKPLUGIN_OK;
+ } catch (...) {
+ return OAKPLUGIN_E_FAILED;
+ }
+}
+
+int oakplugin_instance_set_param_string(OakPluginInstance instance,
+ const char *param_id,
+ const char *value)
+{
+ InstanceBox *box = impl(instance);
+ if (!box || !param_id) {
+ return OAKPLUGIN_E_INVALID;
+ }
+
+ try {
+ const std::map ¶ms_map =
+ box->instance->getParams();
+ auto it = params_map.find(param_id);
+ if (it == params_map.end() || !it->second) {
+ return OAKPLUGIN_E_NOT_FOUND;
+ }
+ OFX::Host::Param::Instance *param = it->second;
+ return static_cast(param)
+ ->set(value ? value : "") == kOfxStatOK
+ ? OAKPLUGIN_OK
+ : OAKPLUGIN_E_FAILED;
+ } catch (...) {
+ return OAKPLUGIN_E_FAILED;
+ }
+}
+
+int oakplugin_instance_get_param_string(OakPluginInstance instance,
+ const char *param_id, char *buf,
+ int buf_size)
+{
+ InstanceBox *box = impl(instance);
+ if (!box || !param_id) {
+ return OAKPLUGIN_E_INVALID;
+ }
+
+ try {
+ const std::map ¶ms_map =
+ box->instance->getParams();
+ auto it = params_map.find(param_id);
+ if (it == params_map.end() || !it->second) {
+ return OAKPLUGIN_E_NOT_FOUND;
+ }
+ OFX::Host::Param::Instance *param = it->second;
+ std::string value;
+ static_cast(param)->get(value);
+ int needed = int(value.size()) + 1;
+ if (buf && buf_size >= needed) {
+ memcpy(buf, value.c_str(), needed);
+ }
+ return needed;
+ } catch (...) {
+ return OAKPLUGIN_E_FAILED;
+ }
+}
+
+int oakplugin_instance_render(OakPluginInstance instance,
+ OakRenderTexture dst, OakRenderTexture src,
+ double time_seconds)
+{
+ InstanceBox *box = impl(instance);
+ if (!box) {
+ return OAKPLUGIN_E_INVALID;
+ }
+
+ try {
+ const char *clip_names[] = { "Output", "Source" };
+ for (const char *clip_name : clip_names) {
+ auto *clip =
+ dynamic_cast(
+ box->instance->getClip(clip_name));
+ if (!clip) {
+ continue;
+ }
+ if (clip->isOutput()) {
+ clip->setOutputTexture(dst, time_seconds);
+ } else if (src.ctx) {
+ clip->setInputTexture(src, time_seconds, false);
+ }
+ }
+
+ OfxRectI roi = { 0, 0, 0, 0 };
+ OfxPointD scale = { 1.0, 1.0 };
+ OfxStatus status = box->instance->renderAction(
+ time_seconds, kOfxImageFieldNone, roi, scale, false, false,
+ false);
+ return status == kOfxStatOK ? OAKPLUGIN_OK : OAKPLUGIN_E_FAILED;
+ } catch (...) {
+ return OAKPLUGIN_E_FAILED;
+ }
+}
+
+int oakplugin_instance_set_progress_cb(OakPluginInstance instance,
+ oakplugin_progress_fn fn,
+ void *userdata)
+{
+ InstanceBox *box = impl(instance);
+ if (!box) {
+ return OAKPLUGIN_E_INVALID;
+ }
+ box->progress_fn = fn;
+ box->progress_userdata = userdata;
+ return OAKPLUGIN_OK;
+}
+
+int oakplugin_instance_cancel(OakPluginInstance instance)
+{
+ InstanceBox *box = impl(instance);
+ if (!box) {
+ return OAKPLUGIN_E_INVALID;
+ }
+ if (box->progress_fn) {
+ box->progress_fn(1.0, box->progress_userdata);
+ }
+ return OAKPLUGIN_OK;
+}
+
+int oakplugin_debug_alive_count(void)
+{
+ return g_alive.load();
+}
diff --git a/src/plugin/src/CMakeLists.txt b/src/plugin/src/CMakeLists.txt
new file mode 100644
index 000000000..ae24d6538
--- /dev/null
+++ b/src/plugin/src/CMakeLists.txt
@@ -0,0 +1,51 @@
+# Olive - Non-Linear Video Editor
+# Copyright (C) 2022 Olive Team
+# Modifications Copyright (C) 2025 mikesolar
+#
+# 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 .
+
+file(GLOB OAKPLUGIN_SOURCES CONFIGURE_DEPENDS *.cpp)
+
+add_library(oakplugin SHARED ${OAKPLUGIN_SOURCES})
+
+target_include_directories(oakplugin PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${OAK_REPO_ROOT}/include
+ ${OAK_REPO_ROOT}/src/common/src
+ ${OAK_REPO_ROOT}/src/undo/src
+ ${OAK_REPO_ROOT}/core/include
+ ${OAK_REPO_ROOT}/third_party/openfx/include
+ ${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
+ ${OAK_REPO_ROOT}/ffmpeg_bridge/include
+)
+
+find_library(OAKPLUGIN_OFX_HOST_ARCHIVE NAMES OfxHost
+ PATHS ${OAK_REPO_ROOT}/build/third_party/openfx/HostSupport
+ NO_DEFAULT_PATH)
+if(NOT OAKPLUGIN_OFX_HOST_ARCHIVE)
+ message(FATAL_ERROR
+ "libOfxHost.a not found; run the full-tree build once or set "
+ "OAKPLUGIN_OFX_HOST_ARCHIVE")
+endif()
+target_link_options(oakplugin PUBLIC
+ "-Wl,-force_load,${OAKPLUGIN_OFX_HOST_ARCHIVE}")
+
+target_link_libraries(oakplugin PUBLIC
+ oaknode
+ oakrender
+ oakcommon
+ oakundo
+ olivecore
+ ffmpeg_bridge
+)
diff --git a/src/plugin/src/image.cpp b/src/plugin/src/image.cpp
new file mode 100644
index 000000000..8d54c9352
--- /dev/null
+++ b/src/plugin/src/image.cpp
@@ -0,0 +1,235 @@
+/*
+ * Oak Video Editor - Non-Linear Video Editor
+ * Copyright (C) 2026 Olive CE Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#include "image.h"
+
+#include "ofxImageEffect.h"
+
+#include
+
+namespace olive
+{
+namespace plugin
+{
+
+static const char *pixel_depth_to_ofx(core::PixelFormat format)
+{
+ switch (format) {
+ case core::PixelFormat::u8:
+ return kOfxBitDepthByte;
+ case core::PixelFormat::u16:
+ return kOfxBitDepthShort;
+ case core::PixelFormat::f16:
+ return kOfxBitDepthHalf;
+ default:
+ break;
+ case core::PixelFormat::f32:
+ return kOfxBitDepthFloat;
+ case core::PixelFormat::u10:
+ case core::PixelFormat::invalid:
+ case core::PixelFormat::count:
+ break;
+ }
+
+ return kOfxBitDepthNone;
+}
+
+static const char *components_to_ofx(int channel_count)
+{
+ switch (channel_count) {
+ case 1:
+ return kOfxImageComponentAlpha;
+ case 3:
+ return kOfxImageComponentRGB;
+ case 4:
+ return kOfxImageComponentRGBA;
+ default:
+ break;
+ }
+
+ return kOfxImageComponentNone;
+}
+
+Image::Image(OFX::Host::ImageEffect::ClipInstance &clip_instance)
+ : OFX::Host::ImageEffect::Image(clip_instance)
+ , width_(0)
+ , height_(0)
+ , format_(core::PixelFormat::invalid)
+ , premultiplied_alpha_(false)
+ , channel_count_(0)
+ , row_bytes_(0)
+ , bounds_{ 0, 0, 0, 0 }
+ , rod_{ 0, 0, 0, 0 }
+{
+}
+
+Image::Image(OFX::Host::ImageEffect::ClipInstance &clip_instance,
+ const VideoParams ¶ms, const OfxRectI &bounds,
+ const OfxRectI &rod, bool clear)
+ : OFX::Host::ImageEffect::Image(clip_instance)
+ , width_(0)
+ , height_(0)
+ , format_(core::PixelFormat::invalid)
+ , premultiplied_alpha_(false)
+ , channel_count_(0)
+ , row_bytes_(0)
+ , bounds_{ 0, 0, 0, 0 }
+ , rod_{ 0, 0, 0, 0 }
+{
+ allocate_from_params(params, bounds, rod, clear);
+}
+
+Image::~Image()
+{
+}
+
+void Image::allocate_from_params(const VideoParams ¶ms,
+ const OfxRectI &bounds, const OfxRectI &rod,
+ bool clear)
+{
+ allocate(bounds.x2 - bounds.x1, bounds.y2 - bounds.y1, params.format(),
+ params.channel_count(), params.premultiplied_alpha(), bounds, rod,
+ clear);
+}
+
+void Image::ensure_allocated_from_params(const VideoParams ¶ms,
+ const OfxRectI &bounds,
+ const OfxRectI &rod, bool clear)
+{
+ bool same = (width_ == bounds.x2 - bounds.x1) &&
+ (height_ == bounds.y2 - bounds.y1) &&
+ (format_ == params.format()) &&
+ (channel_count_ == params.channel_count()) &&
+ (premultiplied_alpha_ == params.premultiplied_alpha()) &&
+ (bounds_.x1 == bounds.x1) && (bounds_.y1 == bounds.y1) &&
+ (bounds_.x2 == bounds.x2) && (bounds_.y2 == bounds.y2) &&
+ (rod_.x1 == rod.x1) && (rod_.y1 == rod.y1) &&
+ (rod_.x2 == rod.x2) && (rod_.y2 == rod.y2);
+
+ if (!same) {
+ allocate_from_params(params, bounds, rod, clear);
+ } else if (clear && !image_.empty()) {
+ std::fill(image_.begin(), image_.end(), 0);
+ }
+}
+
+void Image::allocate(int width, int height, core::PixelFormat format,
+ int channel_count, bool premultiplied_alpha,
+ const OfxRectI &bounds, const OfxRectI &rod, bool clear)
+{
+ width_ = width;
+ height_ = height;
+ format_ = format;
+ channel_count_ = channel_count;
+ premultiplied_alpha_ = premultiplied_alpha;
+ bounds_ = bounds;
+ rod_ = rod;
+
+ int bytes_per_component = format_.byte_count();
+ row_bytes_ = width_ * channel_count_ * bytes_per_component;
+ int buffer_size = row_bytes_ * height_;
+ if (buffer_size < 0) {
+ buffer_size = 0;
+ }
+
+ image_.resize(static_cast(buffer_size));
+ if (clear && !image_.empty()) {
+ std::fill(image_.begin(), image_.end(), 0);
+ }
+
+ setPointerProperty(kOfxImagePropData, image_.data());
+ setIntProperty(kOfxImagePropRowBytes, row_bytes_);
+ setIntProperty(kOfxImagePropBounds, bounds.x1, 0);
+ setIntProperty(kOfxImagePropBounds, bounds.y1, 1);
+ setIntProperty(kOfxImagePropBounds, bounds.x2, 2);
+ setIntProperty(kOfxImagePropBounds, bounds.y2, 3);
+ setIntProperty(kOfxImagePropRegionOfDefinition, rod.x1, 0);
+ setIntProperty(kOfxImagePropRegionOfDefinition, rod.y1, 1);
+ setIntProperty(kOfxImagePropRegionOfDefinition, rod.x2, 2);
+ setIntProperty(kOfxImagePropRegionOfDefinition, rod.y2, 3);
+ setStringProperty(kOfxImageEffectPropComponents,
+ components_to_ofx(channel_count_));
+ setStringProperty(kOfxImageEffectPropPixelDepth, pixel_depth_to_ofx(format_));
+ setStringProperty(kOfxImageEffectPropPreMultiplication,
+ premultiplied_alpha_ ? kOfxImagePreMultiplied :
+ kOfxImageUnPreMultiplied);
+}
+
+core::PixelFormat Image::pixel_format()
+{
+ if (format_ != core::PixelFormat::invalid) {
+ return format_;
+ }
+
+ std::string type = getStringProperty(kOfxImageEffectPropPixelDepth);
+ if (type == kOfxBitDepthByte) {
+ format_ = core::PixelFormat::u8;
+ } else if (type == kOfxBitDepthShort) {
+ format_ = core::PixelFormat::u16;
+ } else if (type == kOfxBitDepthHalf) {
+ format_ = core::PixelFormat::f16;
+ } else if (type == kOfxBitDepthFloat) {
+ format_ = core::PixelFormat::f32;
+ } else {
+ format_ = core::PixelFormat::invalid;
+ }
+ return format_;
+}
+
+bool Image::premultiplied_alpha()
+{
+ std::string premultiplied =
+ getStringProperty(kOfxImageEffectPropPreMultiplication);
+ premultiplied_alpha_ = (premultiplied == kOfxImagePreMultiplied);
+ return premultiplied_alpha_;
+}
+
+int Image::width()
+{
+ int bounds[4] = { 0 };
+ getIntPropertyN(kOfxImagePropBounds, bounds, 4);
+ width_ = bounds[2] - bounds[0];
+ return width_;
+}
+
+int Image::height()
+{
+ int bounds[4] = { 0 };
+ getIntPropertyN(kOfxImagePropBounds, bounds, 4);
+ height_ = bounds[3] - bounds[1];
+ return height_;
+}
+
+int Image::channel_count()
+{
+ std::string type = getStringProperty(kOfxImageEffectPropComponents);
+ if (type == kOfxImageComponentAlpha) {
+ channel_count_ = 1;
+ } else if (type == kOfxImageComponentRGBA) {
+ channel_count_ = 4;
+ } else if (type == kOfxImageComponentRGB) {
+ channel_count_ = 3;
+ } else {
+ channel_count_ = 0;
+ }
+ return channel_count_;
+}
+
+} // namespace plugin
+} // namespace olive
diff --git a/src/plugin/src/image.h b/src/plugin/src/image.h
new file mode 100644
index 000000000..19031adc0
--- /dev/null
+++ b/src/plugin/src/image.h
@@ -0,0 +1,81 @@
+/*
+ * Oak Video Editor - Non-Linear Video Editor
+ * Copyright (C) 2026 Olive CE Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#ifndef OAK_OLIVE_EDITOR_PLUGIN_IMAGE_H
+#define OAK_OLIVE_EDITOR_PLUGIN_IMAGE_H
+
+#include "ofxCore.h"
+#include "ofxImageEffect.h"
+#include "ofxhClip.h"
+#include "olive/core/render/pixelformat.h"
+#include "loopmode.h"
+#include "videoparams.h"
+#include
+#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 (uint8_t *)getPointerProperty(kOfxImagePropData);
+ }
+ int width();
+ int height();
+ core::PixelFormat pixel_format();
+ bool premultiplied_alpha();
+ int channel_count();
+
+ void allocate_from_params(const VideoParams ¶ms, const OfxRectI &bounds,
+ const OfxRectI &rod, bool clear = true);
+ void ensure_allocated_from_params(const VideoParams ¶ms,
+ const OfxRectI &bounds, const OfxRectI &rod,
+ bool clear = false);
+ void allocate(int width, int height, core::PixelFormat format,
+ int channel_count, bool premultiplied_alpha,
+ const OfxRectI &bounds, const OfxRectI &rod,
+ bool clear = true);
+ int row_bytes() const
+ {
+ return row_bytes_;
+ }
+
+protected:
+ std::vector image_;
+ int width_;
+ int height_;
+ core::PixelFormat format_;
+ bool premultiplied_alpha_;
+ int channel_count_;
+ int row_bytes_;
+ OfxRectI bounds_;
+ OfxRectI rod_;
+};
+}
+}
+
+#endif //OAK_OLIVE_EDITOR_PLUGIN_IMAGE_H
diff --git a/src/plugin/src/oliveclip.cpp b/src/plugin/src/oliveclip.cpp
new file mode 100644
index 000000000..99727bc7e
--- /dev/null
+++ b/src/plugin/src/oliveclip.cpp
@@ -0,0 +1,1076 @@
+/*
+ * Oak Video Editor - Non-Linear Video Editor
+ * Copyright (C) 2025 Olive CE Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#include "oliveclip.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include "avframeptr.h"
+#include "common/ffmpegutils.h"
+#include "ofxCore.h"
+#include "ofxhClip.h"
+
+namespace
+{
+
+// The bridge header only defines the little-endian pixel formats. FFmpeg
+// numbers each big-endian variant immediately before its little-endian
+// counterpart (BE == LE - 1), so derive the BE constants used below.
+constexpr int fb_pix_fmt_gray_f32_be = fb_pix_fmt_gray_f32_le - 1;
+constexpr int fb_pix_fmt_rgb_f32_be = fb_pix_fmt_rgb_f32_le - 1;
+constexpr int fb_pix_fmt_rgba_f32_be = fb_pix_fmt_rgba_f32_le - 1;
+
+const std::string k_bit_depth_none_str(kOfxBitDepthNone);
+const std::string k_bit_depth_byte_str(kOfxBitDepthByte);
+const std::string k_bit_depth_short_str(kOfxBitDepthShort);
+const std::string k_bit_depth_half_str(kOfxBitDepthHalf);
+const std::string k_bit_depth_float_str(kOfxBitDepthFloat);
+const std::string k_image_component_none_str(kOfxImageComponentNone);
+const std::string k_image_component_alpha_str(kOfxImageComponentAlpha);
+const std::string k_image_component_rgb_str(kOfxImageComponentRGB);
+const std::string k_image_component_rgba_str(kOfxImageComponentRGBA);
+const std::string k_image_premult_str(kOfxImagePreMultiplied);
+const std::string k_image_un_premult_str(kOfxImageUnPreMultiplied);
+const std::string k_image_field_none_str(kOfxImageFieldNone);
+const std::string k_image_field_upper_str(kOfxImageFieldUpper);
+const std::string k_image_field_lower_str(kOfxImageFieldLower);
+
+int params_width(OakVideoParams params)
+{
+ int v = 0;
+ oakcommon_videoparams_get_width(params, &v);
+ return v;
+}
+
+int params_height(OakVideoParams params)
+{
+ int v = 0;
+ oakcommon_videoparams_get_height(params, &v);
+ return v;
+}
+
+int params_format(OakVideoParams params)
+{
+ int v = OAKCOMMON_PIXEL_FORMAT_INVALID;
+ oakcommon_videoparams_get_format(params, &v);
+ return v;
+}
+
+int params_channels(OakVideoParams params)
+{
+ int v = 0;
+ oakcommon_videoparams_get_channel_count(params, &v);
+ return v;
+}
+
+double params_par(OakVideoParams params)
+{
+ int num = 1, den = 1;
+ oakcommon_videoparams_get_pixel_aspect_ratio(params, &num, &den);
+ if (den == 0) {
+ return 1.0;
+ }
+ double par = double(num) / den;
+ return par == 0.0 ? 1.0 : par;
+}
+
+double params_frame_rate(OakVideoParams params)
+{
+ int num = 0, den = 1;
+ oakcommon_videoparams_get_frame_rate(params, &num, &den);
+ return den ? double(num) / den : 0.0;
+}
+
+int64_t params_duration(OakVideoParams params)
+{
+ int64_t v = 0;
+ oakcommon_videoparams_get_duration(params, &v);
+ return v;
+}
+
+int64_t params_start_time(OakVideoParams params)
+{
+ int64_t v = 0;
+ oakcommon_videoparams_get_start_time(params, &v);
+ return v;
+}
+
+int params_interlacing(OakVideoParams params)
+{
+ int v = 0;
+ oakcommon_videoparams_get_interlacing(params, &v);
+ return v;
+}
+
+int bytes_per_component_of(int format)
+{
+ switch (format) {
+ case OAKCOMMON_PIXEL_FORMAT_U8:
+ return 1;
+ case OAKCOMMON_PIXEL_FORMAT_U16:
+ return 2;
+ case OAKCOMMON_PIXEL_FORMAT_F16:
+ return 2;
+ case OAKCOMMON_PIXEL_FORMAT_F32:
+ return 4;
+ default:
+ return 0;
+ }
+}
+
+int bytes_to_pixels(int byte_linesize, OakVideoParams params)
+{
+ const int bytes_per_pixel =
+ params_channels(params) * bytes_per_component_of(params_format(params));
+ if (bytes_per_pixel <= 0) {
+ return 0;
+ }
+ return byte_linesize / bytes_per_pixel;
+}
+
+int packed_float_channels(int fmt)
+{
+ switch (fmt) {
+ case fb_pix_fmt_gray_f32_le:
+ case fb_pix_fmt_gray_f32_be:
+ return 1;
+ case fb_pix_fmt_rgb_f32_le:
+ case fb_pix_fmt_rgb_f32_be:
+ return 3;
+ case fb_pix_fmt_rgba_f32_le:
+ case fb_pix_fmt_rgba_f32_be:
+ return 4;
+ default:
+ return 0;
+ }
+}
+
+bool packed_dst_info(int fmt, int *channels, int *bytes_per_component)
+{
+ switch (fmt) {
+ case fb_pix_fmt_gra_y8:
+ *channels = 1;
+ *bytes_per_component = 1;
+ return true;
+ case fb_pix_fmt_rg_b24:
+ *channels = 3;
+ *bytes_per_component = 1;
+ return true;
+ case fb_pix_fmt_rgba:
+ *channels = 4;
+ *bytes_per_component = 1;
+ return true;
+ case fb_pix_fmt_gra_y16_le:
+ *channels = 1;
+ *bytes_per_component = 2;
+ return true;
+ case fb_pix_fmt_rg_b48_le:
+ *channels = 3;
+ *bytes_per_component = 2;
+ return true;
+ case fb_pix_fmt_rgb_a64_le:
+ *channels = 4;
+ *bytes_per_component = 2;
+ return true;
+ default:
+ return false;
+ }
+}
+
+olive::AVFramePtr readback_texture_to_frame(OakRenderTexture texture,
+ OakVideoParams params)
+{
+ if (!texture.ctx || oakrender_display_texture_is_dummy(texture)) {
+ return nullptr;
+ }
+
+ int pix_fmt = OAKCOMMON_E_INVALID;
+ oakcommon_ffmpegutils_get_ffmpeg_pixel_format(
+ params_format(params), params_channels(params), &pix_fmt);
+ if (pix_fmt == fb_pix_fmt_none) {
+ return nullptr;
+ }
+
+ if (!fb_pix_fmt_is_planar(pix_fmt)) {
+ olive::AVFramePtr frame = olive::create_av_frame_ptr();
+ frame->set_format(pix_fmt);
+ frame->set_width(params_width(params));
+ frame->set_height(params_height(params));
+ if (frame->get_buffer(0) < 0) {
+ return nullptr;
+ }
+ const int linesize_pixels = bytes_to_pixels(frame->linesize(0), params);
+ oakrender_display_texture_download(texture, frame->data(0),
+ linesize_pixels *
+ bytes_per_component_of(
+ params_format(params)));
+ return frame;
+ }
+
+ // Planar formats read back through an RGBA intermediate and scale
+ olive::AVFramePtr rgba_frame = olive::create_av_frame_ptr();
+ rgba_frame->set_format(fb_pix_fmt_rgba);
+ rgba_frame->set_width(params_width(params));
+ rgba_frame->set_height(params_height(params));
+ if (rgba_frame->get_buffer(0) < 0) {
+ return nullptr;
+ }
+
+ const int rgba_linesize = rgba_frame->linesize(0);
+ oakrender_display_texture_download(texture, rgba_frame->data(0),
+ rgba_linesize);
+
+ olive::AVFramePtr dst = olive::create_av_frame_ptr();
+ dst->set_format(pix_fmt);
+ dst->set_width(params_width(params));
+ dst->set_height(params_height(params));
+ if (dst->get_buffer(0) < 0) {
+ return rgba_frame;
+ }
+
+ FBScaler *scaler = fb_scaler_create(
+ rgba_frame->width(), rgba_frame->height(), rgba_frame->format(),
+ dst->width(), dst->height(), pix_fmt, FB_SCALER_POINT);
+ if (!scaler) {
+ return rgba_frame;
+ }
+
+ uint8_t *src_data[4];
+ int src_linesize[4];
+ uint8_t *dst_data[4];
+ int dst_linesize[4];
+ for (int i = 0; i < 4; ++i) {
+ src_data[i] = rgba_frame->data(i);
+ src_linesize[i] = rgba_frame->linesize(i);
+ dst_data[i] = dst->data(i);
+ dst_linesize[i] = dst->linesize(i);
+ }
+ fb_scaler_scale_slices(scaler, src_data, src_linesize,
+ rgba_frame->height(), dst_data, dst_linesize);
+ fb_scaler_free(&scaler);
+ return dst;
+}
+
+olive::AVFramePtr convert_packed_float_frame(olive::AVFramePtr src,
+ int dst_fmt)
+{
+ if (!src || !src->data(0)) {
+ return nullptr;
+ }
+ const int src_channels = packed_float_channels(src->format());
+ if (src_channels == 0) {
+ return nullptr;
+ }
+
+ int dst_channels = 0;
+ int bytes_per_component = 0;
+ if (!packed_dst_info(dst_fmt, &dst_channels, &bytes_per_component)) {
+ return nullptr;
+ }
+
+ olive::AVFramePtr dst = olive::create_av_frame_ptr();
+ dst->set_format(dst_fmt);
+ dst->set_width(src->width());
+ dst->set_height(src->height());
+ if (dst->get_buffer(0) < 0) {
+ return nullptr;
+ }
+
+ auto clamp01 = [](float v) -> float { return std::clamp(v, 0.0f, 1.0f); };
+
+ for (int y = 0; y < src->height(); ++y) {
+ const float *src_row = reinterpret_cast(
+ src->data(0) + y * src->linesize(0));
+ uint8_t *dst_row = dst->data(0) + y * dst->linesize(0);
+
+ if (bytes_per_component == 2) {
+ auto *dst_row_u16 = reinterpret_cast(dst_row);
+ for (int x = 0; x < src->width(); ++x) {
+ const float *pix = src_row + x * src_channels;
+ float r = pix[0];
+ float g = (src_channels > 1) ? pix[1] : r;
+ float b = (src_channels > 2) ? pix[2] : r;
+ float a = (src_channels > 3) ? pix[3] : 1.0f;
+ if (dst_channels == 1) {
+ float luma = 0.2126f * r + 0.7152f * g + 0.0722f * b;
+ dst_row_u16[x] = static_cast(
+ std::lround(clamp01(luma) * 65535.0f));
+ continue;
+ }
+ dst_row_u16[x * dst_channels + 0] = static_cast(
+ std::lround(clamp01(r) * 65535.0f));
+ dst_row_u16[x * dst_channels + 1] = static_cast(
+ std::lround(clamp01(g) * 65535.0f));
+ dst_row_u16[x * dst_channels + 2] = static_cast(
+ std::lround(clamp01(b) * 65535.0f));
+ if (dst_channels == 4) {
+ dst_row_u16[x * dst_channels + 3] = static_cast(
+ std::lround(clamp01(a) * 65535.0f));
+ }
+ }
+ } else {
+ for (int x = 0; x < src->width(); ++x) {
+ const float *pix = src_row + x * src_channels;
+ float r = pix[0];
+ float g = (src_channels > 1) ? pix[1] : r;
+ float b = (src_channels > 2) ? pix[2] : r;
+ float a = (src_channels > 3) ? pix[3] : 1.0f;
+ if (dst_channels == 1) {
+ float luma = 0.2126f * r + 0.7152f * g + 0.0722f * b;
+ dst_row[x] = static_cast(
+ std::lround(clamp01(luma) * 255.0f));
+ continue;
+ }
+ dst_row[x * dst_channels + 0] = static_cast(
+ std::lround(clamp01(r) * 255.0f));
+ dst_row[x * dst_channels + 1] = static_cast(
+ std::lround(clamp01(g) * 255.0f));
+ dst_row[x * dst_channels + 2] = static_cast(
+ std::lround(clamp01(b) * 255.0f));
+ if (dst_channels == 4) {
+ dst_row[x * dst_channels + 3] = static_cast(
+ std::lround(clamp01(a) * 255.0f));
+ }
+ }
+ }
+ }
+
+ return dst;
+}
+
+} // namespace
+
+namespace olive
+{
+namespace plugin
+{
+
+const std::string &OliveClipInstance::getUnmappedBitDepth() const
+{
+ // Return the plugin's preferred pixel depth from base class
+ // This is set during getClipPreferences action via setPixelDepth()
+ const std::string &depth = getPixelDepth();
+ if (!depth.empty() && depth != k_bit_depth_none_str) {
+ return depth;
+ }
+ // Fallback to params_ if base class value is not set
+ switch (params_format(params_)) {
+ case OAKCOMMON_PIXEL_FORMAT_U8:
+ return k_bit_depth_byte_str;
+ case OAKCOMMON_PIXEL_FORMAT_U16:
+ return k_bit_depth_short_str;
+ case OAKCOMMON_PIXEL_FORMAT_F16:
+ return k_bit_depth_half_str;
+ case OAKCOMMON_PIXEL_FORMAT_F32:
+ return k_bit_depth_float_str;
+ default:
+ return k_bit_depth_none_str;
+ }
+}
+
+const std::string &OliveClipInstance::getUnmappedComponents() const
+{
+ // Return the plugin's preferred components from base class
+ // This is set during getClipPreferences action via setComponents()
+ const std::string &comp = getComponents();
+ if (!comp.empty() && comp != k_image_component_none_str) {
+ return comp;
+ }
+ // Fallback to params_ if base class value is not set
+ switch (params_channels(params_)) {
+ case 1:
+ return k_image_component_alpha_str;
+ case 3:
+ return k_image_component_rgb_str;
+ case 4:
+ return k_image_component_rgba_str;
+ default:
+ return k_image_component_none_str;
+ }
+}
+
+const std::string &OliveClipInstance::getPremult() const
+{
+ int premult = 0;
+ oakcommon_videoparams_get_premultiplied_alpha(params_, &premult);
+ return premult ? k_image_premult_str : k_image_un_premult_str;
+}
+
+double OliveClipInstance::getAspectRatio() const
+{
+ return params_par(params_);
+}
+
+double OliveClipInstance::getFrameRate() const
+{
+ return params_frame_rate(params_);
+}
+
+void OliveClipInstance::getFrameRange(double &start_frame,
+ double &end_frame) const
+{
+ start_frame = params_frame_rate(params_) * double(params_start_time(params_));
+ end_frame = start_frame +
+ params_frame_rate(params_) * double(params_duration(params_));
+}
+
+const std::string &OliveClipInstance::getFieldOrder() const
+{
+ switch (params_interlacing(params_)) {
+ case OAKCOMMON_VIDEO_INTERLACED_TOP_FIRST:
+ return k_image_field_upper_str;
+ case OAKCOMMON_VIDEO_INTERLACED_BOTTOM_FIRST:
+ return k_image_field_lower_str;
+ default:
+ return k_image_field_none_str;
+ }
+}
+
+bool OliveClipInstance::getConnected() const
+{
+ if (name_ == kOfxImageEffectOutputClipName) {
+ if (!output_textures_.empty()) {
+ return true;
+ }
+ return !images_.empty();
+ }
+ if (!input_textures_.empty()) {
+ return true;
+ }
+ return !images_.empty();
+}
+
+double OliveClipInstance::getUnmappedFrameRate() const
+{
+ return getFrameRate();
+}
+
+void OliveClipInstance::getUnmappedFrameRange(double &start_frame,
+ double &end_frame) const
+{
+ getFrameRange(start_frame, end_frame);
+}
+
+bool OliveClipInstance::getContinuousSamples() const
+{
+ return false;
+}
+
+OFX::Host::ImageEffect::Image *
+OliveClipInstance::getImage(OfxTime time, const OfxRectD *optional_bounds)
+{
+ 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)) };
+ (void)optional_bounds;
+ // Always return full-frame images to keep input data consistent.
+ OfxRectI bounds = rod;
+
+ const olive::VideoParams *params_native =
+ oakcommon_videoparams_get_native(params_);
+
+ if (name_ == "Output") {
+ Image *image;
+ auto it = images_.find(time);
+ if (it == images_.end()) {
+ // make a new ref counted image
+ image = new Image(*this, *params_native, bounds, rod, true);
+ images_.insert({ time, image });
+ } else {
+ image = it->second;
+ }
+
+ // 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
+ image->addReference();
+
+ image->ensure_allocated_from_params(*params_native, bounds, rod, true);
+
+ // return it
+ return image;
+ } else {
+ auto it = images_.find(time);
+ if (it != images_.end()) {
+ Image *image = it->second;
+ image->ensure_allocated_from_params(*params_native, bounds, rod,
+ false);
+ image->addReference();
+ return image;
+ }
+
+ // Fetch on demand for the input clip.
+ // Use plugin-preferred params to ensure the image format matches
+ // what the plugin expects (may differ from input texture format)
+ OakVideoParams preferred = getPluginPreferredParams();
+ if (params_format(preferred) == OAKCOMMON_PIXEL_FORMAT_INVALID) {
+ if (preferred.ctx) {
+ oakcommon_videoparams_free(&preferred);
+ }
+ preferred = params_;
+ preferred.addref(preferred.ctx);
+ }
+ // Keep dimensions and other settings from params_
+ oakcommon_videoparams_set_width(preferred, params_width(params_));
+ oakcommon_videoparams_set_height(preferred, params_height(params_));
+ {
+ int num = 1, den = 1;
+ oakcommon_videoparams_get_pixel_aspect_ratio(params_, &num, &den);
+ oakcommon_videoparams_set_pixel_aspect_ratio(preferred, num, den);
+ }
+
+ // Guard against zero-size or invalid-format images that would
+ // cause EXC_BAD_ACCESS when the plugin accesses pixel data.
+ if (params_width(preferred) <= 0 || params_height(preferred) <= 0 ||
+ params_format(preferred) == OAKCOMMON_PIXEL_FORMAT_INVALID ||
+ params_channels(preferred) <= 0) {
+ oakcommon_videoparams_free(&preferred);
+ return nullptr;
+ }
+
+ const olive::VideoParams *preferred_native =
+ oakcommon_videoparams_get_native(preferred);
+
+ // Cache the on-demand image like the output path does, so repeated
+ // fetches at the same time reuse it and getConnected() reflects it.
+ // The extra reference keeps the cached image alive when the plugin
+ // releases its own.
+ prune_images_cache();
+ Image *image = new Image(*this, *preferred_native, bounds, rod, true);
+ images_.insert({ time, image });
+ image->addReference();
+ oakcommon_videoparams_free(&preferred);
+ return image;
+ }
+}
+
+OFX::Host::ImageEffect::Image *
+OliveClipInstance::getOutputImage(OfxTime time)
+{
+ auto it = images_.find(time);
+ if (it != images_.end()) {
+ return it->second;
+ }
+
+ OfxRectD rod_d = getRegionOfDefinition(time);
+ OfxRectI rod = { static_cast(std::floor(rod_d.x1)),
+ static_cast(std::floor(rod_d.y1)),
+ static_cast(std::ceil(rod_d.x2)),
+ static_cast(std::ceil(rod_d.y2)) };
+ OfxRectI bounds = rod;
+
+ // Use plugin-preferred params instead of params_ to ensure the image
+ // is created with the format the plugin expects
+ OakVideoParams preferred = getPluginPreferredParams();
+ if (params_format(preferred) == OAKCOMMON_PIXEL_FORMAT_INVALID) {
+ if (preferred.ctx) {
+ oakcommon_videoparams_free(&preferred);
+ }
+ preferred = params_;
+ preferred.addref(preferred.ctx);
+ }
+ // Keep the dimensions and other settings from params_
+ oakcommon_videoparams_set_width(preferred, params_width(params_));
+ oakcommon_videoparams_set_height(preferred, params_height(params_));
+ {
+ int num = 1, den = 1;
+ oakcommon_videoparams_get_pixel_aspect_ratio(params_, &num, &den);
+ oakcommon_videoparams_set_pixel_aspect_ratio(preferred, num, den);
+ }
+
+ const olive::VideoParams *preferred_native =
+ oakcommon_videoparams_get_native(preferred);
+
+ auto image = new Image(*this, *preferred_native, bounds, rod, true);
+ images_.insert({ time, image });
+ oakcommon_videoparams_free(&preferred);
+ return image;
+}
+
+OakVideoParams OliveClipInstance::getPluginPreferredParams() const
+{
+ OakVideoParams result = {};
+ {
+ const olive::VideoParams *native =
+ oakcommon_videoparams_get_native(params_);
+ result = oakcommon_videoparams_init_from_native(native);
+ }
+
+ // Get format from base class _pixelDepth (set by getClipPreferences)
+ const std::string &depth = getPixelDepth();
+ if (!depth.empty()) {
+ if (depth == kOfxBitDepthByte) {
+ oakcommon_videoparams_set_format(result, OAKCOMMON_PIXEL_FORMAT_U8);
+ } else if (depth == kOfxBitDepthShort) {
+ oakcommon_videoparams_set_format(result,
+ OAKCOMMON_PIXEL_FORMAT_U16);
+ } else if (depth == kOfxBitDepthHalf) {
+ oakcommon_videoparams_set_format(result,
+ OAKCOMMON_PIXEL_FORMAT_F16);
+ } else if (depth == kOfxBitDepthFloat) {
+ oakcommon_videoparams_set_format(result,
+ OAKCOMMON_PIXEL_FORMAT_F32);
+ }
+ }
+
+ // Get channel count from base class _components (set by getClipPreferences)
+ const std::string &comp = getComponents();
+ if (!comp.empty()) {
+ if (comp == kOfxImageComponentRGBA) {
+ oakcommon_videoparams_set_channel_count(result, 4);
+ } else if (comp == kOfxImageComponentRGB) {
+ oakcommon_videoparams_set_channel_count(result, 3);
+ } else if (comp == kOfxImageComponentAlpha) {
+ oakcommon_videoparams_set_channel_count(result, 1);
+ }
+ }
+
+ return result;
+}
+
+OfxRectD OliveClipInstance::getRegionOfDefinition(OfxTime time) const
+{
+ auto it = region_of_definitions_.find(time);
+ if (it != region_of_definitions_.end()) {
+ return it->second;
+ }
+ OfxRectD region_of_definition;
+ region_of_definition.x1 = region_of_definition.y1 = 0;
+ double par = params_par(params_);
+ region_of_definition.x2 = params_width(params_) * par;
+ region_of_definition.y2 = params_height(params_);
+ if (region_of_definition.x2 <= 0 || region_of_definition.y2 <= 0) {
+ // The params provide no usable region; fall back to the default set
+ // via setDefaultRegionOfDefinition().
+ return default_region_of_definition_;
+ }
+ return region_of_definition;
+}
+
+void OliveClipInstance::setRegionOfDefinition(
+ OfxRectD region_of_definition, OfxTime time)
+{
+ region_of_definitions_[time] = region_of_definition;
+}
+
+void OliveClipInstance::setDefaultRegionOfDefinition(
+ OfxRectD region_of_definition)
+{
+ default_region_of_definition_ = region_of_definition;
+}
+
+void OliveClipInstance::prune_images_cache()
+{
+ // Do not prune output clip images; they may have external references
+ // added by getImage()/addReference() and are typically single-frame.
+ if (name_ == kOfxImageEffectOutputClipName) {
+ return;
+ }
+ while (images_.size() > k_max_input_image_cache) {
+ auto it = images_.begin();
+ delete it->second;
+ images_.erase(it);
+ }
+}
+
+void OliveClipInstance::setParams(const olive::VideoParams ¶ms)
+{
+ if (params_.ctx) {
+ oakcommon_videoparams_free(¶ms_);
+ }
+ params_ = oakcommon_videoparams_init_from_native(¶ms);
+ // Sync with OpenFX Host Support's _pixelDepth and _components
+ setPixelDepth(getUnmappedBitDepth());
+ setComponents(getUnmappedComponents());
+}
+
+void OliveClipInstance::setInputTexture(OakRenderTexture texture,
+ OfxTime time, bool readback_cpu)
+{
+ if (!texture.ctx) {
+ return;
+ }
+
+ // Preserve time-related properties from the host/project.
+ // The frame rate of an OFX clip should reflect the project's frame rate,
+ // not the individual input texture's frame rate.
+ int rate_num = 0, rate_den = 1;
+ oakcommon_videoparams_get_frame_rate(params_, &rate_num, &rate_den);
+ int tb_num = 0, tb_den = 1;
+ oakcommon_videoparams_get_time_base(params_, &tb_num, &tb_den);
+
+ // Adopt the incoming texture's params
+ oakrender_video_params texture_params = {};
+ oakrender_display_texture_get_params(texture, &texture_params);
+ oakcommon_videoparams_set_width(params_, texture_params.width);
+ oakcommon_videoparams_set_height(params_, texture_params.height);
+ oakcommon_videoparams_set_format(params_, texture_params.format);
+ oakcommon_videoparams_set_interlacing(params_,
+ texture_params.interlacing);
+ oakcommon_videoparams_set_color_range(params_,
+ texture_params.color_range);
+ oakcommon_videoparams_set_divider(params_, texture_params.divider);
+ oakcommon_videoparams_set_premultiplied_alpha(
+ params_, texture_params.premultiplied_alpha);
+
+ oakcommon_videoparams_set_frame_rate(params_, rate_num, rate_den);
+ oakcommon_videoparams_set_time_base(params_, tb_num, tb_den);
+
+ // Note: We do NOT call setPixelDepth/setComponents here because
+ // those should be set by getClipPreferences to reflect the PLUGIN's
+ // preferred format, not the input texture's format.
+ texture.addref(texture.ctx);
+ auto it = input_textures_.find(time);
+ if (it != input_textures_.end()) {
+ oakrender_display_texture_free(&it->second);
+ }
+ input_textures_[time] = texture;
+
+ // In OpenGL render path, skip CPU readback entirely.
+ // The plugin will fetch input via loadTexture() using GPU texture IDs.
+ // If the plugin falls back to getImage(), it will be created on-demand
+ // in getImage() with zero-initialized data.
+ if (!readback_cpu) {
+ return;
+ }
+
+ OakCodecFrame frame = {};
+ oakrender_display_texture_get_frame(texture, &frame);
+ if (!frame.ctx) {
+ frame = oakrender_codec_frame_create();
+ olive::AVFramePtr readback = readback_texture_to_frame(texture, params_);
+ if (readback) {
+ // Copy readback pixels into the codec frame
+ if (oakrender_codec_frame_allocate(frame) == OAKRENDER_OK) {
+ uint8_t *dst =
+ static_cast(oakrender_codec_frame_data(frame));
+ const uint8_t *src = readback->data(0);
+ int src_linesize = readback->linesize(0);
+ int dst_linesize = oakrender_codec_frame_linesize_bytes(frame);
+ int copy_bytes = std::min(src_linesize, dst_linesize);
+ for (int y = 0; y < readback->height(); y++) {
+ memcpy(dst + size_t(y) * dst_linesize,
+ src + size_t(y) * src_linesize,
+ size_t(copy_bytes));
+ }
+ }
+ }
+ }
+
+ int expected_fmt = OAKCOMMON_E_INVALID;
+ oakcommon_ffmpegutils_get_ffmpeg_pixel_format(
+ params_format(params_), params_channels(params_), &expected_fmt);
+ if (expected_fmt == fb_pix_fmt_none) {
+ if (frame.ctx) {
+ oakrender_codec_frame_free(&frame);
+ }
+ return;
+ }
+ OfxRectI bounds = { 0, 0, params_width(params_), params_height(params_) };
+ OfxRectD rod_d = getRegionOfDefinition(time);
+ OfxRectI region_of_definition = { 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)) };
+
+ const olive::VideoParams *params_native =
+ oakcommon_videoparams_get_native(params_);
+
+ Image *image;
+ auto img_it = images_.find(time);
+ if (img_it != images_.end()) {
+ image = img_it->second;
+ image->ensure_allocated_from_params(*params_native, bounds,
+ region_of_definition, false);
+ } else {
+ prune_images_cache();
+ image = new Image(*this, *params_native, bounds, region_of_definition,
+ false);
+ image->ensure_allocated_from_params(*params_native, bounds,
+ region_of_definition, false);
+ images_.insert({ time, image });
+ }
+
+ uint8_t *dst = static_cast(image->data());
+ if (!dst) {
+ if (frame.ctx) {
+ oakrender_codec_frame_free(&frame);
+ }
+ return;
+ }
+
+ if (!frame.ctx || !oakrender_codec_frame_data(frame)) {
+ std::memset(dst, 0, image->row_bytes() * image->height());
+ if (frame.ctx) {
+ oakrender_codec_frame_free(&frame);
+ }
+ return;
+ }
+
+ AVFramePtr src_frame;
+ bool own_src = false;
+ if (oakrender_codec_frame_fb_format(frame) >= 0) {
+ // Wraps an AVFramePtr: access planes directly
+ // (fb format/width/height available through accessors)
+ }
+ int frame_fmt = oakrender_codec_frame_fb_format(frame);
+ int frame_width = oakrender_codec_frame_width(frame);
+ int frame_height = oakrender_codec_frame_height(frame);
+
+ if (frame_fmt != expected_fmt || frame_width != params_width(params_) ||
+ frame_height != params_height(params_)) {
+ if (packed_float_channels(frame_fmt) > 0) {
+ // Packed float -> packed int conversion
+ AVFramePtr converted_input = create_av_frame_ptr();
+ converted_input->set_format(frame_fmt);
+ converted_input->set_width(frame_width);
+ converted_input->set_height(frame_height);
+ if (converted_input->get_buffer(0) >= 0) {
+ uint8_t *cdst = converted_input->data(0);
+ const uint8_t *csrc = static_cast(
+ oakrender_codec_frame_const_data(frame));
+ int csrc_ls = oakrender_codec_frame_linesize_bytes(frame);
+ int cdst_ls = converted_input->linesize(0);
+ int ccopy = std::min(csrc_ls, cdst_ls);
+ for (int y = 0; y < frame_height; y++) {
+ memcpy(cdst + size_t(y) * cdst_ls,
+ csrc + size_t(y) * csrc_ls, size_t(ccopy));
+ }
+ AVFramePtr converted =
+ convert_packed_float_frame(converted_input, expected_fmt);
+ if (converted) {
+ src_frame = converted;
+ own_src = true;
+ }
+ }
+ }
+ if (!own_src) {
+ AVFramePtr converted = create_av_frame_ptr();
+ converted->set_format(expected_fmt);
+ converted->set_width(params_width(params_));
+ converted->set_height(params_height(params_));
+ if (converted->get_buffer(0) < 0) {
+ oakrender_codec_frame_free(&frame);
+ return;
+ }
+
+ AVFramePtr input = create_av_frame_ptr();
+ input->set_format(frame_fmt);
+ input->set_width(frame_width);
+ input->set_height(frame_height);
+ if (input->get_buffer(0) < 0) {
+ oakrender_codec_frame_free(&frame);
+ return;
+ }
+ uint8_t *idst = input->data(0);
+ const uint8_t *isrc = static_cast(
+ oakrender_codec_frame_const_data(frame));
+ int isrc_ls = oakrender_codec_frame_linesize_bytes(frame);
+ int idst_ls = input->linesize(0);
+ int icopy = std::min(isrc_ls, idst_ls);
+ for (int y = 0; y < frame_height; y++) {
+ memcpy(idst + size_t(y) * idst_ls, isrc + size_t(y) * isrc_ls,
+ size_t(icopy));
+ }
+
+ FBScaler *scaler = fb_scaler_create(
+ input->width(), input->height(), input->format(),
+ converted->width(), converted->height(), converted->format(),
+ FB_SCALER_POINT);
+ if (!scaler) {
+ oakrender_codec_frame_free(&frame);
+ return;
+ }
+
+ uint8_t *src_data[4];
+ int src_linesize[4];
+ uint8_t *dst_data[4];
+ int dst_linesize[4];
+ for (int i = 0; i < 4; ++i) {
+ src_data[i] = input->data(i);
+ src_linesize[i] = input->linesize(i);
+ dst_data[i] = converted->data(i);
+ dst_linesize[i] = converted->linesize(i);
+ }
+ fb_scaler_scale_slices(scaler, src_data, src_linesize,
+ input->height(), dst_data, dst_linesize);
+ fb_scaler_free(&scaler);
+
+ src_frame = converted;
+ own_src = true;
+ }
+ }
+
+ const uint8_t *src;
+ int src_row_bytes;
+ if (own_src) {
+ src = src_frame->data(0);
+ src_row_bytes = src_frame->linesize(0);
+ } else {
+ src = static_cast(oakrender_codec_frame_const_data(frame));
+ src_row_bytes = oakrender_codec_frame_linesize_bytes(frame);
+ }
+
+ int bytes_per_component =
+ bytes_per_component_of(params_format(params_));
+ int bytes_per_row = params_width(params_) * params_channels(params_) *
+ bytes_per_component;
+ 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(), own_src ? src_frame->height() : frame_height);
+
+ // Detect NaN/Inf in float input data before passing to CImg.
+ if (params_format(params_) == OAKCOMMON_PIXEL_FORMAT_F32) {
+ const float *src_f = reinterpret_cast(src);
+ int row_floats = src_row_bytes / static_cast(sizeof(float));
+ bool has_nan = false;
+ for (int y = 0; y < params_height(params_) && !has_nan; ++y) {
+ for (int x = 0;
+ x < params_width(params_) * params_channels(params_); ++x) {
+ float v = src_f[y * row_floats + x];
+ if (std::isnan(v) || std::isinf(v)) {
+ has_nan = true;
+ break;
+ }
+ }
+ }
+ if (has_nan) {
+ fprintf(stderr,
+ "[PLUGIN] NaN/Inf detected in input frame, filling with "
+ "black to avoid CImg crash\n");
+ std::memset(dst, 0, image->row_bytes() * image->height());
+ oakrender_codec_frame_free(&frame);
+ return;
+ }
+ }
+
+ if (params_format(params_) == OAKCOMMON_PIXEL_FORMAT_F32) {
+ const float *src_f = reinterpret_cast(src);
+ float *dst_f = reinterpret_cast(dst);
+ int src_stride = src_row_bytes / static_cast(sizeof(float));
+ int dst_stride = dst_row_bytes / static_cast(sizeof(float));
+ int floats_per_row = copy_bytes / static_cast(sizeof(float));
+ for (int y = 0; y < copy_height; ++y) {
+ for (int i = 0; i < floats_per_row; ++i) {
+ float v = src_f[y * src_stride + i];
+ if (std::isnan(v) || std::isinf(v)) {
+ v = 0.0f;
+ }
+ dst_f[y * dst_stride + i] = v;
+ }
+ }
+ } else if (dst_row_bytes == src_row_bytes && src_row_bytes == copy_bytes) {
+ std::memcpy(dst, src, size_t(copy_bytes) * size_t(copy_height));
+ } else {
+ for (int y = 0; y < copy_height; ++y) {
+ std::memcpy(dst + size_t(y) * dst_row_bytes,
+ src + size_t(y) * src_row_bytes, size_t(copy_bytes));
+ }
+ }
+
+ oakrender_codec_frame_free(&frame);
+}
+
+void OliveClipInstance::setOutputTexture(OakRenderTexture texture,
+ OfxTime time)
+{
+ if (!texture.ctx) {
+ return;
+ }
+ texture.addref(texture.ctx);
+ auto it = output_textures_.find(time);
+ if (it != output_textures_.end()) {
+ oakrender_display_texture_free(&it->second);
+ }
+ output_textures_[time] = texture;
+}
+
+#ifdef OFX_SUPPORTS_OPENGLRENDER
+OFX::Host::ImageEffect::Texture *
+OliveClipInstance::loadTexture(OfxTime time, const char *format,
+ const OfxRectD *optional_bounds)
+{
+ (void)format;
+
+ OakRenderTexture gl_texture = {};
+ if (isOutput()) {
+ auto it = output_textures_.find(time);
+ if (it != output_textures_.end()) {
+ gl_texture = it->second;
+ }
+ } else {
+ auto it = input_textures_.find(time);
+ if (it != input_textures_.end()) {
+ gl_texture = it->second;
+ }
+ }
+
+ if (!gl_texture.ctx || oakrender_display_texture_is_dummy(gl_texture)) {
+ return nullptr;
+ }
+
+ OfxRectD rod_d = getRegionOfDefinition(time);
+ OfxRectI rod = { static_cast(std::floor(rod_d.x1)),
+ static_cast(std::floor(rod_d.y1)),
+ static_cast(std::ceil(rod_d.x2)),
+ static_cast(std::ceil(rod_d.y2)) };
+ OfxRectI bounds = rod;
+ if (optional_bounds) {
+ bounds.x1 = static_cast(std::floor(optional_bounds->x1));
+ bounds.y1 = static_cast(std::floor(optional_bounds->y1));
+ bounds.x2 = static_cast(std::ceil(optional_bounds->x2));
+ bounds.y2 = static_cast(std::ceil(optional_bounds->y2));
+ }
+ bounds.x1 = std::max(bounds.x1, rod.x1);
+ bounds.y1 = std::max(bounds.y1, rod.y1);
+ bounds.x2 = std::min(bounds.x2, rod.x2);
+ bounds.y2 = std::min(bounds.y2, rod.y2);
+
+ const int bytes_per_row = params_width(params_) * params_channels(params_) *
+ bytes_per_component_of(params_format(params_));
+ const std::string &field = getFieldOrder();
+ const std::string unique_id =
+ std::to_string(reinterpret_cast(gl_texture.ctx)) + "_" +
+ std::to_string(static_cast(time));
+
+ const int texture_id = oakrender_display_texture_id(gl_texture);
+ if (texture_id == 0) {
+ return nullptr;
+ }
+ OFX::Host::ImageEffect::Texture *texture =
+ new OFX::Host::ImageEffect::Texture(*this, 1.0, 1.0, texture_id,
+ GL_TEXTURE_2D, bounds, rod,
+ bytes_per_row, field, unique_id);
+ texture->addReference();
+ return texture;
+}
+#endif
+
+}
+}
diff --git a/src/plugin/src/oliveclip.h b/src/plugin/src/oliveclip.h
new file mode 100644
index 000000000..a38809383
--- /dev/null
+++ b/src/plugin/src/oliveclip.h
@@ -0,0 +1,124 @@
+/*
+ * Oak Video Editor - Non-Linear Video Editor
+ * Copyright (C) 2025 Olive CE Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#ifndef OAK_OLIVECLIP_H
+#define OAK_OLIVECLIP_H
+
+#include