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 +#include + +#include "ofxCore.h" +#include "ofxhClip.h" + +#include "common/videoparams.h" +#include "image.h" +#include "render/renderer.h" + +namespace olive +{ +namespace plugin +{ + +class OliveClipInstance : public OFX::Host::ImageEffect::ClipInstance { +public: + OliveClipInstance(OFX::Host::ImageEffect::Instance *effect_instance, + OFX::Host::ImageEffect::ClipDescriptor &desc, + OakVideoParams params) + : ClipInstance(effect_instance, desc) + , params_(params) + , name_(desc.getName()) + { + if (params_.ctx) { + params_.addref(params_.ctx); + } + default_region_of_definition_ = { 0, 0, 0, 0 }; + } + + ~OliveClipInstance() override + { + if (params_.ctx) { + oakcommon_videoparams_free(¶ms_); + } + for (auto &pair : images_) { + delete pair.second; + } + for (auto &pair : input_textures_) { + oakrender_display_texture_free(&pair.second); + } + for (auto &pair : output_textures_) { + oakrender_display_texture_free(&pair.second); + } + } + + OFX::Host::ImageEffect::Image *getOutputImage(OfxTime time); + + const std::string &getUnmappedBitDepth() const override; + const std::string &getUnmappedComponents() const override; + const std::string &getPremult() const override; + double getAspectRatio() const override; + double getFrameRate() const override; + void getFrameRange(double &start_frame, double &end_frame) const override; + const std::string &getFieldOrder() const override; + bool getConnected() const override; + double getUnmappedFrameRate() const override; + void getUnmappedFrameRange(double &start_frame, + double &end_frame) const override; + bool getContinuousSamples() const override; + OFX::Host::ImageEffect::Image * + getImage(OfxTime time, const OfxRectD *optional_bounds) override; + OfxRectD getRegionOfDefinition(OfxTime time) const override; + + void setRegionOfDefinition(OfxRectD region_of_definition, OfxTime time); + void setDefaultRegionOfDefinition(OfxRectD region_of_definition); + void setParams(const olive::VideoParams ¶ms); +#ifdef OFX_SUPPORTS_OPENGLRENDER + OFX::Host::ImageEffect::Texture * + loadTexture(OfxTime time, const char *format, + const OfxRectD *optional_bounds) override; +#endif + + void setInputTexture(OakRenderTexture texture, OfxTime time, + bool readback_cpu = true); + void setOutputTexture(OakRenderTexture texture, OfxTime time); + + // Get the plugin-preferred VideoParams based on base class _pixelDepth/_components + OakVideoParams getPluginPreferredParams() const; + + // Prune old entries from the images_ cache to prevent unbounded growth. + // Output clip images are not pruned (they are typically single-frame). + void prune_images_cache(); + + static constexpr int k_max_input_image_cache = 8; + +private: + OakVideoParams params_; + + std::map region_of_definitions_; + + OfxRectD default_region_of_definition_; + + std::string name_; + std::map images_; + std::map input_textures_; + std::map output_textures_; +}; +} +} + +#endif //OAK_OLIVECLIP_H diff --git a/src/plugin/src/olivehost.cpp b/src/plugin/src/olivehost.cpp new file mode 100644 index 000000000..2399c1a77 --- /dev/null +++ b/src/plugin/src/olivehost.cpp @@ -0,0 +1,321 @@ +/* + * 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 "olivehost.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "common/current.h" +#include "oliveplugininstance.h" + +using namespace OFX::Host; +using namespace olive::plugin; + +namespace olive +{ +namespace plugin +{ +class PluginNode; +} +} + +#ifndef OAK_APP_VERSION +#define OAK_APP_VERSION "0.0.0" +#endif + +namespace +{ + +olive::plugin::HostMessageHandler message_handler_; + +void add_plugin_path(OFX::Host::PluginCache *cache, const std::string &path, + bool recurse = true) +{ + if (!cache || path.empty()) { + return; + } + std::error_code ec; + if (!std::filesystem::exists(path, ec)) { + return; + } + cache->addFileToPath( + std::filesystem::weakly_canonical(path, ec).string(), recurse); +} + +void add_plugin_paths_from_env(OFX::Host::PluginCache *cache, + const char *env_var) +{ + const char *raw = std::getenv(env_var); + if (!raw || !*raw) { + return; + } + + const char separator = +#if defined(_WIN32) + ';'; +#else + ':'; +#endif + + std::string remaining(raw); + size_t pos; + while ((pos = remaining.find(separator)) != std::string::npos) { + std::string path = remaining.substr(0, pos); + remaining.erase(0, pos + 1); + if (!path.empty()) { + add_plugin_path(cache, path); + } + } + if (!remaining.empty()) { + add_plugin_path(cache, remaining); + } +} + +std::string format_message(const char *format, va_list args) +{ + char buffer[1024]; + buffer[0] = '\0'; + vsnprintf(buffer, sizeof(buffer), format, args); + return buffer; +} + +} // namespace + +void olive::plugin::set_host_message_handler(HostMessageHandler handler) +{ + message_handler_ = std::move(handler); +} + +olive::plugin::HostMessageHandler olive::plugin::get_host_message_handler() +{ + return message_handler_; +} + +void olive::plugin::load_plugins(const std::string &path) +{ + OakCurrent current = oakcommon_current_instance(); + + void *host_ptr = nullptr; + void *cache_ptr = nullptr; + oakcommon_current_get_plugin_host(current, &host_ptr); + oakcommon_current_get_plugin_cache(current, &cache_ptr); + + std::shared_ptr host; + std::shared_ptr image_effect_plugin_cache; + + if (host_ptr) { + host = *static_cast *>(host_ptr); + } + if (cache_ptr) { + image_effect_plugin_cache = + *static_cast *>( + cache_ptr); + } + + if (!host || !image_effect_plugin_cache) { + host = std::make_shared(); + image_effect_plugin_cache = + std::make_shared(*host); + + // The Current slots hold owning shared_ptr copies; the destroy + // callbacks free those copies when the slots are replaced or the + // current object dies. + auto *host_slot = new std::shared_ptr(host); + oakcommon_current_set_plugin_host( + current, host_slot, + [](void *p) { delete static_cast *>(p); }); + + auto *cache_slot = + new std::shared_ptr( + image_effect_plugin_cache); + oakcommon_current_set_plugin_cache( + current, cache_slot, + [](void *p) { + delete static_cast *>( + p); + }); + + image_effect_plugin_cache->registerInCache( + *OFX::Host::PluginCache::getPluginCache()); + } + oakcommon_current_free(¤t); + + OFX::Host::PluginCache *cache = OFX::Host::PluginCache::getPluginCache(); + cache->setPluginHostPath("Olive"); + + const std::string home_path = std::getenv("HOME") ? std::getenv("HOME") : ""; + if (!home_path.empty()) { + add_plugin_path(cache, home_path + "/.OFX/Plugins"); + add_plugin_path(cache, home_path + "/.local/share/OFX/Plugins"); + add_plugin_path(cache, + home_path + "/.local/share/olive/ofx/Plugins"); + } + + // Application-relative plugin paths are resolved by the facade; scan + // the default locations relative to the working directory here. + add_plugin_path(cache, "../OFX/Plugins"); + add_plugin_path(cache, "../share/olive/ofx/Plugins"); + add_plugin_path(cache, "../lib/olive/ofx/Plugins"); + + add_plugin_paths_from_env(cache, "OLIVE_OFX_PLUGIN_PATH"); + add_plugin_paths_from_env(cache, "OLIVE_PLUGIN_PATH"); + + if (!path.empty()) { + add_plugin_path(cache, path, true); + } + cache->scanPluginFiles(); +} + +OliveHost::OliveHost() +{ + // Identify the host to plugins; HostSupport seeds these with "UNKNOWN". + _properties.setStringProperty(kOfxPropName, "Oak Video Editor"); + _properties.setStringProperty(kOfxPropLabel, "Oak Video Editor"); + _properties.setStringProperty(kOfxPropVersionLabel, OAK_APP_VERSION); + + // Numeric version for plugins that query kOfxPropVersion directly. + int version_parts[3] = { 0, 0, 0 }; + sscanf(OAK_APP_VERSION, "%d.%d.%d", &version_parts[0], &version_parts[1], + &version_parts[2]); + _properties.setIntProperty(kOfxPropVersion, version_parts[0], 0); + _properties.setIntProperty(kOfxPropVersion, version_parts[1], 1); + _properties.setIntProperty(kOfxPropVersion, version_parts[2], 2); +} + +OliveHost::~OliveHost() +{ +} + +void OliveHost::destroy_instance(OFX::Host::ImageEffect::Instance *instance) +{ + if (!instance) { + return; + } + for (auto it = instances_.begin(); it != instances_.end(); ++it) { + if (it->get() == instance) { + instances_.erase(it); + break; + } + } +} + +std::shared_ptr +OliveHost::makeDescriptor(ImageEffect::ImageEffectPlugin *plugin) +{ + std::shared_ptr desc = + std::make_shared(plugin); + descriptors_.push_back(std::shared_ptr(desc)); + return desc; +} + +std::shared_ptr +OliveHost::makeDescriptor(const ImageEffect::Descriptor &root_context, + ImageEffect::ImageEffectPlugin *plugin) +{ + std::shared_ptr desc = + std::make_shared(root_context, plugin); + descriptors_.push_back(std::shared_ptr(desc)); + return desc; +} + +std::shared_ptr +OliveHost::makeDescriptor(const std::string &bundle_path, + ImageEffect::ImageEffectPlugin *plugin) +{ + std::shared_ptr desc = + std::make_shared(bundle_path, plugin); + descriptors_.push_back(std::shared_ptr(desc)); + return desc; +} + +ImageEffect::Instance * +OliveHost::newInstance(void *client_data, ImageEffect::ImageEffectPlugin *plugin, + ImageEffect::Descriptor &desc, + const std::string &context) +{ + auto *instance = new OlivePluginInstance(plugin, desc, context, true); + if (client_data) { + instance->set_node_handle( + *static_cast(client_data)); + } + instances_.push_back(std::shared_ptr(instance)); + return instance; +} + +OfxStatus OliveHost::vmessage(const char *type, const char *id, + const char *format, va_list args) +{ + if (!type || !format) { + return kOfxStatFailed; + } + + std::string message = format_message(format, args); + + if (message_handler_) { + return message_handler_(type, message); + } + + // Headless default: log to stderr, questions get "no". + fprintf(stderr, "OFX message: %s %s\n", type, message.c_str()); + if (strcmp(type, kOfxMessageQuestion) == 0) { + return kOfxStatReplyNo; + } + return kOfxStatOK; +} + +OfxStatus OliveHost::setPersistentMessage(const char *type, const char *id, + const char *format, va_list args) +{ + if (!type || !format) { + return kOfxStatFailed; + } + + std::string message = format_message(format, args); + + if (strcmp(type, kOfxMessageError) == 0) { + persistent_messages_.push_back({ HostMessageType::error, message }); + } else if (strcmp(type, kOfxMessageWarning) == 0) { + persistent_messages_.push_back({ HostMessageType::warning, message }); + } else if (strcmp(type, kOfxMessageMessage) == 0) { + persistent_messages_.push_back({ HostMessageType::message, message }); + } else { + return kOfxStatFailed; + } + + if (message_handler_) { + return message_handler_(type, message); + } + + fprintf(stderr, "OFX %s: %s\n", type, message.c_str()); + return kOfxStatOK; +} + +OfxStatus OliveHost::clearPersistentMessage() +{ + persistent_messages_.clear(); + return kOfxStatOK; +} diff --git a/src/plugin/src/olivehost.h b/src/plugin/src/olivehost.h new file mode 100644 index 000000000..0ae0e9cc5 --- /dev/null +++ b/src/plugin/src/olivehost.h @@ -0,0 +1,132 @@ +/* + * 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_OLIVE_HOST_H +#define OAK_OLIVE_HOST_H + +#include +#include +#include +#include +#include + +#include "ofxCore.h" +#include "ofxhHost.h" +#include "ofxhImageEffect.h" +#include "ofxhImageEffectAPI.h" + +namespace olive +{ +namespace plugin +{ + +enum class HostMessageType { error, warning, message }; +struct HostPersistentMessage { + HostMessageType type; + std::string message; +}; + +/** + * @brief UI message handler for OFX host messages + * + * Registered by the facade. Return kOfxStatReplyYes/No for questions, + * kOfxStatOK otherwise. Without a handler, messages are logged to + * stderr and questions get kOfxStatReplyNo (headless default). + */ +using HostMessageHandler = + std::function; + +void set_host_message_handler(HostMessageHandler handler); + +/** @brief Currently registered handler (may be empty). */ +HostMessageHandler get_host_message_handler(); + +void load_plugins(const std::string &path = std::string()); + +class OliveHost : public OFX::Host::ImageEffect::Host { +public: + OliveHost(); + ~OliveHost() override; + void destroy_instance(OFX::Host::ImageEffect::Instance *instance); + + bool pluginSupported(OFX::Host::ImageEffect::ImageEffectPlugin *plugin, + std::string &reason) const override + { + if (!plugin) { + reason = "null plugin"; + return false; + } + if (plugin->getContexts().empty()) { + reason = "no supported contexts (describe failed)"; + return false; + } + return true; + }; + + OFX::Host::ImageEffect::Instance * + newInstance(void *client_data, + OFX::Host::ImageEffect::ImageEffectPlugin *plugin, + OFX::Host::ImageEffect::Descriptor &desc, + const std::string &context) override; + + std::shared_ptr + makeDescriptor(OFX::Host::ImageEffect::ImageEffectPlugin *plugin) override; + + std::shared_ptr + makeDescriptor(const OFX::Host::ImageEffect::Descriptor &root_context, + OFX::Host::ImageEffect::ImageEffectPlugin *plugin) override; + + std::shared_ptr + makeDescriptor(const std::string &bundle_path, + OFX::Host::ImageEffect::ImageEffectPlugin *plugin) override; + /// vmessage + OfxStatus vmessage(const char *type, const char *id, + const char *format, va_list args) override; + + /// vmessage + OfxStatus setPersistentMessage(const char *type, const char *id, + const char *format, va_list args) override; + /// vmessage + OfxStatus clearPersistentMessage() override; + +#ifdef OFX_SUPPORTS_OPENGLRENDER + /// @see OfxImageEffectOpenGLRenderSuiteV1.flushResources() + virtual OfxStatus flushOpenGLResources() const override + { + return kOfxStatFailed; + }; +#endif + + int persistent_message_count() const + { + return int(persistent_messages_.size()); + } + + const std::vector &persistent_messages() const + { + return persistent_messages_; + } + +private: + std::vector> + descriptors_; + std::vector> instances_; + std::vector persistent_messages_; +}; +} +} +#endif diff --git a/src/plugin/src/oliveplugininstance.cpp b/src/plugin/src/oliveplugininstance.cpp new file mode 100644 index 000000000..1a7c17bb8 --- /dev/null +++ b/src/plugin/src/oliveplugininstance.cpp @@ -0,0 +1,608 @@ +/* + * 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 "oliveplugininstance.h" + +#include +#include + +#include "ofxGPURender.h" +#include "node/sequence.h" +#include "olivehost.h" +#include "ofxMessage.h" +#include "oliveclip.h" +#include "paraminstance.h" + +namespace olive +{ +namespace plugin +{ +namespace +{ + +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); + +std::string format_ofx_message(const char *format, va_list args) +{ + char buffer[1024]; + va_list args_copy; + va_copy(args_copy, args); + const int needed = vsnprintf(buffer, sizeof(buffer), format, args_copy); + va_end(args_copy); + if (needed < 0) { + return std::string(); + } + if (needed < static_cast(sizeof(buffer))) { + return std::string(buffer); + } + std::vector dynamic_buffer(size_t(needed) + 1); + const int written = + vsnprintf(dynamic_buffer.data(), dynamic_buffer.size(), format, args); + if (written < 0) { + return std::string(); + } + return std::string(dynamic_buffer.data()); +} + +const std::string &field_order_for_params(OakVideoParams params) +{ + int interlacing = 0; + if (params.ctx) { + oakcommon_videoparams_get_interlacing(params, &interlacing); + } + switch (interlacing) { + 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; + } +} + +std::thread::id main_thread_id_; +bool main_thread_id_set_ = false; + +ActiveViewerProvider active_viewer_provider_; + +UndoSubmitFn undo_submit_fn_; + +} // namespace + +bool is_gui_thread() +{ + if (!main_thread_id_set_) { + main_thread_id_ = std::this_thread::get_id(); + main_thread_id_set_ = true; + } + return std::this_thread::get_id() == main_thread_id_; +} + +void set_main_thread_id(std::thread::id id) +{ + main_thread_id_ = id; + main_thread_id_set_ = true; +} + +void set_active_viewer_provider(ActiveViewerProvider provider) +{ + active_viewer_provider_ = std::move(provider); +} + +void set_undo_submit_callback(UndoSubmitFn fn) +{ + undo_submit_fn_ = std::move(fn); +} + +const std::string &OlivePluginInstance::getDefaultOutputFielding() const +{ + return field_order_for_params(params_); +} + +void register_node_instance(uintptr_t identity, + OlivePluginInstance *instance); + +void OlivePluginInstance::set_node_handle(OakNodeNode node) +{ + if (node_.ctx) { + register_node_instance(oaknode_node_identity(node_), nullptr); + } + node_ = node; + if (node_.ctx) { + register_node_instance(oaknode_node_identity(node_), this); + } + for (const auto &entry : getParams()) { + if (!entry.second) { + continue; + } + if (auto *bound = dynamic_cast(entry.second)) { + bound->set_node(node_); + } + } +} + +OfxStatus OlivePluginInstance::vmessage(const char *type, const char *id, + const char *format, va_list args) +{ + const std::string message = format_ofx_message(format, args); + if (message.empty()) { + return kOfxStatFailed; + } + + // UI messages route through the host's registered handler + HostMessageHandler handler = get_host_message_handler(); + if (handler) { + return handler(type, message); + } + + fprintf(stderr, "OFX message: %s %s\n", type, message.c_str()); + if (strcmp(type, kOfxMessageQuestion) == 0) { + return kOfxStatReplyNo; + } + return kOfxStatOK; +} + +OfxStatus OlivePluginInstance::setPersistentMessage(const char *type, + const char *id, + const char *format, + va_list args) +{ + const std::string message = format_ofx_message(format, args); + if (message.empty()) { + return kOfxStatFailed; + } + + ErrorType error_type; + // If this is an error message + if (strncmp(type, kOfxMessageError, strlen(kOfxMessageError)) == 0) { + error_type = ErrorType::error; + } + // A warning + else if (strncmp(type, kOfxMessageWarning, strlen(kOfxMessageWarning)) == + 0) { + error_type = ErrorType::warning; + } + // A simple information + else if (strncmp(type, kOfxMessageMessage, strlen(kOfxMessageMessage)) == + 0) { + error_type = ErrorType::message; + } else { + return kOfxStatFailed; + } + + persistent_errors_.push_back({ error_type, message }); + + HostMessageHandler handler = get_host_message_handler(); + if (handler) { + return handler(type, message); + } + + fprintf(stderr, "OFX %s: %s\n", type, message.c_str()); + return kOfxStatOK; +} + +OfxStatus OlivePluginInstance::clearPersistentMessage() +{ + persistent_errors_.clear(); + return kOfxStatOK; +} + +void OlivePluginInstance::getProjectSize(double &x_size, double &y_size) const +{ + double par = 1.0; + int par_num = 0, par_den = 1; + int width = 0, height = 0; + if (params_.ctx) { + oakcommon_videoparams_get_pixel_aspect_ratio(params_, &par_num, + &par_den); + oakcommon_videoparams_get_width(params_, &width); + oakcommon_videoparams_get_height(params_, &height); + } + if (par_den != 0) { + par = double(par_num) / par_den; + } + x_size = width * par; + y_size = height; +} + +void OlivePluginInstance::getProjectOffset(double &x_offset, + double &y_offset) const +{ + double par = 1.0; + int par_num = 0, par_den = 1; + float x = 0, y = 0; + if (params_.ctx) { + oakcommon_videoparams_get_pixel_aspect_ratio(params_, &par_num, + &par_den); + oakcommon_videoparams_get_x(params_, &x); + oakcommon_videoparams_get_y(params_, &y); + } + if (par_den != 0) { + par = double(par_num) / par_den; + } + x_offset = x * par; + y_offset = y; +} + +void OlivePluginInstance::getProjectExtent(double &x_size, double &y_size) const +{ + getProjectSize(x_size, y_size); +} + +double OlivePluginInstance::getProjectPixelAspectRatio() const +{ + int par_num = 0, par_den = 1; + if (params_.ctx) { + oakcommon_videoparams_get_pixel_aspect_ratio(params_, &par_num, + &par_den); + } + if (par_den == 0) { + return 1.0; + } + double par = double(par_num) / par_den; + if (par == 0.0) { + return 1.0; // default PAR when not explicitly set + } + return par; +} + +double OlivePluginInstance::getFrameRate() const +{ + int num = 0, den = 1; + if (params_.ctx) { + oakcommon_videoparams_get_frame_rate(params_, &num, &den); + } + return den ? double(num) / den : 0.0; +} + +double OlivePluginInstance::getEffectDuration() const +{ + // Return a default duration value + return 100.0; +} + +double OlivePluginInstance::getFrameRecursive() const +{ + // Return current frame (this would typically be set by the host during rendering) + return 0.0; +} + +void OlivePluginInstance::getRenderScaleRecursive(double &x, double &y) const +{ + // Return default render scale (1.0, 1.0) + x = 1.0; + y = 1.0; +} + +OFX::Host::Param::Instance * +OlivePluginInstance::newParam(const std::string &name, + OFX::Host::Param::Descriptor &desc) +{ + const std::string &type = desc.getType(); + + if (type == kOfxParamTypeInteger) { + return new IntegerInstance(node_, desc, this); + } else if (type == kOfxParamTypeDouble) { + return new DoubleInstance(node_, name, desc, this); + } else if (type == kOfxParamTypeBoolean) { + return new BooleanInstance(node_, name, desc, this); + } else if (type == kOfxParamTypeChoice) { + return new ChoiceInstance(node_, name, desc, this); + } else if (type == kOfxParamTypeString) { + return new StringInstance(node_, name, desc, this); + } else if (type == kOfxParamTypeRGBA) { + return new RGBAInstance(node_, name, desc, this); + } else if (type == kOfxParamTypeRGB) { + return new RGBInstance(node_, name, desc, this); + } else if (type == kOfxParamTypeDouble2D) { + return new Double2DInstance(node_, name, desc, this); + } else if (type == kOfxParamTypeInteger2D) { + return new Integer2DInstance(node_, name, desc, this); + } else if (type == kOfxParamTypeDouble3D) { + return new Double3DInstance(node_, name, desc, this); + } else if (type == kOfxParamTypeInteger3D) { + return new Integer3DInstance(node_, name, desc, this); + } else if (type == kOfxParamTypeCustom || type == kOfxParamTypeBytes) { + return new CustomInstance(node_, name, desc, this); + } else if (type == kOfxParamTypeGroup) { + return new GroupInstance(desc, this); + } else if (type == kOfxParamTypePage) { + return new PageInstance(desc, this); + } else if (type == kOfxParamTypePushButton) { + return new PushbuttonInstance(node_, name, desc, this); + } + + return nullptr; // 未实现的类型 +} + +OfxStatus OlivePluginInstance::editBegin(const std::string &name) +{ + edit_depth_++; + if (edit_depth_ == 1) { + edit_command_ = {}; + edit_label_.clear(); + edit_first_label_.clear(); + edit_param_count_ = 0; + if (!name.empty()) { + edit_first_label_ = "Change " + name; + } + } + return kOfxStatOK; +} + +OfxStatus OlivePluginInstance::editEnd() +{ + if (edit_depth_ > 0) { + edit_depth_--; + } + if (edit_depth_ == 0 && edit_command_.ctx) { + std::string label = edit_label_; + if (label.empty()) { + if (edit_param_count_ <= 1 && !edit_first_label_.empty()) { + label = edit_first_label_; + } else if (edit_param_count_ > 1 && !edit_first_label_.empty()) { + label = edit_first_label_ + " (+" + + std::to_string(edit_param_count_ - 1) + ")"; + } else { + label = "Edit Parameters"; + } + } + if (undo_submit_fn_) { + undo_submit_fn_(edit_command_, label); + } else { + oakundo_command_redo_now(edit_command_); + oakundo_command_free(&edit_command_); + } + edit_command_ = {}; + edit_label_.clear(); + edit_first_label_.clear(); + edit_param_count_ = 0; + } + return kOfxStatOK; +} + +void OlivePluginInstance::submit_undo_command(OakUndoCommand command, + const std::string &label) +{ + if (!command.ctx) { + return; + } + + if (edit_depth_ > 0) { + if (!edit_command_.ctx) { + edit_command_ = oakundo_command_init_multi(); + } + edit_param_count_++; + if (!label.empty() && edit_first_label_.empty()) { + edit_first_label_ = label; + } + + oakundo_command_redo_now(command); + oakundo_command_multi_add_child(edit_command_, command); + return; + } + + if (undo_submit_fn_ && is_gui_thread()) { + undo_submit_fn_(command, label); + return; + } + + oakundo_command_redo_now(command); + oakundo_command_free(&command); +} + +void OlivePluginInstance::progressStart(const std::string &message, + const std::string &messageid) +{ + (void)messageid; + progress_cancelled_ = false; + progress_active_ = true; + + if (progress_reporter_) { + progress_reporter_->close(); + progress_reporter_.reset(); + } + + std::string dialog_message = + message.empty() ? "Processing..." : message; + + progress_reporter_.reset( + create_plugin_progress_reporter(dialog_message, "OpenFX")); + progress_reporter_->set_cancel_callback( + [this](void *) { progress_cancelled_ = true; }, nullptr); + progress_reporter_->show(); +} + +void OlivePluginInstance::progressEnd() +{ + progress_active_ = false; + progress_cancelled_ = false; + + if (progress_reporter_) { + progress_reporter_->close(); + progress_reporter_.reset(); + } +} + +bool OlivePluginInstance::progressUpdate(double t) +{ + if (!progress_active_) { + return true; + } + + if (progress_reporter_) { + double clamped = t < 0.0 ? 0.0 : (t > 1.0 ? 1.0 : t); + progress_reporter_->set_progress(clamped); + } + + return !progress_cancelled_; +} + +#ifdef OFX_SUPPORTS_OPENGLRENDER +OfxStatus OlivePluginInstance::contextAttachedAction() +{ + if (!open_gl_enabled_) { + return kOfxStatReplyDefault; + } + return kOfxStatOK; +} + +OfxStatus OlivePluginInstance::contextDetachedAction() +{ + if (!open_gl_enabled_) { + return kOfxStatReplyDefault; + } + return kOfxStatOK; +} +#endif + +double OlivePluginInstance::timeLineGetTime() +{ + if (active_viewer_provider_) { + OakNodeNode viewer = active_viewer_provider_(); + if (viewer.ctx) { + // Playhead as seconds + int num = 0, den = 1; + if (oaknode_sequence_get_playhead( + oaknode_sequence_from_node(viewer), + &num, &den) == OAKNODE_OK && den != 0) { + return double(num) / den; + } + } + } + + return 0.0; +} + +void OlivePluginInstance::timeLineGotoTime(double t) +{ + if (active_viewer_provider_) { + OakNodeNode viewer = active_viewer_provider_(); + if (viewer.ctx) { + olive::core::Rational r = olive::core::Rational::from_double(t); + oaknode_sequence_set_playhead( + oaknode_sequence_from_node(viewer), r.numerator(), + r.denominator()); + } + } +} + +void OlivePluginInstance::timeLineGetBounds(double &t1, double &t2) +{ + if (active_viewer_provider_) { + OakNodeNode viewer = active_viewer_provider_(); + if (viewer.ctx) { + int len_num = 0, len_den = 1; + if (oaknode_sequence_get_length( + oaknode_sequence_from_node(viewer), &len_num, + &len_den) == OAKNODE_OK && len_den != 0) { + t1 = 0.0; + t2 = double(len_num) / len_den; + return; + } + } + } + + t1 = 0.0; + t2 = 0.0; +} + +void OlivePluginInstance::setCustomInArgs(const std::string &action, + OFX::Host::Property::Set &in_args) +{ + if (action == kOfxImageEffectActionRender || + action == kOfxImageEffectActionBeginSequenceRender || + action == kOfxImageEffectActionEndSequenceRender) { + in_args.setIntProperty(kOfxImageEffectPropOpenGLEnabled, + open_gl_enabled_ ? 1 : 0); + } +} + +OFX::Host::ImageEffect::ClipInstance *OlivePluginInstance::newClipInstance( + OFX::Host::ImageEffect::Instance *plugin, + OFX::Host::ImageEffect::ClipDescriptor *descriptor, int index) +{ + // Create a new clip instance + OliveClipInstance *clip_instance = + new OliveClipInstance(plugin, *descriptor, params_); + + // Initialize base class clip properties from VideoParams so that + // setupClipPreferencesArgs and plugin constructors (which may fetch + // clips and query their properties before getClipPreferences is called) + // have valid defaults instead of kOfxImageComponentNone / kOfxBitDepthNone. + std::string depth = kOfxBitDepthFloat; // host default + std::string comp = kOfxImageComponentRGBA; // host default + + int format = -1; + int channels = 0; + if (params_.ctx) { + oakcommon_videoparams_get_format(params_, &format); + oakcommon_videoparams_get_channel_count(params_, &channels); + } + + switch (format) { + case OAKCOMMON_PIXEL_FORMAT_U8: + depth = kOfxBitDepthByte; + break; + case OAKCOMMON_PIXEL_FORMAT_U16: + depth = kOfxBitDepthShort; + break; + case OAKCOMMON_PIXEL_FORMAT_F16: + depth = kOfxBitDepthHalf; + break; + case OAKCOMMON_PIXEL_FORMAT_F32: + depth = kOfxBitDepthFloat; + break; + default: + break; // keep F32 default + } + + switch (channels) { + case 1: + comp = kOfxImageComponentAlpha; + break; + case 3: + comp = kOfxImageComponentRGB; + break; + case 4: + comp = kOfxImageComponentRGBA; + break; + default: + break; // keep RGBA default + } + + clip_instance->setPixelDepth(depth); + clip_instance->setComponents(comp); + + return clip_instance; +} + +OlivePluginInstance::~OlivePluginInstance() +{ + if (node_.ctx) { + register_node_instance(oaknode_node_identity(node_), nullptr); + } + _created = false; + if (params_.ctx) { + oakcommon_videoparams_free(¶ms_); + } +} + +} +} diff --git a/src/plugin/src/oliveplugininstance.h b/src/plugin/src/oliveplugininstance.h new file mode 100644 index 000000000..2b7553458 --- /dev/null +++ b/src/plugin/src/oliveplugininstance.h @@ -0,0 +1,253 @@ +/* + * 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_OLIVE_INSTANCE_H +#define OAK_OLIVE_INSTANCE_H + +#include +#include +#include +#include +#include +#include + +#include "ofxCore.h" +#include "ofxImageEffect.h" +#include "ofxhImageEffect.h" + +#include "common/videoparams.h" +#include "node/node.h" +#include "pluginprogressreporter.h" +#include "undo/undocommand.h" + +namespace olive +{ +namespace plugin +{ + +/** + * @brief Whether the calling thread is the registered main (GUI) thread + * + * The facade registers the main thread at startup with + * set_main_thread_id(); before registration the first caller's thread + * is treated as main (headless default). + */ +bool is_gui_thread(); + +void set_main_thread_id(std::thread::id id); + +class PluginProgressReporter; +enum class ErrorType { error, warning, message }; +struct PersistentErrors { + ErrorType type; + std::string message; +}; + +/** + * @brief Provider returning the currently active viewer as an oaknode + * handle + * + * Registered by the facade. Without a provider, the OFX timeline suite + * falls back to its safe defaults (current time 0, empty bounds, + * seeking does nothing). + */ +using ActiveViewerProvider = std::function; + +void set_active_viewer_provider(ActiveViewerProvider provider); + +/** + * @brief Undo submission callback (facade pushes onto its undo stack) + * + * The command handle is consumed by the callback (the facade stack + * takes a reference; see oakundo push semantics). Without a callback, + * commands are redone immediately and released. + */ +using UndoSubmitFn = std::function; + +void set_undo_submit_callback(UndoSubmitFn fn); + +class OlivePluginInstance : public OFX::Host::ImageEffect::Instance { +public: + OlivePluginInstance(OFX::Host::ImageEffect::ImageEffectPlugin *plugin, + OFX::Host::ImageEffect::Descriptor &desc, + const std::string &context, bool interactive) + : OFX::Host::ImageEffect::Instance(plugin, desc, context, interactive) + { + } + OlivePluginInstance(OlivePluginInstance &instance) + : Instance(instance._plugin, *instance._descriptor, instance._context, + instance._interactive) + { + // Do NOT shallow-copy _clips: Instance::~Instance() deletes them, + // which would cause a double-free. Clips are re-created in populate(). + _created = instance._created; + _clipPrefsDirty = instance._clipPrefsDirty; + _continuousSamples = instance._continuousSamples; + _frameVarying = instance._frameVarying; + _outputPreMultiplication = instance._outputPreMultiplication; + _outputFielding = instance._outputFielding; + _outputFrameRate = instance._outputFrameRate; + } + explicit OlivePluginInstance(Instance &instance) + : Instance(instance) {}; + ~OlivePluginInstance() override; + const std::string &getDefaultOutputFielding() const override; + + void setVideoParam(OakVideoParams params) + { + if (params_.ctx) { + oakcommon_videoparams_free(¶ms_); + } + params_ = params; + if (params_.ctx) { + params_.addref(params_.ctx); + } + } + void set_node_handle(OakNodeNode node); + OakNodeNode node_handle() const + { + return node_; + } + void setOpenGLEnabled(bool enabled) + { + open_gl_enabled_ = enabled; + } + bool isCreated() const + { + return _created; + } + OFX::Host::ImageEffect::ClipInstance * + newClipInstance(OFX::Host::ImageEffect::Instance *plugin, + OFX::Host::ImageEffect::ClipDescriptor *descriptor, + int index) override; + + OfxStatus vmessage(const char *type, const char *id, const char *format, + va_list args) override; + + OfxStatus setPersistentMessage(const char *type, const char *id, + const char *format, va_list args) override; + + OfxStatus clearPersistentMessage() override; + int persistent_message_count() const + { + return int(persistent_errors_.size()); + } + const std::vector &persistent_messages() const + { + return persistent_errors_; + } + + void getProjectSize(double &x_size, double &y_size) const override; + void getProjectOffset(double &x_offset, double &y_offset) const override; + void getProjectExtent(double &x_size, double &y_size) const override; + // The pixel aspect ratio of the current project + double getProjectPixelAspectRatio() const override; + + // The duration of the effect + // This contains the duration of the plug-in effect, in frames. + double getEffectDuration() const override; + + // For an instance, this is the frame rate of the project the effect is in. + double getFrameRate() const override; + + /// This is called whenever a param is changed by the plugin so that + /// the recursive instanceChangedAction will be fed the correct frame + double getFrameRecursive() const override; + + /// This is called whenever a param is changed by the plugin so that + /// the recursive instanceChangedAction will be fed the correct + /// renderScale + void getRenderScaleRecursive(double &x, double &y) const override; + + //////////////////////////////////////////////////////////////////////////////// + // overridden for Param::SetInstance + + /// make a parameter instance + OFX::Host::Param::Instance * + newParam(const std::string &name, + OFX::Host::Param::Descriptor &descriptor) override; + + void submit_undo_command(OakUndoCommand command, const std::string &label); + + /// Triggered when the plug-in calls OfxParameterSuiteV1::paramEditBegin + virtual OfxStatus editBegin(const std::string &name) override; + + /// Triggered when the plug-in calls OfxParameterSuiteV1::paramEditEnd + virtual OfxStatus editEnd() override; + + //////////////////////////////////////////////////////////////////////////////// + // overridden for Progress::ProgressI + + /// Start doing progress. + virtual void progressStart(const std::string &message, + const std::string &messageid) override; + + /// finish yer progress + virtual void progressEnd() override; + + /// set the progress to some level of completion, returns + /// false if you should abandon processing, true to continue + virtual bool progressUpdate(double t) override; + +#ifdef OFX_SUPPORTS_OPENGLRENDER + virtual OfxStatus contextAttachedAction() override; + virtual OfxStatus contextDetachedAction() override; +#endif + + //////////////////////////////////////////////////////////////////////////////// + // overridden for TimeLine::TimeLineI + + /// get the current time on the timeline. This is not necessarily the same + /// time as being passed to an action (eg render) + double timeLineGetTime() override; + + /// set the timeline to a specific time + void timeLineGotoTime(double t) override; + + /// get the first and last times available on the effect's timeline + void timeLineGetBounds(double &t1, double &t2) override; + + void setCustomInArgs(const std::string &action, + OFX::Host::Property::Set &in_args) override; + +private: + std::vector persistent_errors_; + OakVideoParams params_ = {}; + OakNodeNode node_ = {}; + int edit_depth_ = 0; + OakUndoCommand edit_command_ = {}; + std::string edit_label_; + std::string edit_first_label_; + int edit_param_count_ = 0; + std::unique_ptr progress_reporter_; + bool progress_cancelled_ = false; + bool progress_active_ = false; + bool open_gl_enabled_ = false; + +public: + std::mutex &mutex() + { + return mutex_; + } + +private: + std::mutex mutex_; +}; +} +} +#endif diff --git a/src/plugin/src/paraminstance.cpp b/src/plugin/src/paraminstance.cpp new file mode 100644 index 000000000..75dfb022b --- /dev/null +++ b/src/plugin/src/paraminstance.cpp @@ -0,0 +1,75 @@ +/* + * 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 "paraminstance.h" + +#include + +#include "oliveplugininstance.h" + +namespace olive +{ +namespace plugin +{ +namespace +{ + +/** + * @brief Node-identity → plugin instance registry, maintained by + * OlivePluginInstance::set_node_handle() + */ +std::map &instance_registry() +{ + static std::map registry; + return registry; +} + +} // namespace + +void register_node_instance(uintptr_t identity, OlivePluginInstance *instance) +{ + if (instance) { + instance_registry()[identity] = instance; + } else { + instance_registry().erase(identity); + } +} + +void submit_undo_command(OakNodeNode node, OakUndoCommand command, + const std::string &label) +{ + if (!command.ctx) { + return; + } + + if (node.ctx) { + auto it = instance_registry().find(oaknode_node_identity(node)); + if (it != instance_registry().end() && it->second) { + it->second->submit_undo_command(command, label); + return; + } + } + + // No instance bound: run immediately and release + oakundo_command_redo_now(command); + oakundo_command_free(&command); +} + +} +} diff --git a/src/plugin/src/paraminstance.h b/src/plugin/src/paraminstance.h new file mode 100644 index 000000000..4c48b54ed --- /dev/null +++ b/src/plugin/src/paraminstance.h @@ -0,0 +1,1397 @@ +/* + * 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. + * + */ + +// Copyright OpenFX and contributors to the OpenFX project. +#ifndef OAK_PARAM_INSTANCE_H +#define OAK_PARAM_INSTANCE_H + +#include +#include +#include + +#include "ofxhParam.h" + +#include "common/current.h" +#include "videoparams.h" +#include "node/node.h" +#include "olive/core/util/rational.h" +#include "undo/undocommand.h" + +namespace olive +{ +namespace plugin +{ + +inline bool +is_normalised_coordinate_system(const OFX::Host::Param::Descriptor &descriptor) +{ + return descriptor.getDefaultCoordinateSystem() == + kOfxParamCoordinatesNormalised; +} + +inline void get_project_extent(double &x_size, double &y_size) +{ + // The Current video-params slot holds a heap + // std::shared_ptr (facade sets it); fall back to + // a 0-extent when unset. + x_size = 0; + y_size = 0; + + OakCurrent current = oakcommon_current_instance(); + void *slot = nullptr; + oakcommon_current_get_video_params(current, &slot); + oakcommon_current_free(¤t); + if (!slot) { + return; + } + + const auto &vp = + *static_cast *>(slot); + if (!vp) { + return; + } + + double par = vp->pixel_aspect_ratio().to_double(); + x_size = vp->width() * par; + y_size = vp->height(); +} + +inline double to_normalised(double canonical, double extent) +{ + return extent > 0 ? canonical / extent : canonical; +} + +inline double to_canonical(double normalised, double extent) +{ + return extent > 0 ? normalised * extent : normalised; +} + +inline std::string +param_change_label(const OFX::Host::Param::Descriptor &descriptor) +{ + return "Change " + descriptor.getName(); +} + +/** + * @brief Route an undo command to the node's plugin instance (edit + * batching) or run it immediately when unbound. + */ +void submit_undo_command(OakNodeNode node, OakUndoCommand command, + const std::string &label); + +class NodeBoundParam { +public: + virtual ~NodeBoundParam() = default; + virtual void set_node(OakNodeNode node) = 0; +}; + +namespace detail +{ + +inline oaknode_value value_int(int64_t v) +{ + oaknode_value value = {}; + value.type = OAKNODE_VALUE_INT; + value.num = v; + return value; +} + +inline oaknode_value value_bool(bool v) +{ + oaknode_value value = {}; + value.type = OAKNODE_VALUE_BOOL; + value.num = v ? 1 : 0; + return value; +} + +inline oaknode_value value_double(double v) +{ + oaknode_value value = {}; + value.type = OAKNODE_VALUE_FLOAT; + value.f[0] = v; + return value; +} + +inline oaknode_value value_vec(double x, double y) +{ + oaknode_value value = {}; + value.type = OAKNODE_VALUE_VEC2; + value.f[0] = x; + value.f[1] = y; + return value; +} + +inline oaknode_value value_vec(double x, double y, double z) +{ + oaknode_value value = {}; + value.type = OAKNODE_VALUE_VEC3; + value.f[0] = x; + value.f[1] = y; + value.f[2] = z; + return value; +} + +inline oaknode_value value_vec(double x, double y, double z, double w) +{ + oaknode_value value = {}; + value.type = OAKNODE_VALUE_VEC4; + value.f[0] = x; + value.f[1] = y; + value.f[2] = z; + value.f[3] = w; + return value; +} + +inline oaknode_value value_color(double r, double g, double b, double a) +{ + oaknode_value value = {}; + value.type = OAKNODE_VALUE_COLOR; + value.f[0] = r; + value.f[1] = g; + value.f[2] = b; + value.f[3] = a; + return value; +} + +/** + * @brief Set an input instantly with an undo command and submit it + */ +inline OfxStatus node_set(OakNodeNode node, const std::string &id, + const oaknode_value &value, + const std::string &label) +{ + OakUndoCommand command = {}; + if (oaknode_node_set_input_undoable(node, id.c_str(), &value, + &command) != OAKNODE_OK) { + return kOfxStatErrValue; + } + submit_undo_command(node, command, label); + return kOfxStatOK; +} + +/** + * @brief Set an input at a time (keyframe logic), one command per call + */ +inline OfxStatus node_set_at(OakNodeNode node, const std::string &id, + OfxTime time, const oaknode_value &value, + int track, const std::string &label) +{ + olive::core::Rational t = olive::core::Rational::from_double(time); + OakUndoCommand command = {}; + if (oaknode_node_set_input_at_time_undoable(node, id.c_str(), + t.numerator(), + t.denominator(), &value, + track, + &command) != OAKNODE_OK) { + return kOfxStatErrValue; + } + submit_undo_command(node, command, label); + return kOfxStatOK; +} + +/** + * @brief Set multiple tracks at a time, batched into one undo command + */ +inline OfxStatus node_set_at_multi(OakNodeNode node, const std::string &id, + OfxTime time, const oaknode_value &value, + int track_count, + const std::string &label) +{ + olive::core::Rational t = olive::core::Rational::from_double(time); + OakUndoCommand multi = oakundo_command_init_multi(); + if (!multi.ctx) { + return kOfxStatErrMemory; + } + for (int track = 0; track < track_count; track++) { + if (oaknode_node_set_input_at_time_into(node, id.c_str(), + t.numerator(), + t.denominator(), &value, + track, multi) != OAKNODE_OK) { + oakundo_command_free(&multi); + return kOfxStatErrValue; + } + } + submit_undo_command(node, multi, label); + return kOfxStatOK; +} + +inline bool node_get(OakNodeNode node, const std::string &id, + oaknode_value *out) +{ + return oaknode_node_get_input(node, id.c_str(), out) == OAKNODE_OK; +} + +inline bool node_get_at(OakNodeNode node, const std::string &id, + OfxTime time, oaknode_value *out) +{ + olive::core::Rational t = olive::core::Rational::from_double(time); + return oaknode_node_get_input_at_time(node, id.c_str(), t.numerator(), + t.denominator(), + out) == OAKNODE_OK; +} + +} // namespace detail + +class PushbuttonInstance : public OFX::Host::Param::PushbuttonInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor *descriptor_; + +public: + PushbuttonInstance(OakNodeNode effect, const std::string &name, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::PushbuttonInstance(descriptor, param_set) + , node_(effect) + { + (void)name; + descriptor_ = &descriptor; + }; + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } +}; + +class IntegerInstance : public OFX::Host::Param::IntegerInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor &descriptor_; + std::string id_; + mutable std::mutex no_node_mutex_; + bool has_value_ = false; + int value_ = 0; + +public: + IntegerInstance(OakNodeNode node, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::IntegerInstance(descriptor, param_set) + , node_(node) + , descriptor_(descriptor) + , id_(descriptor_.getName()) + { + try { + value_ = descriptor_.getProperties().getIntProperty( + kOfxParamPropDefault); + has_value_ = true; + } catch (...) { + value_ = 0; + has_value_ = false; + } + } + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } + OfxStatus get(int &a) override + { + if (!node_.ctx) { + std::lock_guard lock(no_node_mutex_); + a = has_value_ ? value_ : 0; + return kOfxStatOK; + } + if (id_.empty()) { + return kOfxStatErrBadHandle; + } + oaknode_value value = {}; + if (detail::node_get(node_, id_, &value)) { + a = int(value.num); + return kOfxStatOK; + } + a = 0; + return kOfxStatErrValue; + } + OfxStatus get(OfxTime time, int &data) override + { + if (!node_.ctx) { + std::lock_guard lock(no_node_mutex_); + data = has_value_ ? value_ : 0; + return kOfxStatOK; + } + if (id_.empty()) { + return kOfxStatErrBadHandle; + } + oaknode_value value = {}; + if (detail::node_get_at(node_, id_, time, &value)) { + data = int(value.num); + return kOfxStatOK; + } + data = 0; + return kOfxStatErrValue; + } + OfxStatus set(int data) override + { + if (!node_.ctx) { + std::lock_guard lock(no_node_mutex_); + value_ = data; + has_value_ = true; + return kOfxStatOK; + } + return detail::node_set(node_, id_, detail::value_int(data), + param_change_label(descriptor_)); + } + OfxStatus set(OfxTime time, int data) override + { + if (!node_.ctx) { + std::lock_guard lock(no_node_mutex_); + value_ = data; + has_value_ = true; + return kOfxStatOK; + } + return detail::node_set_at(node_, id_, time, + detail::value_int(data), 0, + param_change_label(descriptor_)); + } +}; + +class DoubleInstance : public OFX::Host::Param::DoubleInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor &descriptor_; + bool has_value_ = false; + double value_ = 0.0; + +public: + DoubleInstance(OakNodeNode effect, const std::string &name, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::DoubleInstance(descriptor, param_set) + , node_(effect) + , descriptor_(descriptor) + { + (void)name; + try { + value_ = descriptor_.getProperties().getDoubleProperty( + kOfxParamPropDefault); + has_value_ = true; + } catch (...) { + value_ = 0.0; + has_value_ = false; + } + } + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } + OfxStatus get(double &data) override + { + if (!node_.ctx) { + data = has_value_ ? value_ : 0.0; + return kOfxStatOK; + } + oaknode_value value = {}; + if (detail::node_get(node_, descriptor_.getName(), &value)) { + data = value.f[0]; + if (is_normalised_coordinate_system(descriptor_)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + data = to_normalised(data, x_size); + } + return kOfxStatOK; + } + data = 0.0; + return kOfxStatErrValue; + } + OfxStatus get(OfxTime time, double &data) override + { + if (!node_.ctx) { + data = has_value_ ? value_ : 0.0; + return kOfxStatOK; + } + oaknode_value value = {}; + if (detail::node_get_at(node_, descriptor_.getName(), time, + &value)) { + data = value.f[0]; + if (is_normalised_coordinate_system(descriptor_)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + data = to_normalised(data, x_size); + } + return kOfxStatOK; + } + data = 0.0; + return kOfxStatErrValue; + } + OfxStatus set(double data) override + { + if (!node_.ctx) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } + double val = data; + if (is_normalised_coordinate_system(descriptor_)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + val = to_canonical(val, x_size); + } + return detail::node_set(node_, descriptor_.getName(), + detail::value_double(val), + param_change_label(descriptor_)); + } + OfxStatus set(OfxTime time, double data) override + { + if (!node_.ctx) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } + double val = data; + if (is_normalised_coordinate_system(descriptor_)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + val = to_canonical(val, x_size); + } + return detail::node_set_at(node_, descriptor_.getName(), time, + detail::value_double(val), 0, + param_change_label(descriptor_)); + } + OfxStatus derive(OfxTime, double &) override + { + return kOfxStatErrUnsupported; + } + OfxStatus integrate(OfxTime, OfxTime, double &) override + { + return kOfxStatErrUnsupported; + } +}; + +class BooleanInstance : public OFX::Host::Param::BooleanInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor &descriptor_; + bool has_value_ = false; + bool value_ = false; + bool default_value() const + { + return descriptor_.getProperties().getIntProperty( + kOfxParamPropDefault) != 0; + } + +public: + BooleanInstance(OakNodeNode effect, const std::string &name, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::BooleanInstance(descriptor, param_set) + , node_(effect) + , descriptor_(descriptor) + { + (void)name; + value_ = default_value(); + has_value_ = true; + } + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } + OfxStatus get(bool &data) override + { + if (!node_.ctx) { + data = has_value_ ? value_ : false; + return kOfxStatOK; + } + oaknode_value value = {}; + if (detail::node_get(node_, descriptor_.getName(), &value)) { + data = value.num != 0; + return kOfxStatOK; + } + data = default_value(); + return kOfxStatOK; + } + OfxStatus get(OfxTime time, bool &data) override + { + if (!node_.ctx) { + data = has_value_ ? value_ : false; + return kOfxStatOK; + } + oaknode_value value = {}; + if (detail::node_get_at(node_, descriptor_.getName(), time, + &value)) { + data = value.num != 0; + return kOfxStatOK; + } + data = default_value(); + return kOfxStatOK; + } + OfxStatus set(bool data) override + { + if (!node_.ctx) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } + return detail::node_set(node_, descriptor_.getName(), + detail::value_bool(data), + param_change_label(descriptor_)); + } + OfxStatus set(OfxTime time, bool data) override + { + if (!node_.ctx) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } + return detail::node_set_at(node_, descriptor_.getName(), time, + detail::value_bool(data), 0, + param_change_label(descriptor_)); + } +}; + +class ChoiceInstance : public OFX::Host::Param::ChoiceInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor &descriptor_; + bool has_value_ = false; + int value_ = 0; + +public: + ChoiceInstance(OakNodeNode effect, const std::string &name, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::ChoiceInstance(descriptor, param_set) + , node_(effect) + , descriptor_(descriptor) + { + (void)name; + try { + value_ = descriptor_.getProperties().getIntProperty( + kOfxParamPropDefault); + has_value_ = true; + } catch (...) { + value_ = 0; + has_value_ = false; + } + } + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } + OfxStatus get(int &data) override + { + if (!node_.ctx) { + data = has_value_ ? value_ : 0; + return kOfxStatOK; + } + oaknode_value value = {}; + if (detail::node_get(node_, descriptor_.getName(), &value)) { + data = int(value.num); + return kOfxStatOK; + } + data = 0; + return kOfxStatErrValue; + } + OfxStatus get(OfxTime time, int &data) override + { + if (!node_.ctx) { + data = has_value_ ? value_ : 0; + return kOfxStatOK; + } + oaknode_value value = {}; + if (detail::node_get_at(node_, descriptor_.getName(), time, + &value)) { + data = int(value.num); + return kOfxStatOK; + } + data = 0; + return kOfxStatErrValue; + } + OfxStatus set(int data) override + { + if (!node_.ctx) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } + oaknode_value value = detail::value_int(data); + value.type = OAKNODE_VALUE_COMBO; + return detail::node_set(node_, descriptor_.getName(), value, + param_change_label(descriptor_)); + } + OfxStatus set(OfxTime time, int data) override + { + if (!node_.ctx) { + value_ = data; + has_value_ = true; + return kOfxStatOK; + } + oaknode_value value = detail::value_int(data); + value.type = OAKNODE_VALUE_COMBO; + return detail::node_set_at(node_, descriptor_.getName(), time, + value, 0, param_change_label(descriptor_)); + } +}; + +class RGBAInstance : public OFX::Host::Param::RGBAInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor &descriptor_; + bool has_value_ = false; + double value_[4] = { 0.0, 0.0, 0.0, 0.0 }; + +public: + RGBAInstance(OakNodeNode effect, const std::string &name, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::RGBAInstance(descriptor, param_set) + , node_(effect) + , descriptor_(descriptor) + { + (void)name; + } + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } + OfxStatus get(double &r, double &g, double &b, double &a) override + { + if (!node_.ctx) { + if (has_value_) { + r = value_[0]; + g = value_[1]; + b = value_[2]; + a = value_[3]; + } else { + r = g = b = a = 0.0; + } + return kOfxStatOK; + } + oaknode_value value = {}; + detail::node_get(node_, descriptor_.getName(), &value); + r = value.f[0]; + g = value.f[1]; + b = value.f[2]; + a = value.f[3]; + return kOfxStatOK; + } + OfxStatus get(OfxTime time, double &r, double &g, double &b, + double &a) override + { + if (!node_.ctx) { + if (has_value_) { + r = value_[0]; + g = value_[1]; + b = value_[2]; + a = value_[3]; + } else { + r = g = b = a = 0.0; + } + return kOfxStatOK; + } + oaknode_value value = {}; + detail::node_get_at(node_, descriptor_.getName(), time, &value); + r = value.f[0]; + g = value.f[1]; + b = value.f[2]; + a = value.f[3]; + return kOfxStatOK; + } + OfxStatus set(double r, double g, double b, double a) override + { + if (!node_.ctx) { + value_[0] = r; + value_[1] = g; + value_[2] = b; + value_[3] = a; + has_value_ = true; + return kOfxStatOK; + } + return detail::node_set(node_, descriptor_.getName(), + detail::value_color(r, g, b, a), + param_change_label(descriptor_)); + } + OfxStatus set(OfxTime time, double r, double g, double b, + double a) override + { + if (!node_.ctx) { + value_[0] = r; + value_[1] = g; + value_[2] = b; + value_[3] = a; + has_value_ = true; + return kOfxStatOK; + } + return detail::node_set_at_multi(node_, descriptor_.getName(), time, + detail::value_color(r, g, b, a), 4, + param_change_label(descriptor_)); + } +}; + +class RGBInstance : public OFX::Host::Param::RGBInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor &descriptor_; + bool has_value_ = false; + double value_[3] = { 0.0, 0.0, 0.0 }; + +public: + RGBInstance(OakNodeNode effect, const std::string &name, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::RGBInstance(descriptor, param_set) + , node_(effect) + , descriptor_(descriptor) + { + (void)name; + } + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } + OfxStatus get(double &r, double &g, double &b) override + { + if (!node_.ctx) { + if (has_value_) { + r = value_[0]; + g = value_[1]; + b = value_[2]; + } else { + r = g = b = 0.0; + } + return kOfxStatOK; + } + oaknode_value value = {}; + detail::node_get(node_, descriptor_.getName(), &value); + r = value.f[0]; + g = value.f[1]; + b = value.f[2]; + return kOfxStatOK; + } + OfxStatus get(OfxTime time, double &r, double &g, double &b) override + { + if (!node_.ctx) { + if (has_value_) { + r = value_[0]; + g = value_[1]; + b = value_[2]; + } else { + r = g = b = 0.0; + } + return kOfxStatOK; + } + oaknode_value value = {}; + detail::node_get_at(node_, descriptor_.getName(), time, &value); + r = value.f[0]; + g = value.f[1]; + b = value.f[2]; + return kOfxStatOK; + } + OfxStatus set(double r, double g, double b) override + { + if (!node_.ctx) { + value_[0] = r; + value_[1] = g; + value_[2] = b; + has_value_ = true; + return kOfxStatOK; + } + return detail::node_set(node_, descriptor_.getName(), + detail::value_color(r, g, b, 1.0), + param_change_label(descriptor_)); + } + OfxStatus set(OfxTime time, double r, double g, double b) override + { + if (!node_.ctx) { + value_[0] = r; + value_[1] = g; + value_[2] = b; + has_value_ = true; + return kOfxStatOK; + } + return detail::node_set_at_multi(node_, descriptor_.getName(), time, + detail::value_color(r, g, b, 1.0), + 3, param_change_label(descriptor_)); + } +}; + +class Double2DInstance : public OFX::Host::Param::Double2DInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor &descriptor_; + bool has_value_ = false; + double value_[2] = { 0.0, 0.0 }; + +public: + Double2DInstance(OakNodeNode effect, const std::string &name, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::Double2DInstance(descriptor, param_set) + , node_(effect) + , descriptor_(descriptor) + { + (void)name; + } + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } + OfxStatus get(double &x, double &y) override + { + if (!node_.ctx) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + } else { + x = y = 0.0; + } + return kOfxStatOK; + } + oaknode_value value = {}; + detail::node_get(node_, descriptor_.getName(), &value); + x = value.f[0]; + y = value.f[1]; + if (is_normalised_coordinate_system(descriptor_)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + x = to_normalised(x, x_size); + y = to_normalised(y, y_size); + } + return kOfxStatOK; + } + OfxStatus get(OfxTime time, double &x, double &y) override + { + if (!node_.ctx) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + } else { + x = y = 0.0; + } + return kOfxStatOK; + } + oaknode_value value = {}; + detail::node_get_at(node_, descriptor_.getName(), time, &value); + x = value.f[0]; + y = value.f[1]; + if (is_normalised_coordinate_system(descriptor_)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + x = to_normalised(x, x_size); + y = to_normalised(y, y_size); + } + return kOfxStatOK; + } + OfxStatus set(double x, double y) override + { + if (!node_.ctx) { + value_[0] = x; + value_[1] = y; + has_value_ = true; + return kOfxStatOK; + } + double xv = x, yv = y; + if (is_normalised_coordinate_system(descriptor_)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + xv = to_canonical(xv, x_size); + yv = to_canonical(yv, y_size); + } + return detail::node_set(node_, descriptor_.getName(), + detail::value_vec(xv, yv), + param_change_label(descriptor_)); + } + OfxStatus set(OfxTime time, double x, double y) override + { + if (!node_.ctx) { + value_[0] = x; + value_[1] = y; + has_value_ = true; + return kOfxStatOK; + } + double xv = x, yv = y; + if (is_normalised_coordinate_system(descriptor_)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + xv = to_canonical(xv, x_size); + yv = to_canonical(yv, y_size); + } + return detail::node_set_at_multi(node_, descriptor_.getName(), time, + detail::value_vec(xv, yv), 2, + param_change_label(descriptor_)); + } +}; + +class Integer2DInstance : public OFX::Host::Param::Integer2DInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor &descriptor_; + bool has_value_ = false; + int value_[2] = { 0, 0 }; + +public: + Integer2DInstance(OakNodeNode effect, const std::string &name, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::Integer2DInstance(descriptor, param_set) + , node_(effect) + , descriptor_(descriptor) + { + (void)name; + } + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } + OfxStatus get(int &x, int &y) override + { + if (!node_.ctx) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + } else { + x = y = 0; + } + return kOfxStatOK; + } + oaknode_value value = {}; + detail::node_get(node_, descriptor_.getName(), &value); + x = int(value.f[0]); + y = int(value.f[1]); + return kOfxStatOK; + } + OfxStatus get(OfxTime time, int &x, int &y) override + { + if (!node_.ctx) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + } else { + x = y = 0; + } + return kOfxStatOK; + } + oaknode_value value = {}; + detail::node_get_at(node_, descriptor_.getName(), time, &value); + x = int(value.f[0]); + y = int(value.f[1]); + return kOfxStatOK; + } + OfxStatus set(int x, int y) override + { + if (!node_.ctx) { + value_[0] = x; + value_[1] = y; + has_value_ = true; + return kOfxStatOK; + } + return detail::node_set(node_, descriptor_.getName(), + detail::value_vec(double(x), double(y)), + param_change_label(descriptor_)); + } + OfxStatus set(OfxTime time, int x, int y) override + { + if (!node_.ctx) { + value_[0] = x; + value_[1] = y; + has_value_ = true; + return kOfxStatOK; + } + return detail::node_set_at_multi(node_, descriptor_.getName(), time, + detail::value_vec(double(x), + double(y)), + 2, param_change_label(descriptor_)); + } +}; + +class Double3DInstance : public OFX::Host::Param::Double3DInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor &descriptor_; + bool has_value_ = false; + double value_[3] = { 0.0, 0.0, 0.0 }; + +public: + Double3DInstance(OakNodeNode effect, const std::string &name, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::Double3DInstance(descriptor, param_set) + , node_(effect) + , descriptor_(descriptor) + { + (void)name; + } + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } + OfxStatus get(double &x, double &y, double &z) override + { + if (!node_.ctx) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + z = value_[2]; + } else { + x = y = z = 0.0; + } + return kOfxStatOK; + } + oaknode_value value = {}; + detail::node_get(node_, descriptor_.getName(), &value); + x = value.f[0]; + y = value.f[1]; + z = value.f[2]; + if (is_normalised_coordinate_system(descriptor_)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + x = to_normalised(x, x_size); + y = to_normalised(y, y_size); + z = to_normalised(z, x_size); + } + return kOfxStatOK; + } + OfxStatus get(OfxTime time, double &x, double &y, double &z) override + { + if (!node_.ctx) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + z = value_[2]; + } else { + x = y = z = 0.0; + } + return kOfxStatOK; + } + oaknode_value value = {}; + detail::node_get_at(node_, descriptor_.getName(), time, &value); + x = value.f[0]; + y = value.f[1]; + z = value.f[2]; + if (is_normalised_coordinate_system(descriptor_)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + x = to_normalised(x, x_size); + y = to_normalised(y, y_size); + z = to_normalised(z, x_size); + } + return kOfxStatOK; + } + OfxStatus set(double x, double y, double z) override + { + if (!node_.ctx) { + value_[0] = x; + value_[1] = y; + value_[2] = z; + has_value_ = true; + return kOfxStatOK; + } + double xv = x, yv = y, zv = z; + if (is_normalised_coordinate_system(descriptor_)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + xv = to_canonical(xv, x_size); + yv = to_canonical(yv, y_size); + zv = to_canonical(zv, x_size); + } + return detail::node_set(node_, descriptor_.getName(), + detail::value_vec(xv, yv, zv), + param_change_label(descriptor_)); + } + OfxStatus set(OfxTime time, double x, double y, double z) override + { + if (!node_.ctx) { + value_[0] = x; + value_[1] = y; + value_[2] = z; + has_value_ = true; + return kOfxStatOK; + } + double xv = x, yv = y, zv = z; + if (is_normalised_coordinate_system(descriptor_)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + xv = to_canonical(xv, x_size); + yv = to_canonical(yv, y_size); + zv = to_canonical(zv, x_size); + } + return detail::node_set_at_multi(node_, descriptor_.getName(), time, + detail::value_vec(xv, yv, zv), 3, + param_change_label(descriptor_)); + } +}; + +class Integer3DInstance : public OFX::Host::Param::Integer3DInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor &descriptor_; + bool has_value_ = false; + int value_[3] = { 0, 0, 0 }; + +public: + Integer3DInstance(OakNodeNode effect, const std::string &name, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::Integer3DInstance(descriptor, param_set) + , node_(effect) + , descriptor_(descriptor) + { + (void)name; + } + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } + OfxStatus get(int &x, int &y, int &z) override + { + if (!node_.ctx) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + z = value_[2]; + } else { + x = y = z = 0; + } + return kOfxStatOK; + } + oaknode_value value = {}; + detail::node_get(node_, descriptor_.getName(), &value); + x = int(value.f[0]); + y = int(value.f[1]); + z = int(value.f[2]); + return kOfxStatOK; + } + OfxStatus get(OfxTime time, int &x, int &y, int &z) override + { + if (!node_.ctx) { + if (has_value_) { + x = value_[0]; + y = value_[1]; + z = value_[2]; + } else { + x = y = z = 0; + } + return kOfxStatOK; + } + oaknode_value value = {}; + detail::node_get_at(node_, descriptor_.getName(), time, &value); + x = int(value.f[0]); + y = int(value.f[1]); + z = int(value.f[2]); + return kOfxStatOK; + } + OfxStatus set(int x, int y, int z) override + { + if (!node_.ctx) { + value_[0] = x; + value_[1] = y; + value_[2] = z; + has_value_ = true; + return kOfxStatOK; + } + return detail::node_set(node_, descriptor_.getName(), + detail::value_vec(double(x), double(y), + double(z)), + param_change_label(descriptor_)); + } + OfxStatus set(OfxTime time, int x, int y, int z) override + { + if (!node_.ctx) { + value_[0] = x; + value_[1] = y; + value_[2] = z; + has_value_ = true; + return kOfxStatOK; + } + return detail::node_set_at_multi(node_, descriptor_.getName(), time, + detail::value_vec(double(x), + double(y), + double(z)), + 3, param_change_label(descriptor_)); + } +}; + +class StringInstance : public OFX::Host::Param::StringInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor &descriptor_; + bool has_value_ = false; + std::string value_; + +public: + StringInstance(OakNodeNode effect, const std::string &name, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::StringInstance(descriptor, param_set) + , node_(effect) + , descriptor_(descriptor) + { + (void)name; + try { + value_ = descriptor_.getProperties().getStringProperty( + kOfxParamPropDefault); + has_value_ = true; + } catch (...) { + value_.clear(); + has_value_ = false; + } + } + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } + OfxStatus get(std::string &data) override + { + if (!node_.ctx) { + data = has_value_ ? value_ : std::string(); + return kOfxStatOK; + } + int needed = oaknode_node_get_input_string( + node_, descriptor_.getName().c_str(), nullptr, 0); + if (needed > 0) { + data.resize(size_t(needed) - 1); + oaknode_node_get_input_string(node_, descriptor_.getName().c_str(), + data.data(), needed); + return kOfxStatOK; + } + data.clear(); + return kOfxStatErrValue; + } + OfxStatus get(OfxTime time, std::string &data) override + { + if (!node_.ctx) { + data = has_value_ ? value_ : std::string(); + return kOfxStatOK; + } + // String inputs are not keyframable; the standard value is the + // value at any time + (void)time; + return get(data); + } + OfxStatus set(const char *data) override + { + if (!node_.ctx) { + value_ = data ? data : ""; + has_value_ = true; + return kOfxStatOK; + } + OakUndoCommand command = {}; + if (oaknode_node_set_input_string_undoable( + node_, descriptor_.getName().c_str(), data ? data : "", + &command) != OAKNODE_OK) { + return kOfxStatErrValue; + } + submit_undo_command(node_, command, + param_change_label(descriptor_)); + return kOfxStatOK; + } + OfxStatus set(OfxTime time, const char *data) override + { + (void)time; + return set(data); + } +}; + +class CustomInstance : public OFX::Host::Param::CustomInstance, + public NodeBoundParam { +protected: + OakNodeNode node_ = {}; + OFX::Host::Param::Descriptor &descriptor_; + bool has_value_ = false; + std::string value_; + +public: + CustomInstance(OakNodeNode effect, const std::string &name, + OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::CustomInstance(descriptor, param_set) + , node_(effect) + , descriptor_(descriptor) + { + (void)name; + } + void set_node(OakNodeNode new_node) override + { + node_ = new_node; + } + OfxStatus get(std::string &data) override + { + if (!node_.ctx) { + data = has_value_ ? value_ : std::string(); + return kOfxStatOK; + } + int needed = oaknode_node_get_input_string( + node_, descriptor_.getName().c_str(), nullptr, 0); + if (needed > 0) { + data.resize(size_t(needed) - 1); + oaknode_node_get_input_string(node_, descriptor_.getName().c_str(), + data.data(), needed); + return kOfxStatOK; + } + data.clear(); + return kOfxStatErrValue; + } + OfxStatus get(OfxTime time, std::string &data) override + { + (void)time; + return get(data); + } + OfxStatus set(const char *data) override + { + if (!node_.ctx) { + value_ = data ? data : ""; + has_value_ = true; + return kOfxStatOK; + } + OakUndoCommand command = {}; + if (oaknode_node_set_input_string_undoable( + node_, descriptor_.getName().c_str(), data ? data : "", + &command) != OAKNODE_OK) { + return kOfxStatErrValue; + } + submit_undo_command(node_, command, + param_change_label(descriptor_)); + return kOfxStatOK; + } + OfxStatus set(OfxTime time, const char *data) override + { + (void)time; + return set(data); + } +}; + +class GroupInstance : public OFX::Host::Param::GroupInstance { +public: + GroupInstance(OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::GroupInstance(descriptor, param_set) + { + } +}; + +class PageInstance : public OFX::Host::Param::PageInstance { +public: + PageInstance(OFX::Host::Param::Descriptor &descriptor, + OFX::Host::Param::SetInstance *param_set = nullptr) + : OFX::Host::Param::PageInstance(descriptor, param_set) + { + } +}; +} +} + +#endif // OAK_PARAM_INSTANCE_H diff --git a/src/plugin/src/pluginprogressreporter.cpp b/src/plugin/src/pluginprogressreporter.cpp new file mode 100644 index 000000000..fdb073e3d --- /dev/null +++ b/src/plugin/src/pluginprogressreporter.cpp @@ -0,0 +1,70 @@ +/* + * 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 "pluginprogressreporter.h" + +namespace olive +{ +namespace plugin +{ +namespace +{ + +/** + * @brief No-op reporter used when no UI factory is registered + * + * Never reports cancellation, so processing always continues. + */ +class NullPluginProgressReporter : public PluginProgressReporter { +public: + void set_progress(double value) override + { + (void)value; + } + + void show() override + { + } + + void close() override + { + } +}; + +PluginProgressReporterFactory reporter_factory_; + +} + +void set_plugin_progress_reporter_factory( + PluginProgressReporterFactory factory) +{ + reporter_factory_ = std::move(factory); +} + +PluginProgressReporter * +create_plugin_progress_reporter(const std::string &message, + const std::string &title) +{ + if (reporter_factory_) { + return reporter_factory_(message, title); + } + + return new NullPluginProgressReporter(); +} + +} +} diff --git a/src/plugin/src/pluginprogressreporter.h b/src/plugin/src/pluginprogressreporter.h new file mode 100644 index 000000000..d75fc7c65 --- /dev/null +++ b/src/plugin/src/pluginprogressreporter.h @@ -0,0 +1,113 @@ +/* + * 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_PLUGIN_PROGRESS_REPORTER_H +#define OAK_PLUGIN_PROGRESS_REPORTER_H + +#include +#include + +namespace olive +{ +namespace plugin +{ + +/** + * @brief UI-independent interface for reporting plugin progress + * + * The engine cannot show UI itself, so OFX progress reporting goes through + * this interface. The UI layer registers a factory (see + * set_plugin_progress_reporter_factory()) that creates a reporter wrapping a + * ProgressDialog; without a factory, a no-op reporter is used instead. + * + * Cancellation is delivered through a C-style callback. + */ +class PluginProgressReporter { +public: + PluginProgressReporter() = default; + + virtual ~PluginProgressReporter() = default; + + virtual void set_progress(double value) = 0; + + virtual void show() = 0; + + virtual void close() = 0; + + /** + * @brief Register a callback to be invoked when the user cancels. + * + * Only one callback is supported; subsequent calls replace the + * previous registration. Pass nullptr to clear. + */ + void set_cancel_callback(std::function cb, void *userdata) + { + cancel_callback_ = std::move(cb); + cancel_callback_userdata_ = userdata; + } + + /** + * @brief Mark this reporter as cancelled and notify the registered + * callback. + */ + void set_cancelled() + { + cancelled_ = true; + if (cancel_callback_) { + cancel_callback_(cancel_callback_userdata_); + } + } + + bool cancelled() const + { + return cancelled_; + } + +private: + std::function cancel_callback_; + void *cancel_callback_userdata_ = nullptr; + bool cancelled_ = false; +}; + +/** + * @brief Factory creating a PluginProgressReporter for a progress session + * + * Registered by the UI layer at startup. The caller takes ownership of the + * returned reporter. + */ +using PluginProgressReporterFactory = + std::function; + +void set_plugin_progress_reporter_factory( + PluginProgressReporterFactory factory); + +/** + * @brief Create a progress reporter through the registered factory + * + * Without a factory, returns a no-op reporter so engine code can run + * headless. The caller takes ownership of the returned reporter. + */ +PluginProgressReporter * +create_plugin_progress_reporter(const std::string &message, + const std::string &title); + +} +} + +#endif // OAK_PLUGIN_PROGRESS_REPORTER_H diff --git a/src/plugin/standalone/CMakeLists.txt b/src/plugin/standalone/CMakeLists.txt new file mode 100644 index 000000000..e17f2c005 --- /dev/null +++ b/src/plugin/standalone/CMakeLists.txt @@ -0,0 +1,168 @@ +# 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 . + +cmake_minimum_required(VERSION 3.16) +project(oakplugin-standalone CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# oakplugin manipulates node graphs through the oaknode C ABI; the +# not-yet-split engine modules' symbols are allowed to dangle, resolved +# by transitional stubs in src/{node,render}/transition. +set(OAK_REPO_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../../..) +list(APPEND CMAKE_MODULE_PATH "${OAK_REPO_ROOT}/cmake") + +set(BUILD_TESTS ON CACHE BOOL "" FORCE) +set(OLIVECORE_BUILD_TESTS OFF CACHE BOOL "" FORCE) +# The /opt/otio OpenTimelineIO dylibs use @loader_path install names that +# break when linked from a build tree; oakplugin does not need OTIO. +set(CMAKE_DISABLE_FIND_PACKAGE_OpenTimelineIO ON) + +find_package(EXPAT REQUIRED) +find_package(OpenColorIO CONFIG REQUIRED) +find_package(OpenImageIO CONFIG REQUIRED) +set(OCIO_LIBRARIES OpenColorIO::OpenColorIO) +set(OCIO_INCLUDE_DIRS ${OpenColorIO_INCLUDE_DIRS}) +set(OIIO_LIBRARIES OpenImageIO::OpenImageIO) +set(OIIO_INCLUDE_DIRS ${OpenImageIO_INCLUDE_DIRS}) + +add_subdirectory(${OAK_REPO_ROOT}/core ${CMAKE_BINARY_DIR}/core) +target_include_directories(olivecore PUBLIC + ${OAK_REPO_ROOT}/third_party/openfx/include +) +add_subdirectory(${OAK_REPO_ROOT}/ffmpeg_bridge ${CMAKE_BINARY_DIR}/ffmpeg_bridge) + +set(BUILD_TESTS OFF) +add_subdirectory(${OAK_REPO_ROOT}/src/undo ${CMAKE_BINARY_DIR}/undo) +add_subdirectory(${OAK_REPO_ROOT}/src/common ${CMAKE_BINARY_DIR}/common) +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) +set(BUILD_TESTS ON) + +enable_testing() +set(BUILD_TESTS OFF) +add_subdirectory(${OAK_REPO_ROOT}/src/timeline ${CMAKE_BINARY_DIR}/timeline) +add_subdirectory(${OAK_REPO_ROOT}/src/codec ${CMAKE_BINARY_DIR}/codec) +set(BUILD_TESTS ON) +add_subdirectory(${OAK_REPO_ROOT}/src/node ${CMAKE_BINARY_DIR}/node) +add_subdirectory(${OAK_REPO_ROOT}/src/plugin ${CMAKE_BINARY_DIR}/task) + +target_include_directories(oakcodec PUBLIC + ${OAK_REPO_ROOT}/src/render/transition + ${OAK_REPO_ROOT}/src/node/transition + ${OAK_REPO_ROOT}/src/render/src + ${OAK_REPO_ROOT}/src/node/src +) +foreach(t oakcodec oaktimeline) + if(TARGET ${t}) + target_link_options(${t} PRIVATE + "-undefined" "dynamic_lookup" + ) + endif() +endforeach() + +# oaknode needs its transition stubs and the oakrender real headers +# (src/node/transition/render/* bridge there); src/render/transition +# must precede src/node/transition. +target_include_directories(oaknode BEFORE PUBLIC + ${OAK_REPO_ROOT}/src/render/transition + ${OAK_REPO_ROOT}/src/node/transition + ${OAK_REPO_ROOT}/src/render/src +) +target_include_directories(oaknode PUBLIC + ${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include + /opt/homebrew/include + /opt/homebrew/include/Imath +) +target_link_options(oaknode PRIVATE + "-undefined" "dynamic_lookup" +) + +target_include_directories(oakrender BEFORE PUBLIC + ${OAK_REPO_ROOT}/src/render/transition + ${OAK_REPO_ROOT}/src/node/transition +) +target_include_directories(oakrender PUBLIC + ${OAK_REPO_ROOT}/engine/include + ${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include + /opt/homebrew/include + /opt/homebrew/include/Imath + /opt/homebrew/include/OpenEXR +) +foreach(t oakrender oakgl oakgl2 oakvulkan) + if(TARGET ${t}) + target_link_options(${t} PRIVATE + "-undefined" "dynamic_lookup" + ) + endif() +endforeach() + +target_include_directories(oakplugin PUBLIC + ${OAK_REPO_ROOT}/src/render/transition + ${OAK_REPO_ROOT}/src/node/transition + ${OAK_REPO_ROOT}/src/render/src + ${OAK_REPO_ROOT}/src/node/src +) +target_link_options(oakplugin PRIVATE + "-undefined" "dynamic_lookup" +) + +target_link_libraries(oakrender PRIVATE + oaknode + oakcommon + oakundo + olivecore + ffmpeg_bridge + ${OCIO_LIBRARIES} + ${OIIO_LIBRARIES} + "-framework OpenGL" + "-framework CoreVideo" + "-framework Metal" + "-framework QuartzCore" +) + +target_link_libraries(oaknode-gtest PRIVATE oakrender oaktimeline oakcodec) + +# liboakrender references the oakengine_ipc_* C ABI (worker IPC) via +# dynamic_lookup; the real implementation (engine/src/capi/ipc.cpp) is still +# Qt-based, so test binaries link an inert shim instead. +target_sources(oaknode-gtest PRIVATE + ${OAK_REPO_ROOT}/src/node/standalone/oakengine_ipc_shim.cpp +) +target_include_directories(oaknode-gtest PRIVATE + ${OAK_REPO_ROOT}/engine/include +) + + +# OFX typeinfo is referenced via dynamic_lookup; force-load the host +# support archive so RTTI resolves (same as oaknode's test wiring). +# oakplugin-gtest wires itself up in src/plugin/tests/CMakeLists.txt. +find_library(OAKNODE_OFX_HOST_ARCHIVE NAMES OfxHost + PATHS ${OAK_REPO_ROOT}/build/third_party/openfx/HostSupport + NO_DEFAULT_PATH) +if(NOT OAKNODE_OFX_HOST_ARCHIVE) + message(FATAL_ERROR + "libOfxHost.a not found; run the full-tree build once or set " + "OAKNODE_OFX_HOST_ARCHIVE") +endif() +target_link_options(oaknode-gtest PRIVATE + "-Wl,-force_load,${OAKNODE_OFX_HOST_ARCHIVE}") + +# TimelineAddTrackCommand, which lives in liboakplugin; the two +# libraries resolve each other at runtime (dynamic_lookup). +target_link_libraries(oaknode-gtest PRIVATE oaktimeline) diff --git a/src/plugin/tests/CMakeLists.txt b/src/plugin/tests/CMakeLists.txt new file mode 100644 index 000000000..cd9f58cea --- /dev/null +++ b/src/plugin/tests/CMakeLists.txt @@ -0,0 +1,65 @@ +# 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 . + +find_package(GTest REQUIRED) +include(GoogleTest) + +# In a full-tree build the repo root is CMAKE_SOURCE_DIR; a standalone +# build (see src/plugin/standalone) sets OAK_REPO_ROOT explicitly. +if(NOT DEFINED OAK_REPO_ROOT) + set(OAK_REPO_ROOT ${CMAKE_SOURCE_DIR}) +endif() + +add_executable(oakplugin-gtest + plugin_test.cpp +) + +target_link_libraries(oakplugin-gtest PRIVATE + oakplugin + oakrender + GTest::gtest + GTest::gtest_main +) + +# liboakrender/liboaknode dangle OFX host symbols (-undefined +# dynamic_lookup); force-load the host support archive into the test +# process so dyld finds them in the flat namespace at startup. Mirrors +# src/render/tests/CMakeLists.txt. +if(NOT DEFINED OAKRENDER_OFX_HOST_ARCHIVE) + find_library(OAKRENDER_OFX_HOST_ARCHIVE NAMES OfxHost + PATHS ${OAK_REPO_ROOT}/build/third_party/openfx/HostSupport) +endif() +if(NOT OAKRENDER_OFX_HOST_ARCHIVE) + message(FATAL_ERROR + "libOfxHost.a not found; run the full-tree build once or set " + "OAKRENDER_OFX_HOST_ARCHIVE") +endif() +target_link_options(oakplugin-gtest PRIVATE + "-Wl,-force_load,${OAKRENDER_OFX_HOST_ARCHIVE}") + +# liboakrender references the oakengine_ipc_* C ABI (worker IPC) via +# dynamic_lookup; the test binary links the inert shim from +# src/node/standalone instead. +target_sources(oakplugin-gtest PRIVATE + ${OAK_REPO_ROOT}/src/node/standalone/oakengine_ipc_shim.cpp) +target_include_directories(oakplugin-gtest PRIVATE + ${OAK_REPO_ROOT}/engine/include +) + +gtest_discover_tests(oakplugin-gtest + DISCOVERY_MODE PRE_TEST + PROPERTIES ENVIRONMENT + "OCIO=${OAK_REPO_ROOT}/engine/render/ocioconf/config.ocio") diff --git a/src/plugin/tests/plugin_test.cpp b/src/plugin/tests/plugin_test.cpp new file mode 100644 index 000000000..b0cbc5c61 --- /dev/null +++ b/src/plugin/tests/plugin_test.cpp @@ -0,0 +1,106 @@ +/*** + + 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 + +#include + +#include "plugin/host.h" +#include "plugin/instance.h" + +TEST(OakPluginHost, InitAndEnumerate) +{ + EXPECT_EQ(oakplugin_host_init(), OAKPLUGIN_OK); + + int count = oakplugin_host_plugin_count(); + EXPECT_GE(count, 0); + + char buf[256]; + EXPECT_EQ(oakplugin_host_plugin_id_at(-1, buf, sizeof(buf)), + OAKPLUGIN_E_NOT_FOUND); + EXPECT_EQ(oakplugin_host_plugin_id_at(count + 100, buf, sizeof(buf)), + OAKPLUGIN_E_NOT_FOUND); + EXPECT_EQ(oakplugin_host_plugin_label(nullptr, buf, sizeof(buf)), + OAKPLUGIN_E_INVALID); + + const char *dirs[] = { "/nonexistent/ofx/path" }; + EXPECT_EQ(oakplugin_host_scan(dirs, 1), OAKPLUGIN_OK); + EXPECT_EQ(oakplugin_host_scan(nullptr, 1), OAKPLUGIN_E_INVALID); + + oakplugin_host_shutdown(); +} + +TEST(OakPluginHost, MessageHandler) +{ + static int calls = 0; + calls = 0; + + oakplugin_host_set_message_handler( + [](const char *type, const char *message, void *userdata) -> int { + int *counter = static_cast(userdata); + (*counter)++; + EXPECT_STREQ(type, "Message"); + EXPECT_STRNE(message, ""); + return OAKPLUGIN_MESSAGE_ANSWER_NO; + }, + &calls); + + // No plugin loaded here; just verify registration does not crash and + // the default path works after clearing + oakplugin_host_set_message_handler(nullptr, nullptr); + SUCCEED(); +} + +TEST(OakPluginInstance, CreateUnknownIdFails) +{ + EXPECT_EQ(oakplugin_host_init(), OAKPLUGIN_OK); + + OakPluginInstance instance = + oakplugin_instance_create("com.example.nonexistent.plugin"); + EXPECT_EQ(instance.ctx, nullptr); + oakplugin_instance_free(&instance); // no-op on empty + + EXPECT_EQ(oakplugin_debug_alive_count(), 0); +} + +TEST(OakPluginInstance, InvalidArgs) +{ + OakPluginInstance empty = {}; + + oakplugin_instance_free(nullptr); // no-op + oakplugin_instance_free(&empty); // no-op + + oaknode_value value = {}; + EXPECT_EQ(oakplugin_instance_set_param(empty, "p", &value), + OAKPLUGIN_E_INVALID); + EXPECT_EQ(oakplugin_instance_get_param(empty, "p", &value), + OAKPLUGIN_E_INVALID); + EXPECT_EQ(oakplugin_instance_set_param_string(empty, "p", "x"), + OAKPLUGIN_E_INVALID); + EXPECT_EQ(oakplugin_instance_get_param_string(empty, "p", nullptr, 0), + OAKPLUGIN_E_INVALID); + + EXPECT_EQ(oakplugin_instance_render(empty, OakRenderTexture{}, + OakRenderTexture{}, 0.0), + OAKPLUGIN_E_INVALID); + EXPECT_EQ(oakplugin_instance_set_progress_cb(empty, nullptr, nullptr), + OAKPLUGIN_E_INVALID); + EXPECT_EQ(oakplugin_instance_cancel(empty), OAKPLUGIN_E_INVALID); +} diff --git a/src/render/c_api/internalhandles.h b/src/render/c_api/internalhandles.h index 492d3e5d4..249f7cae7 100644 --- a/src/render/c_api/internalhandles.h +++ b/src/render/c_api/internalhandles.h @@ -52,6 +52,8 @@ #include "alivecount.h" +#include "avframeptr.h" + #include "codec/frame.h" #include "colorprocessor.h" #include "texture.h" @@ -70,6 +72,7 @@ struct OakRenderTextureImpl { */ struct OakCodecFrameImpl { olive::FramePtr ptr; + olive::AVFramePtr avptr; /**< Set when wrapping a texture's CPU copy. */ }; /** diff --git a/src/render/c_api/renderer.cpp b/src/render/c_api/renderer.cpp index 1b231912b..062180747 100644 --- a/src/render/c_api/renderer.cpp +++ b/src/render/c_api/renderer.cpp @@ -363,6 +363,9 @@ void *oakrender_codec_frame_data(OakCodecFrame frame) if (!f || !f->ptr) { return nullptr; } + if (f->avptr) { + return f->avptr->data(0); + } return f->ptr->data(); } @@ -380,6 +383,9 @@ int oakrender_codec_frame_linesize_bytes(OakCodecFrame frame) { OakCodecFrameImpl *f = oakrender_c_api::to_native(frame); + if (f && f->avptr) { + return f->avptr->linesize(0); + } if (!f || !f->ptr) { return 0; } @@ -508,3 +514,84 @@ int oakrender_current_backend(char *buf, int n) } return write_string(g_requested_backend, buf, n); } + +int oakrender_display_texture_is_dummy(OakRenderTexture texture) +{ + if (!texture.ctx) { + return 0; + } + return oakrender_c_api::to_native(texture) + ->ptr->is_dummy() + ? 1 + : 0; +} + +int oakrender_display_texture_get_frame(OakRenderTexture texture, + OakCodecFrame *out) +{ + if (!texture.ctx || !out) { + return OAKRENDER_E_INVALID; + } + *out = OakCodecFrame{}; + + olive::AVFramePtr frame = + oakrender_c_api::to_native(texture)->ptr->frame(); + if (!frame) { + return OAKRENDER_E_NOT_FOUND; + } + // The texture's CPU copy is an AVFramePtr (ffmpeg_bridge RAII); wrap + // the shared_ptr so the frame stays alive with the handle. + auto *fimpl = new OakCodecFrameImpl; + fimpl->avptr = frame; + *out = oakrender_c_api::make_handle( + fimpl, true, &oakrender_c_api::delete_as); + return out->ctx ? OAKRENDER_OK : OAKRENDER_E_NOMEM; +} + +OakRenderTexture oakrender_display_texture_wrap_native( + const olive::TexturePtr &texture) +{ + if (!texture) { + return OakRenderTexture{}; + } + auto *impl = new OakRenderTextureImpl; + impl->ptr = texture; + return oakrender_c_api::make_handle( + impl, true, &oakrender_c_api::delete_as); +} + +int oakrender_codec_frame_width(OakCodecFrame frame) +{ + OakCodecFrameImpl *f = + oakrender_c_api::to_native(frame); + if (!f) { + return 0; + } + if (f->avptr) { + return f->avptr->width(); + } + return f->ptr ? f->ptr->width() : 0; +} + +int oakrender_codec_frame_height(OakCodecFrame frame) +{ + OakCodecFrameImpl *f = + oakrender_c_api::to_native(frame); + if (!f) { + return 0; + } + if (f->avptr) { + return f->avptr->height(); + } + return f->ptr ? f->ptr->height() : 0; +} + +int oakrender_codec_frame_fb_format(OakCodecFrame frame) +{ + OakCodecFrameImpl *f = + oakrender_c_api::to_native(frame); + if (!f || !f->avptr) { + return -1; + } + return f->avptr->format(); +} diff --git a/src/render/src/texture.h b/src/render/src/texture.h index fa7c1f1b6..87dfc096e 100644 --- a/src/render/src/texture.h +++ b/src/render/src/texture.h @@ -22,7 +22,7 @@ #ifndef OAK_RENDERTEXTURE_H #define OAK_RENDERTEXTURE_H -#include "common/avframeptr.h" +#include "avframeptr.h" #include #include diff --git a/src/render/standalone/CMakeLists.txt b/src/render/standalone/CMakeLists.txt index 0656b7f40..4946e25d7 100644 --- a/src/render/standalone/CMakeLists.txt +++ b/src/render/standalone/CMakeLists.txt @@ -154,3 +154,4 @@ if(BUILD_TESTS) enable_testing() add_subdirectory(${OAK_REPO_ROOT}/src/render/tests ${CMAKE_BINARY_DIR}/render_tests) endif() +add_subdirectory(${OAK_REPO_ROOT}/src/plugin ${CMAKE_BINARY_DIR}/plugin) diff --git a/src/render/tests/CMakeLists.txt b/src/render/tests/CMakeLists.txt index 69adc5485..22ac72f9f 100644 --- a/src/render/tests/CMakeLists.txt +++ b/src/render/tests/CMakeLists.txt @@ -28,6 +28,7 @@ target_link_libraries(oakrender-gtest PRIVATE oaktimeline oakrender oaknode + oakplugin oakcommon oakundo olivecore diff --git a/src/render/transition/common/avframeptr.h b/src/render/transition/common/avframeptr.h deleted file mode 100644 index eccb31423..000000000 --- a/src/render/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/task/standalone/CMakeLists.txt b/src/task/standalone/CMakeLists.txt index 298921309..9750dde3c 100644 --- a/src/task/standalone/CMakeLists.txt +++ b/src/task/standalone/CMakeLists.txt @@ -175,3 +175,4 @@ target_link_options(oaknode-gtest PRIVATE # TimelineAddTrackCommand, which lives in liboaktask; the two # libraries resolve each other at runtime (dynamic_lookup). target_link_libraries(oaknode-gtest PRIVATE oaktimeline) +add_subdirectory(${OAK_REPO_ROOT}/src/plugin ${CMAKE_BINARY_DIR}/plugin) diff --git a/src/task/tests/CMakeLists.txt b/src/task/tests/CMakeLists.txt index 5257b3dee..f1c54d06e 100644 --- a/src/task/tests/CMakeLists.txt +++ b/src/task/tests/CMakeLists.txt @@ -11,6 +11,7 @@ target_compile_definitions(oaktask-gtest PRIVATE target_link_libraries(oaktask-gtest PRIVATE oaktask oaktimeline + oakplugin GTest::gtest GTest::gtest_main ) diff --git a/src/timeline/standalone/CMakeLists.txt b/src/timeline/standalone/CMakeLists.txt index d09f2bc07..f3d892b86 100644 --- a/src/timeline/standalone/CMakeLists.txt +++ b/src/timeline/standalone/CMakeLists.txt @@ -51,6 +51,7 @@ set(BUILD_TESTS OFF) add_subdirectory(${OAK_REPO_ROOT}/src/undo ${CMAKE_BINARY_DIR}/undo) add_subdirectory(${OAK_REPO_ROOT}/src/common ${CMAKE_BINARY_DIR}/common) 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) set(BUILD_TESTS ON) enable_testing() @@ -156,3 +157,4 @@ target_link_options(oaknode-gtest PRIVATE # TimelineAddTrackCommand, which lives in liboaktimeline; the two # libraries resolve each other at runtime (dynamic_lookup). target_link_libraries(oaknode-gtest PRIVATE oaktimeline) +add_subdirectory(${OAK_REPO_ROOT}/src/plugin ${CMAKE_BINARY_DIR}/plugin) diff --git a/src/timeline/tests/CMakeLists.txt b/src/timeline/tests/CMakeLists.txt index b7f9d3906..351fa5a71 100644 --- a/src/timeline/tests/CMakeLists.txt +++ b/src/timeline/tests/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(oaktimeline-gtest target_link_libraries(oaktimeline-gtest PRIVATE oaktimeline + oakplugin GTest::gtest GTest::gtest_main )