Files
oak-editor/crates/oak-plugin/src/host.rs
T
Mike-Solar ea9b451d0b
CI / Build & test (Linux) (push) Successful in 18m55s
CI / Build & test (Windows) (push) Successful in 33m21s
plugin: instantiate more real-world plugins (FieldOrder, UIColour, context fallback)
Three independent host-side gaps kept real plugins from instantiating:

- clips now define OfxImageClipPropFieldOrder (default OfxFieldNone):
  ofxs Clip::getFieldOrder() is a strong read with no default, so
  field-aware plugins (Mirror) threw PropertyUnknownToHost ->
  MissingHostFeature from createInstance
- OfxParamPropParametricUIColour is no longer predefined as an empty
  array: the OFX implicit-create semantics let the plugin's first
  propSetDouble create the property and grow it index by index, while
  the empty predefined array rejected every write at index >= 1 with
  BadIndex (ColorLookup, HueCorrect describeInContext)
- Host::create_instance_preferred: context selection with fallback
  (filter -> general -> tracker -> paint -> rest). TrackerPM advertises
  the filter context but its createInstance fetches a Mask clip that
  its describe only defines for tracker/general/paint; Natron simply
  instantiates it in the tracker context, and now so do we. Both the
  node-registration scan and the shared instance factory use it.

Still unsupported, by design: Premult/Unpremult (this openfx-misc
build hard-requires the Nuke multi-plane suite + dynamic choices) and
the stereo view plugins (Switch/anaglyph/joinViews/etc. need Natron's
isNatron multi-clip folding or view rendering).

Verified: ColorLookup, HueCorrect, Mirror, TrackerPM now instantiate;
full oak-plugin suite green.
2026-08-25 21:56:33 +08:00

1663 lines
66 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 <http://www.gnu.org/licenses/>.
//! Host 单例与插件缓存。
//!
//! 对应 C++ 的 `OliveHost` + HostSupport 的 `PluginCache`。职责:
//! bundle 扫描、`dlopen` 插件、调 `setHost`/`load`/`describe`
//! 建立 [`Plugin`] 记录、按标识创建实例。参照:
//! HS: ofxhPluginCache.cpp(扫描去重与索引语义)、
//! HS: ofxhImageEffectAPI.cpp:150-238load/describe/
//! describeInContext 的 action 序列)。
//!
//! ## 扫描语义(对照 olivehost.cpp:118-191 + OFX 官方规范)
//!
//! 默认路径集按平台组织:
//! 用户级(`$HOME` 非空时)——`$HOME/.OFX/Plugins`、
//! `$HOME/.local/share/OFX/Plugins`、`$HOME/.local/share/olive/ofx/Plugins`
//! macOS 另含 `$HOME/Library/OFX/Plugins`);
//! 系统级——macOS `/Library/OFX/Plugins`Linux `/usr/OFX/Plugins`、
//! `/usr/local/OFX/Plugins`Windows `%ProgramFiles%\Common Files\OFX\Plugins`
//! `%ProgramFiles%` 未设置时回退 `C:\Program Files\...`);
//! Olive app-relative——`../OFX/Plugins`、`../share/olive/ofx/Plugins`、
//! `../lib/olive/ofx/Plugins`olivehost.cpp:104-107);
//! 以及 `OFX_PLUGIN_PATH`OFX 官方)与 `OLIVE_OFX_PLUGIN_PATH`/
//! `OLIVE_PLUGIN_PATH`Olive 扩展)三个环境变量(平台路径分隔符:
//! Unix ':' / Windows ';')。不存在的目录直接跳过;重复扫描按规范化
//! 路径去重。
//! `// [P2]`Info.plist 解析(CFBundleExecutable)不做——bundle 内
//! 二进制以探测方式定位(`Contents/MacOS/*`、`Contents/Linux-*/*`、
//! 根下 `*.so`/`*.dylib`)。
//!
//! ## dlopen(零依赖)
//!
//! 直接声明 `dlopen`/`dlsym`/`dlclose`macOS libSystem / Linux libc
//! 内建;RTLD 常量按平台取值)。加载顺序按 ofxCore.h:574
//! `OfxGetNumberOfPlugins` → `OfxGetPlugin`,随后
//! `OfxPlugin::setHost`mandatoryofxCore.h:124-132)→
//! `load` → `describe`HS: ofxhImageEffectAPI.cpp:158-190)。
use std::ffi::{c_char, c_int, c_uint, c_void, CStr, CString};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use crate::descriptor::EffectDescriptor;
use crate::handle::RefBox;
use crate::instance::Instance;
use crate::property::{PropertySet, Value};
use crate::suites::status;
// ---- dlopen FFI(零依赖;RTLD 常量按平台)----
#[cfg(target_os = "macos")]
const RTLD_NOW: c_int = 0x2;
#[cfg(target_os = "linux")]
const RTLD_NOW: c_int = 0x2;
#[cfg(target_os = "macos")]
const RTLD_LOCAL: c_int = 0x4;
#[cfg(target_os = "linux")]
const RTLD_LOCAL: c_int = 0x0;
// Windows has no dlopen/dlsym/dlclose: the POSIX FFI below is compiled
// out and replaced by LoadLibraryExW/GetProcAddress/FreeLibrary (see the
// `win32` module further down), keeping the same function signatures.
#[cfg(not(target_os = "windows"))]
extern "C" {
fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void;
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
fn dlclose(handle: *mut c_void) -> c_int;
}
#[cfg(not(target_os = "windows"))]
/// 动态加载共享库;失败返回 None。
fn dl_open(path: &Path) -> Option<*mut c_void> {
let c = CString::new(path.to_str()?).ok()?;
let h = unsafe { dlopen(c.as_ptr(), RTLD_NOW | RTLD_LOCAL) };
if h.is_null() {
None
} else {
Some(h)
}
}
#[cfg(not(target_os = "windows"))]
/// 查符号;失败返回 None。
fn dl_sym(handle: *mut c_void, name: &str) -> Option<*mut c_void> {
let c = CString::new(name).ok()?;
let p = unsafe { dlsym(handle, c.as_ptr()) };
if p.is_null() {
None
} else {
Some(p)
}
}
#[cfg(not(target_os = "windows"))]
/// 从 `dlsym` 结果取函数指针(libloading 同款转换;调用方保证符号
/// 类型正确)。
///
/// # Safety
/// 符号必须确实是该函数类型。
unsafe fn dlsym_fn<T>(handle: *mut c_void, name: &str) -> Option<T> {
let p = dl_sym(handle, name)?;
Some(unsafe { std::mem::transmute_copy(&p) })
}
// ---- Windows dynamic loading (kernel32) ----
//
// LoadLibraryExW / GetProcAddress / FreeLibrary in place of the POSIX
// dlopen family. The Windows handle is an HMODULE, which is pointer-sized
// and stored in the same `*mut c_void` slot, so the public signatures
// (`dl_open` / `dl_sym` / `dlsym_fn` / `dlclose`) are unchanged.
/// Windows dynamic loading (`LoadLibraryExW` / `GetProcAddress` /
/// `FreeLibrary`, kernel32). The handle is an `HMODULE` kept as
/// `*mut c_void` for signature parity with the POSIX path.
#[cfg(target_os = "windows")]
mod win32 {
use super::*;
use std::os::windows::ffi::OsStrExt;
/// `LOAD_WITH_ALTERED_SEARCH_PATH` (winbase.h): resolve the loaded
/// module's dependent DLLs from the module's own directory first, so a
/// plugin bundle's sibling DLLs are found next to the binary.
const LOAD_WITH_ALTERED_SEARCH_PATH: u32 = 0x0000_0008;
#[link(name = "kernel32")]
unsafe extern "C" {
fn LoadLibraryExW(
lp_file_name: *const u16,
h_file: *mut c_void,
dw_flags: u32,
) -> *mut c_void;
fn GetProcAddress(h_module: *mut c_void, lp_proc_name: *const c_char) -> *mut c_void;
fn FreeLibrary(h_module: *mut c_void) -> c_int;
}
/// Dynamically load a shared library from an arbitrary (possibly
/// non-UTF-8) path; `None` on failure. The path is converted to a
/// NUL-terminated UTF-16 string for `LoadLibraryExW`.
pub(super) fn dl_open(path: &Path) -> Option<*mut c_void> {
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let h = unsafe {
LoadLibraryExW(wide.as_ptr(), std::ptr::null_mut(), LOAD_WITH_ALTERED_SEARCH_PATH)
};
if h.is_null() {
None
} else {
Some(h)
}
}
/// Look up an exported symbol by name; `None` on failure. The symbol
/// name is a narrow (ANSI) string — `OfxGetNumberOfPlugins` /
/// `OfxGetPlugin`, both plain ASCII.
pub(super) fn dl_sym(handle: *mut c_void, name: &str) -> Option<*mut c_void> {
let c = CString::new(name).ok()?;
let p = unsafe { GetProcAddress(handle, c.as_ptr()) };
if p.is_null() {
None
} else {
Some(p)
}
}
/// Function pointer from a `GetProcAddress` result (same transmute as
/// the POSIX `dlsym_fn`; the caller guarantees the symbol type).
///
/// # Safety
/// The symbol must actually be that function type.
pub(super) unsafe fn dlsym_fn<T>(handle: *mut c_void, name: &str) -> Option<T> {
let p = dl_sym(handle, name)?;
Some(unsafe { std::mem::transmute_copy(&p) })
}
/// Unload a library (`FreeLibrary`; reference-counted like POSIX
/// `dlclose`).
pub(super) unsafe fn dlclose(handle: *mut c_void) -> c_int {
unsafe { FreeLibrary(handle) }
}
}
#[cfg(target_os = "windows")]
use win32::{dl_open, dl_sym, dlsym_fn, dlclose};
// ---- OFX 宿主侧结构(ofxCore.h,字段序与 SDK 逐字一致)----
/// `OfxHost`ofxCore.h:44)。
#[repr(C)]
struct OfxHost {
/// 宿主属性集句柄(`Host::props` 的裸指针,标签 0)。
host: *mut c_void,
/// fetchSuite。
fetch_suite: unsafe extern "C" fn(*mut c_void, *const c_char, c_int) -> *const c_void,
}
/// `OfxPlugin`ofxCore.h:96)。
#[repr(C)]
struct OfxPlugin {
plugin_api: *const c_char,
api_version: c_int,
plugin_identifier: *const c_char,
plugin_version_major: c_uint,
plugin_version_minor: c_uint,
set_host: Option<unsafe extern "C" fn(*mut OfxHost)>,
main_entry: Option<
unsafe extern "C" fn(*const c_char, *const c_void, *mut c_void, *mut c_void) -> c_int,
>,
}
/// fetchSuite 的宿主出口(插件经 `OfxHost::fetchSuite` 拿到;
/// 经 [`crate::suites::fetch_suite`] 分发表)。
unsafe extern "C" fn host_fetch_suite(
host: *mut c_void,
name: *const c_char,
version: c_int,
) -> *const c_void {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if host.is_null() || name.is_null() {
return None;
}
let name = unsafe { CStr::from_ptr(name) }.to_str().ok()?;
crate::suites::fetch_suite(name, version)
}));
match result {
Ok(Some(p)) => {
if std::env::var_os("OAK_OFX_TRACE").is_some() && !name.is_null() {
if let Ok(n) = unsafe { CStr::from_ptr(name) }.to_str() {
eprintln!("[ofx] fetchSuite hit: {n} v{version}");
}
}
p
}
Ok(None) => {
// 诊断:插件请求的 suite 宿主没有。厂商套件(Nuke/Vegas/
// Foundry)的探测是插件的正常行为,不打正式日志——需要
// 排查时用 OAK_OFX_TRACE 打开。
if std::env::var_os("OAK_OFX_TRACE").is_some() && !name.is_null() {
if let Ok(n) = unsafe { CStr::from_ptr(name) }.to_str() {
eprintln!("[ofx] fetchSuite miss: {n} v{version}");
}
}
std::ptr::null()
}
_ => std::ptr::null(),
}
}
/// 进程级 `OfxHost`ofxs 支持库的 setHost 只保存**指针**而不拷贝
/// 结构体——栈上临时变量会在 setHost 返回后悬垂,describe/渲染期
/// 再经 fetchSuite 回调就是野指针)。堆泄漏一次,永久有效。
fn global_ofx_host() -> *mut OfxHost {
static PTR: OnceLock<usize> = OnceLock::new();
*PTR.get_or_init(|| {
let host = Host::global();
Box::into_raw(Box::new(OfxHost {
host: &host.props as *const PropertySet as *mut c_void,
fetch_suite: host_fetch_suite,
})) as usize
}) as *mut OfxHost
}
// ---- 动作与属性常量(ofxCore.h / ofxImageEffect.h----
/// kOfxActionLoad。
pub(crate) const ACTION_LOAD: &str = "OfxActionLoad";
/// kOfxActionUnload。
pub(crate) const ACTION_UNLOAD: &str = "OfxActionUnload";
/// kOfxActionDescribe。
pub(crate) const ACTION_DESCRIBE: &str = "OfxActionDescribe";
/// kOfxActionCreateInstance。
pub(crate) const ACTION_CREATE_INSTANCE: &str = "OfxActionCreateInstance";
/// kOfxActionDestroyInstance。
pub(crate) const ACTION_DESTROY_INSTANCE: &str = "OfxActionDestroyInstance";
/// kOfxImageEffectActionDescribeInContext。
pub(crate) const ACTION_DESCRIBE_IN_CONTEXT: &str = "OfxImageEffectActionDescribeInContext";
/// kOfxImageEffectActionGetClipPreferences。
pub(crate) const ACTION_GET_CLIP_PREFERENCES: &str = "OfxImageEffectActionGetClipPreferences";
/// kOfxImageEffectActionGetRegionOfDefinition。
pub(crate) const ACTION_GET_ROD: &str = "OfxImageEffectActionGetRegionOfDefinition";
/// kOfxImageEffectActionGetRegionsOfInterest。
pub(crate) const ACTION_GET_ROI: &str = "OfxImageEffectActionGetRegionsOfInterest";
/// kOfxImageEffectActionIsIdentity。
pub(crate) const ACTION_IS_IDENTITY: &str = "OfxImageEffectActionIsIdentity";
/// kOfxImageEffectActionRender。
pub(crate) const ACTION_RENDER: &str = "OfxImageEffectActionRender";
/// kOfxImageEffectActionBeginSequenceRender。
pub(crate) const ACTION_BEGIN_SEQUENCE: &str = "OfxImageEffectActionBeginSequenceRender";
/// kOfxImageEffectActionEndSequenceRender。
pub(crate) const ACTION_END_SEQUENCE: &str = "OfxImageEffectActionEndSequenceRender";
/// kOfxActionOpenGLContextAttachedofxGPURender.h:345)。
pub(crate) const ACTION_GL_CONTEXT_ATTACHED: &str = "OfxActionOpenGLContextAttached";
/// kOfxActionOpenGLContextDetachedofxGPURender.h:371;宏值自带
/// "kOfx" 前缀,是规范原文)。
pub(crate) const ACTION_GL_CONTEXT_DETACHED: &str = "kOfxActionOpenGLContextDetached";
/// kOfxImageEffectActionGetOutputColourspaceofxColour.h:283)。
pub(crate) const ACTION_GET_OUTPUT_COLOURSPACE: &str = "OfxImageEffectActionGetOutputColourspace";
/// kOfxActionInstanceChangedofxCore.h:449):宿主侧参数/时间变更
/// 通知。按下 push button 后宿主须以 kOfxChangeUserEdited 的原因调用
/// 它(ofxCore.h:405-435 的 inArgs 契约)。
pub(crate) const ACTION_INSTANCE_CHANGED: &str = "OfxActionInstanceChanged";
// ---- InteractOFX 自定义交互;ofxInteract.h / ofxDrawSuite.h / ofxKeySyms.h----
/// kOfxActionNewInteract:宿主创建 interact 时发给 interact 入口的
/// 首个 action。vendored ofxInteract.h 未定义该宏(官方头文件无此
/// action;任务契约按 "向插件 main entry 发 kOfxActionNewInteract"
/// 命名)——取值 "OfxActionNewInteract",与 OFX action 命名惯例一致。
/// 插件不实现该 action 时返回 kOfxStatReplyDefault(宿主视为"无
/// interact"或"不参与 NewInteract 协议",见 [`crate::instance::Instance::new_interact`])。
pub(crate) const ACTION_NEW_INTERACT: &str = "OfxActionNewInteract";
/// kOfxInteractActionIdle:宿主空闲泵(任务契约;vendored ofxInteract.h
/// 未收录——OFX 官方规范无 Idle action,属本宿主扩展,取值
/// "OfxInteractActionIdle")。插件实现与否自愿;未处理返回
/// kOfxStatReplyDefault。
pub(crate) const ACTION_INTERACT_IDLE: &str = "OfxInteractActionIdle";
/// kOfxInteractActionDrawofxInteract.h:265)。
pub(crate) const ACTION_INTERACT_DRAW: &str = "OfxInteractActionDraw";
/// kOfxInteractActionPenMotionofxInteract.h:302)。
pub(crate) const ACTION_INTERACT_PEN_MOTION: &str = "OfxInteractActionPenMotion";
/// kOfxInteractActionPenDownofxInteract.h:340)。
pub(crate) const ACTION_INTERACT_PEN_DOWN: &str = "OfxInteractActionPenDown";
/// kOfxInteractActionPenUpofxInteract.h:376)。
pub(crate) const ACTION_INTERACT_PEN_UP: &str = "OfxInteractActionPenUp";
/// kOfxInteractActionKeyDownofxInteract.h:410)。
pub(crate) const ACTION_INTERACT_KEY_DOWN: &str = "OfxInteractActionKeyDown";
/// kOfxInteractActionKeyUpofxInteract.h:443)。
pub(crate) const ACTION_INTERACT_KEY_UP: &str = "OfxInteractActionKeyUp";
/// kOfxInteractActionGainFocusofxInteract.h:501)。
pub(crate) const ACTION_INTERACT_GAIN_FOCUS: &str = "OfxInteractActionGainFocus";
/// kOfxInteractActionLoseFocusofxInteract.h:526)。
pub(crate) const ACTION_INTERACT_LOSE_FOCUS: &str = "OfxInteractActionLoseFocus";
/// kOfxInteractPropPixelScaleofxInteract.h:58):canonical→屏幕像素
/// 换算比例(Double×2)。
pub(crate) const PROP_INTERACT_PIXEL_SCALE: &str = "OfxInteractPropPixelScale";
/// kOfxInteractPropViewportSizeOFX 1.3 命名 "OfxInteractPropViewport"
/// vendored 1.5 头文件已删,任务契约要求 draw inArgs 携带视口尺寸)。
pub(crate) const PROP_INTERACT_VIEWPORT_SIZE: &str = "OfxInteractPropViewport";
/// kOfxInteractPropBackgroundImage:任务契约的 draw inArgs 背景图像
/// 句柄(Pointer;无背景时为空——本宿主 Phase 1 无合成背景,恒空)。
/// 非官方 OFX 属性(官方只有 BackgroundColour),属本宿主扩展。
pub(crate) const PROP_INTERACT_BACKGROUND_IMAGE: &str = "OfxInteractPropBackgroundImage";
/// kOfxInteractPropBackgroundColourofxInteract.h:71):宿主视口背景色
/// Double×3)。
pub(crate) const PROP_INTERACT_BACKGROUND_COLOUR: &str = "OfxInteractPropBackgroundColour";
/// kOfxInteractPropSuggestedColourofxInteract.h:86):宿主建议的 overlay
/// 颜色(Double×3;宿主不支持颜色选择时返回 ReplyDefault)。
pub(crate) const PROP_INTERACT_SUGGESTED_COLOUR: &str = "OfxInteractPropSuggestedColour";
/// kOfxInteractPropSlaveToParamofxInteract.h:50):值变化触发 interact
/// 重绘的参数名(String×N)。
pub(crate) const PROP_INTERACT_SLAVE_TO_PARAM: &str = "OfxInteractPropSlaveToParam";
/// kOfxInteractPropPenPositionofxInteract.h:95):笔的 canonical 位置
/// Double×2,只读 inArgs)。
pub(crate) const PROP_INTERACT_PEN_POSITION: &str = "OfxInteractPropPenPosition";
/// kOfxInteractPropPenViewportPositionofxInteract.h:104):笔的视口像素
/// 位置(Int×2,只读 inArgs)。
pub(crate) const PROP_INTERACT_PEN_VIEWPORT_POSITION: &str = "OfxInteractPropPenViewportPosition";
/// kOfxInteractPropPenPressureofxInteract.h:114):笔压(Double×1
/// 0..1;两态笔映射 0/1)。
pub(crate) const PROP_INTERACT_PEN_PRESSURE: &str = "OfxInteractPropPenPressure";
/// kOfxInteractPropBitDepthofxInteract.h:122):interact 帧缓冲位深
/// Int×1,只读)。
pub(crate) const PROP_INTERACT_BIT_DEPTH: &str = "OfxInteractPropBitDepth";
/// kOfxInteractPropHasAlphaofxInteract.h:132):interact 帧缓冲是否含
/// alphaInt×1,只读)。
pub(crate) const PROP_INTERACT_HAS_ALPHA: &str = "OfxInteractPropHasAlpha";
/// kOfxInteractPropDrawContextofxDrawSuite.h:34):Draw suite 上下文句柄
/// Pointerdraw inArgs 携带,插件取来传给 Draw suite 函数)。
pub(crate) const PROP_INTERACT_DRAW_CONTEXT: &str = "OfxInteractPropDrawContext";
/// kOfxPropKeySymofxKeySyms.h:30):键盘事件的关键码(Int×1)。
pub(crate) const PROP_KEY_SYM: &str = "kOfxPropKeySym";
/// kOfxPropKeyStringofxKeySyms.h:49):键盘事件的 UTF-8 字符(String×1)。
pub(crate) const PROP_KEY_STRING: &str = "kOfxPropKeyString";
/// kOfxImageEffectPluginPropOverlayInteractV2ofxImageEffect.h:825):
/// 插件声明的 overlay interact 入口(Pointer→OfxPluginEntryPointV2
/// 要求 Draw suite 绘制)。
pub(crate) const PROP_OVERLAY_INTERACT_V2: &str = "OfxImageEffectPluginPropOverlayInteractV2";
/// kOfxImageEffectPluginPropOverlayInteractV1ofxImageEffect.h:812)。
pub(crate) const PROP_OVERLAY_INTERACT_V1: &str = "OfxImageEffectPluginPropOverlayInteractV1";
/// kOfxImageEffectPropSupportsOverlaysofxImageEffect.h:801):宿主是否
/// 允许插件在输出图像上绘制 overlay(能力宣告)。
pub(crate) const PROP_SUPPORTS_OVERLAYS: &str = "OfxImageEffectPropSupportsOverlays";
// ---- OFX 关键码(ofxKeySyms.hX11 keysym 值,测试/宿主常用子集)----
//
// 公共:app 侧(WG3b)经 [`crate::suites::interact::Interact::key_down`]/
// `key_up` 传关键码。
/// kOfxKey_UnknownofxKeySyms.h:121)。
pub const KEY_UNKNOWN: i32 = 0x0;
/// kOfxKey_BackSpaceofxKeySyms.h:128)。
pub const KEY_BACKSPACE: i32 = 0xFF08;
/// kOfxKey_TabofxKeySyms.h:129)。
pub const KEY_TAB: i32 = 0xFF09;
/// kOfxKey_ReturnofxKeySyms.h:132)。
pub const KEY_RETURN: i32 = 0xFF0D;
/// kOfxKey_EscapeofxKeySyms.h:136)。
pub const KEY_ESCAPE: i32 = 0xFF1B;
/// kOfxKey_DeleteofxKeySyms.h:137)。
pub const KEY_DELETE: i32 = 0xFFFF;
/// kOfxKey_HomeofxKeySyms.h:172)。
pub const KEY_HOME: i32 = 0xFF50;
/// kOfxKey_LeftofxKeySyms.h:173)。
pub const KEY_LEFT: i32 = 0xFF51;
/// kOfxKey_UpofxKeySyms.h:174)。
pub const KEY_UP: i32 = 0xFF52;
/// kOfxKey_RightofxKeySyms.h:175)。
pub const KEY_RIGHT: i32 = 0xFF53;
/// kOfxKey_DownofxKeySyms.h:176)。
pub const KEY_DOWN: i32 = 0xFF54;
/// kOfxKey_Page_UpofxKeySyms.h:178)。
pub const KEY_PAGE_UP: i32 = 0xFF55;
/// kOfxKey_Page_DownofxKeySyms.h:180)。
pub const KEY_PAGE_DOWN: i32 = 0xFF56;
/// kOfxKey_EndofxKeySyms.h:181)。
pub const KEY_END: i32 = 0xFF57;
/// kOfxKey_F1ofxKeySyms.h:252)。
pub const KEY_F1: i32 = 0xFFBE;
/// kOfxKey_Shift_LofxKeySyms.h:315)。
pub const KEY_SHIFT_L: i32 = 0xFFE1;
/// kOfxKey_Control_LofxKeySyms.h:317)。
pub const KEY_CONTROL_L: i32 = 0xFFE3;
/// kOfxKey_Alt_LofxKeySyms.h:323)。
pub const KEY_ALT_L: i32 = 0xFFE9;
/// kOfxKey_spaceofxKeySyms.h:331)。
pub const KEY_SPACE: i32 = 0x020;
/// kOfxKey_aofxKeySyms.h:398)。
pub const KEY_A: i32 = 0x061;
/// kOfxKey_zofxKeySyms.h:423)。
pub const KEY_Z: i32 = 0x07a;
/// kOfxPropChangeReasonofxCore.h:763):instanceChanged 的 inArgs 里
/// 说明变更来源(UserEdited / PluginEdited / Time)。
pub(crate) const PROP_CHANGE_REASON: &str = "OfxPropChangeReason";
/// kOfxChangeUserEditedofxCore.h:792)。
pub(crate) const CHANGE_USER_EDITED: &str = "OfxChangeUserEdited";
/// kOfxChangePluginEditedofxCore.h:795)。
pub(crate) const CHANGE_PLUGIN_EDITED: &str = "OfxChangePluginEdited";
/// kOfxChangeTimeofxCore.h:798)。
pub(crate) const CHANGE_TIME: &str = "OfxChangeTime";
/// kOfxPropTimeofxCore.h:613)。
pub(crate) const PROP_TIME: &str = "OfxPropTime";
/// kOfxPropEffectInstanceofxCore.h:776):interact/渲染 inArgs 里指向
/// 效果实例句柄的指针属性。
pub(crate) const PROP_EFFECT_INSTANCE: &str = "OfxPropEffectInstance";
/// kOfxImageEffectPropRenderScale。
pub(crate) const PROP_RENDER_SCALE: &str = "OfxImageEffectPropRenderScale";
/// kOfxImageEffectPropRenderWindow。
pub(crate) const PROP_RENDER_WINDOW: &str = "OfxImageEffectPropRenderWindow";
/// kOfxImageEffectPropRegionOfInterest。
pub(crate) const PROP_ROI: &str = "OfxImageEffectPropRegionOfInterest";
/// kOfxImageEffectPropRegionOfDefinition。
pub(crate) const PROP_ROD: &str = "OfxImageEffectPropRegionOfDefinition";
/// kOfxImageEffectPropIsIdentity。
pub(crate) const PROP_IS_IDENTITY: &str = "OfxImageEffectPropIsIdentity";
/// kOfxImageEffectPropFieldToRender。
pub(crate) const PROP_FIELD_TO_RENDER: &str = "OfxImageEffectPropFieldToRender";
/// kOfxImageEffectPropSequentialRenderStatus。
pub(crate) const PROP_SEQUENTIAL_RENDER: &str = "OfxImageEffectPropSequentialRenderStatus";
/// kOfxImageEffectPropInteractiveRenderStatus。
pub(crate) const PROP_INTERACTIVE_RENDER: &str = "OfxImageEffectPropInteractiveRenderStatus";
/// kOfxImageEffectPropRenderQualityDraft。
pub(crate) const PROP_RENDER_QUALITY_DRAFT: &str = "OfxImageEffectPropRenderQualityDraft";
/// kOfxImageEffectPropNoSpatialAwareness。
pub(crate) const PROP_NO_SPATIAL_AWARENESS: &str = "OfxImageEffectPropNoSpatialAwareness";
/// kOfxImageEffectPropFrameRange。
pub(crate) const PROP_FRAME_RANGE: &str = "OfxImageEffectPropFrameRange";
/// kOfxImageEffectPropFrameStep。
pub(crate) const PROP_FRAME_STEP: &str = "OfxImageEffectPropFrameStep";
/// kOfxImageEffectPropFrameRate。
pub(crate) const PROP_FRAME_RATE: &str = "OfxImageEffectPropFrameRate";
/// kOfxImageEffectPropPreMultiplication。
pub(crate) const PROP_PREMULT: &str = "OfxImageEffectPropPreMultiplication";
/// kOfxImageClipPropFieldOrder。
pub(crate) const PROP_FIELD_ORDER: &str = "OfxImageClipPropFieldOrder";
/// kOfxImageClipPropContinuousSamples。
pub(crate) const PROP_CONTINUOUS_SAMPLES: &str = "OfxImageClipPropContinuousSamples";
/// kOfxImageEffectFrameVarying。
pub(crate) const PROP_FRAME_VARYING: &str = "OfxImageEffectFrameVarying";
/// kOfxImageClipPropConnected。
pub(crate) const PROP_CLIP_CONNECTED: &str = "OfxImageClipPropConnected";
/// kOfxImageEffectPropContext。
pub(crate) const PROP_CONTEXT: &str = "OfxImageEffectPropContext";
/// kOfxImageEffectPropSupportedContexts。
pub(crate) const PROP_SUPPORTED_CONTEXTS: &str = "OfxImageEffectPropSupportedContexts";
/// kOfxImageEffectPropProjectSize。
pub(crate) const PROP_PROJECT_SIZE: &str = "OfxImageEffectPropProjectSize";
/// kOfxImageEffectPropProjectOffset。
pub(crate) const PROP_PROJECT_OFFSET: &str = "OfxImageEffectPropProjectOffset";
/// kOfxImageEffectPropProjectExtent。
pub(crate) const PROP_PROJECT_EXTENT: &str = "OfxImageEffectPropProjectExtent";
/// kOfxImageEffectPropProjectPixelAspectRatio(宏值即 PixelAspectRatio)。
pub(crate) const PROP_PROJECT_PAR: &str = "OfxImageEffectPropPixelAspectRatio";
/// kOfxImageEffectInstancePropEffectDuration。
pub(crate) const PROP_EFFECT_DURATION: &str = "OfxImageEffectInstancePropEffectDuration";
/// kOfxImageEffectInstancePropSequentialRender。
pub(crate) const PROP_SEQUENTIAL: &str = "OfxImageEffectInstancePropSequentialRender";
/// kOfxImageEffectPropPluginHandle。
pub(crate) const PROP_PLUGIN_HANDLE: &str = "OfxImageEffectPropPluginHandle";
/// kOfxPluginPropFilePath。
pub(crate) const PROP_PLUGIN_FILE_PATH: &str = "OfxPluginPropFilePath";
/// kOfxImageEffectPropSupportsTiles。
pub(crate) const PROP_SUPPORTS_TILES: &str = "OfxImageEffectPropSupportsTiles";
/// kOfxPropType。
pub(crate) const PROP_TYPE: &str = "OfxPropType";
/// kOfxPropLabel。
pub(crate) const PROP_LABEL: &str = "OfxPropLabel";
// ---- GL 能力与协商属性(ofxGPURender.hM11 第 2 期)----
/// kOfxImageEffectPropOpenGLRenderSupportedofxGPURender.h:62):
/// 宿主描述符("true")与插件描述符("false"/"true"/"needed")。
pub(crate) const PROP_GL_RENDER_SUPPORTED: &str = "OfxImageEffectPropOpenGLRenderSupported";
/// kOfxImageEffectPropOpenGLEnabledofxGPURender.h:117):Render/
/// Begin/EndSequenceRender 的 in args。
pub(crate) const PROP_GL_ENABLED: &str = "OfxImageEffectPropOpenGLEnabled";
/// kOfxOpenGLPropPixelDepthofxGPURender.h:89):插件描述符的 GL
/// 渲染支持位深(可选;格式协商的宿主侧输入)。
pub(crate) const PROP_GL_PIXEL_DEPTH: &str = "OfxOpenGLPropPixelDepth";
// ---- ofxColour 属性(ofxColour.hM11 第 2 期)----
/// kOfxImageEffectPropColourManagementStyleofxColour.h:60)。
pub(crate) const PROP_COLOUR_STYLE: &str = "OfxImageEffectPropColourManagementStyle";
/// kOfxImageEffectColourManagementOCIOofxColour.h:73):宿主声明的
/// 色彩管理模式(任务要求)。
pub(crate) const COLOUR_STYLE_OCIO: &str = "OfxImageEffectColourManagementOCIO";
/// kOfxImageEffectPropColourManagementAvailableConfigsofxColour.h:85)。
pub(crate) const PROP_COLOUR_AVAILABLE_CONFIGS: &str =
"OfxImageEffectPropColourManagementAvailableConfigs";
/// kOfxImageEffectPropColourManagementConfigofxColour.h:98):实例期
/// 协商出的 native 配置。
pub(crate) const PROP_COLOUR_CONFIG: &str = "OfxImageEffectPropColourManagementConfig";
/// kOfxImageEffectPropOCIOConfigofxColour.h:110):实例期 OCIO 配置
/// 路径/URI。
pub(crate) const PROP_OCIO_CONFIG: &str = "OfxImageEffectPropOCIOConfig";
/// kOfxImageClipPropColourspaceofxColour.h:142):clip 实际色彩空间。
pub(crate) const PROP_CLIP_COLOURSPACE: &str = "OfxImageClipPropColourspace";
/// kOfxImageClipPropPreferredColourspacesofxColour.h:201):clip 偏好
/// 色彩空间(GetClipPreferences 的 out args 可写)。
pub(crate) const PROP_CLIP_PREFERRED_COLOURSPACES: &str = "OfxImageClipPropPreferredColourspaces";
/// 工作空间(任务要求:全链路 ACEScg)。
pub(crate) const WORKING_COLOURSPACE: &str = "ACEScg";
/// 宿主支持的 native 色彩配置标识(ofxColour.h:78 的唯一现行值)。
pub(crate) const NATIVE_CONFIG_ID: &str = "ofx-native-v1.5_aces-v1.3_ocio-v2.3";
/// 宿主 OCIO 配置 URIofxColour.h:110 允许 ocio:// 内建配置;本宿主
/// 无自有 config 文件,用 OCIO 内建默认配置并以 clip 色彩空间属性
/// 传达 ACEScg 工作空间)。
pub(crate) const OCIO_CONFIG_URI: &str = "ocio://default";
/// 协商期 per-clip 约定名(HS: "OfxImageClipPropComponents_<clip>"
/// 等,ofxhImageEffect.cpp:1725-1727)。
pub(crate) fn clip_pref_prop(prefix: &str, clip: &str) -> String {
format!("{prefix}_{clip}")
}
/// 组件名前缀。
pub(crate) const CLIP_PREF_COMPONENTS: &str = "OfxImageClipPropComponents";
/// 位深名前缀。
pub(crate) const CLIP_PREF_DEPTH: &str = "OfxImageClipPropDepth";
/// 像素比名前缀。
pub(crate) const CLIP_PREF_PAR: &str = "OfxImageClipPropPAR";
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
// ---- Plugin --------------------------------------------------------------
/// 一个已加载插件(bundle 内的一个 effect 入口)。
pub struct Plugin {
/// 插件标识(OfxPlugin::pluginIdentifier,如 "net.sf.cimg.CImgInvert")。
pub identifier: String,
/// 版本号(major/minor)。
pub version: (u32, u32),
/// bundle 路径。
pub bundle_path: PathBuf,
/// describe 得到的支持上下文(filter/generator/transition)。
pub contexts: Vec<String>,
/// describe 产物(根描述符)。
pub descriptor: EffectDescriptor,
/// dlopen 句柄(公开字段供测试构造假插件)。
pub lib: *mut c_void,
/// 插件入口函数 `OfxPluginEntryPoint`(公开字段供测试构造;
/// 签名与 SDK ofxCore.h:84 逐字一致——handle 是 `const void*`)。
pub entry: unsafe extern "C" fn(
action: *const c_char,
handle: *const c_void,
in_args: *mut c_void,
out_args: *mut c_void,
) -> i32,
/// OfxPlugin 结构指针(kOfxImageEffectPropPluginHandle 属性用;
/// 公开:测试构造假插件需要)。
pub ofx_plugin: *mut c_void,
/// 二进制已卸载(host shutdown / unload_all)。置位后任何
/// call_action/call_entry 直接失败而不触碰入口——插件入口指向
/// 已 dlclose 的代码,调用即 SIGSEGV(测试串行 shutdown/重扫描
/// 会产生跨代实例,它们持有的正是旧代插件记录)。
pub unloaded: std::sync::atomic::AtomicBool,
}
// dlopen 句柄与 OfxPlugin 指针是**不透明令牌**(只经 dlsym/插件
// 结构字段访问,不直接解引用);跨线程搬运句柄是宿主分发语义。
// 与 property.rs 的 Value::Pointer 同理。
unsafe impl Send for Plugin {}
unsafe impl Sync for Plugin {}
/// 插件入口函数类型(`OfxPluginEntryPoint`ofxCore.h:84)。
pub(crate) type EntryPoint = unsafe extern "C" fn(
action: *const c_char,
handle: *const c_void,
in_args: *mut c_void,
out_args: *mut c_void,
) -> i32;
/// 插件声明的 overlay interact 入口(ofxImageEffect.h:825/812):
/// V2 优先、V1 次之;两者都未声明返回 None。属性值是
/// `Pointer→OfxPluginEntryPoint`(宿主预定义,插件 describe 期写入)。
pub(crate) fn overlay_interact_entry(props: &PropertySet) -> Option<EntryPoint> {
for name in [PROP_OVERLAY_INTERACT_V2, PROP_OVERLAY_INTERACT_V1] {
if let Some(Value::Pointer(p)) = props.get(name, 0) {
if !p.is_null() {
// 属性值是函数指针(void* 存放);转换与 dlsym_fn 同款
// (调用方保证类型正确)。
return Some(unsafe { std::mem::transmute_copy(&p) });
}
}
}
None
}
impl Plugin {
/// 调插件的 action。`handle` 视 action 而定(describe 时为
/// descriptorrender 时为 instance)。`in_args`/`out_args` 按
/// OFX 规范组装属性集。
///
/// 返回 OFX 状态码(kOfxStatOK == 0)。任何插件侧异常/崩溃信号
/// 无法捕获——FFI 纪律只兜 Rust 侧 panic。
///
/// # Safety
/// `handle`/参数集必须与 action 匹配(规范约定)。
pub(crate) unsafe fn call_action(
&self,
action: &str,
handle: *mut c_void,
in_args: &PropertySet,
out_args: &PropertySet,
) -> i32 {
if self.unloaded.load(std::sync::atomic::Ordering::Acquire) {
return status::ERR_FATAL;
}
// 属性集经裸指针(标签 0)传给插件(property suite 接受)。
let in_ptr = in_args as *const PropertySet as *mut c_void;
let out_ptr = out_args as *const PropertySet as *mut c_void;
let action = cs(action);
unsafe { (self.entry)(action.as_ptr(), handle as *const c_void, in_ptr, out_ptr) }
}
/// 按指定入口函数调用(interact 的 overlay 入口与 main entry 可
/// 不同——插件经 kOfxImageEffectPluginPropOverlayInteractV2 声明)。
/// 语义同 [`Plugin::call_action`]。
///
/// # Safety
/// `entry` 必须是插件导出的合法 OfxPluginEntryPoint`handle`/参数
/// 集与 action 匹配。
pub(crate) unsafe fn call_entry(
&self,
entry: EntryPoint,
action: &str,
handle: *mut c_void,
in_args: &PropertySet,
out_args: &PropertySet,
) -> i32 {
if self.unloaded.load(std::sync::atomic::Ordering::Acquire) {
return status::ERR_FATAL;
}
let in_ptr = in_args as *const PropertySet as *mut c_void;
let out_ptr = out_args as *const PropertySet as *mut c_void;
let action = cs(action);
unsafe { (entry)(action.as_ptr(), handle as *const c_void, in_ptr, out_ptr) }
}
}
// ---- PluginCache ---------------------------------------------------------
/// 一个已加载二进制(dlopen 句柄;多个 Plugin 可共享一个二进制)。
struct LoadedBinary {
handle: *mut c_void,
bundle_path: PathBuf,
}
impl Drop for LoadedBinary {
fn drop(&mut self) {
unsafe { dlclose(self.handle) };
}
}
// 同 Plugindlopen 句柄为不透明令牌。
unsafe impl Send for LoadedBinary {}
unsafe impl Sync for LoadedBinary {}
/// 插件缓存(进程单例,见 [`Host::global`])。
pub struct PluginCache {
/// 已加载插件(identifier 唯一)。
plugins: Mutex<Vec<Arc<Plugin>>>,
/// 已扫描的搜索路径(防重复扫描)。
scanned_paths: Mutex<Vec<PathBuf>>,
/// 已加载二进制(dlclose 责任;shutdown 时释放)。
binaries: Mutex<Vec<LoadedBinary>>,
}
/// bundle 目录判定:目录名以 `.bundle` 或 `.plugin` 结尾。
fn is_bundle_dir(path: &Path) -> bool {
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
return false;
};
name.ends_with(".bundle") || name.ends_with(".plugin")
}
/// 在 bundle 目录内定位二进制(探测式;`// [P2]` 起读 Info.plist 的
/// CFBundleExecutable)。
fn find_binary_in_bundle(bundle: &Path) -> Option<PathBuf> {
let contents = bundle.join("Contents");
for platform_dir in [
contents.join("MacOS"),
contents.join("Linux-x86-64"),
contents.join("Linux-aarch64"),
// OFX 规范的 64 位 Windows 平台目录。
contents.join("Win64"),
] {
if let Ok(entries) = std::fs::read_dir(&platform_dir) {
for e in entries.flatten() {
let p = e.path();
if p.is_file() && !p.extension().is_some_and(|x| x == "plist") {
return Some(p);
}
}
}
}
// 退而求其次:bundle 根下的 .so/.dylib/.ofx。
if let Ok(entries) = std::fs::read_dir(bundle) {
for e in entries.flatten() {
let p = e.path();
let ext = p.extension().and_then(|x| x.to_str()).unwrap_or("");
if p.is_file() && (ext == "so" || ext == "dylib" || ext == "ofx") {
return Some(p);
}
}
}
None
}
/// 默认插件搜索路径(扫描顺序):OFX 规范的用户/系统级位置(按平台
/// cfg 组织)、Olive app-relative 位置(C++ 对齐,olivehost.cpp:104-107)。
///
/// OFX 官方标准位置(见 OFX "plug-in discovery" 规范):
///
/// * macOS:用户 `~/Library/OFX/Plugins`、系统 `/Library/OFX/Plugins`
/// * Windows`%ProgramFiles%\Common Files\OFX\Plugins``%ProgramFiles%`
/// 未设置时回退字面 `C:\Program Files\...`
/// * Linux`/usr/OFX/Plugins`、`/usr/local/OFX/Plugins`
///
/// 外加 Olive 的历史位置:`$HOME/.OFX/Plugins`、
/// `$HOME/.local/share/OFX/Plugins`、`$HOME/.local/share/olive/ofx/Plugins`、
/// `../OFX/Plugins`、`../share/olive/ofx/Plugins`、`../lib/olive/ofx/Plugins`。
/// 环境变量(`OFX_PLUGIN_PATH`/`OLIVE_OFX_PLUGIN_PATH`/`OLIVE_PLUGIN_PATH`
/// 由 [`PluginCache::scan`] 追加。`home` 为 `$HOME``None` 时跳过所有
/// home 基路径。
fn default_plugin_paths(home: Option<&Path>) -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Some(home) = home {
paths.push(home.join(".OFX/Plugins"));
paths.push(home.join(".local/share/OFX/Plugins"));
paths.push(home.join(".local/share/olive/ofx/Plugins"));
// OFX 规范:macOS 用户级位置。
#[cfg(target_os = "macos")]
paths.push(home.join("Library/OFX/Plugins"));
}
// OFX 规范:macOS 系统级位置。
#[cfg(target_os = "macos")]
paths.push(PathBuf::from("/Library/OFX/Plugins"));
// OFX 规范:Windows 系统级位置(优先真实安装根 `%ProgramFiles%`)。
#[cfg(target_os = "windows")]
{
match std::env::var("ProgramFiles")
.or_else(|_| std::env::var("ProgramW6432"))
.map(PathBuf::from)
{
Ok(root) => paths.push(root.join("Common Files").join("OFX").join("Plugins")),
Err(_) => paths.push(PathBuf::from(r"C:\Program Files\Common Files\OFX\Plugins")),
}
}
// OFX 规范:Linux 系统级位置。
#[cfg(target_os = "linux")]
{
paths.push(PathBuf::from("/usr/OFX/Plugins"));
paths.push(PathBuf::from("/usr/local/OFX/Plugins"));
}
// Olive app-relative 位置(olivehost.cpp:104-107)。
paths.push(PathBuf::from("../OFX/Plugins"));
paths.push(PathBuf::from("../share/olive/ofx/Plugins"));
paths.push(PathBuf::from("../lib/olive/ofx/Plugins"));
paths
}
/// 把 `canonical` 记入已扫描路径表,返回 `false` 时表示已存在(调用方
/// 跳过再次扫描)。保持首见顺序;去重键是规范化后的路径(HS:
/// addFileToPath 的 weakly_canonical 语义)。
fn record_scanned_path(seen: &mut Vec<PathBuf>, canonical: PathBuf) -> bool {
if seen.iter().any(|p| p == &canonical) {
return false;
}
seen.push(canonical);
true
}
impl PluginCache {
/// 扫描标准目录(OFX 规范 + C++ 对齐):用户级 `$HOME` 路径、平台
/// 系统级路径(macOS/Linux/Windows)、Olive app-relative 路径,以及
/// `OFX_PLUGIN_PATH`/`OLIVE_OFX_PLUGIN_PATH`/`OLIVE_PLUGIN_PATH` 三个
/// 环境变量。重复调用是 no-op(按规范化路径去重);不存在的目录
/// 直接跳过(见 [`Self::scan_path`])。
pub fn scan(&self) -> crate::error::Result<()> {
let home = std::env::var("HOME").ok().filter(|h| !h.is_empty());
let mut paths = default_plugin_paths(home.as_deref().map(Path::new));
for var in [
"OLIVE_OFX_PLUGIN_PATH",
"OLIVE_PLUGIN_PATH",
"OFX_PLUGIN_PATH",
] {
if let Ok(raw) = std::env::var(var) {
// 平台路径分隔符(Unix ':' / Windows ';'),与 C++ 的
// `QDir::listSeparator` 一致。
paths.extend(std::env::split_paths(&raw));
}
}
for p in paths {
self.scan_path(&p)?;
}
Ok(())
}
/// 追加一个自定义搜索目录并扫描之(递归至深度 3;按路径去重)。
pub fn scan_path(&self, path: &std::path::Path) -> crate::error::Result<()> {
// 规范化去重(HS: addFileToPath 的 weakly_canonical)。
let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
{
let mut seen = self.scanned_paths.lock().unwrap_or_else(|e| e.into_inner());
if !record_scanned_path(&mut seen, canonical.clone()) {
return Ok(());
}
}
if !canonical.is_dir() {
return Ok(());
}
let mut bundles = Vec::new();
walk_dirs(&canonical, 0, 3, &mut bundles);
for bundle in bundles {
self.load_bundle(&bundle);
}
Ok(())
}
/// 加载一个 bundle(幂等:按 bundle 路径去重)。每个早退分支都打
/// 诊断日志——静默失败会让效果库毫无线索地缺插件。
fn load_bundle(&self, bundle: &Path) {
let Some(binary) = find_binary_in_bundle(bundle) else {
eprintln!("[ofx] {}: no plugin binary in bundle", bundle.display());
return;
};
let Some(handle) = dl_open(&binary) else {
eprintln!("[ofx] {}: dlopen failed", binary.display());
return;
};
{
let bins = self.binaries.lock().unwrap_or_else(|e| e.into_inner());
if bins.iter().any(|b| b.bundle_path == bundle) {
unsafe { dlclose(handle) };
return;
}
}
let plugins = unsafe { self.collect_plugins(handle, bundle) };
if plugins.is_empty() {
eprintln!(
"[ofx] {}: no usable plugins (setHost/load/describe failed)",
binary.display()
);
unsafe { dlclose(handle) };
return;
}
let mut plugs = self.plugins.lock().unwrap_or_else(|e| e.into_inner());
let mut bins = self.binaries.lock().unwrap_or_else(|e| e.into_inner());
for p in plugins {
// identifier 去重(后加载者忽略,HS 语义)。
if !plugs.iter().any(|e| e.identifier == p.identifier) {
plugs.push(p);
}
}
bins.push(LoadedBinary {
handle,
bundle_path: bundle.to_path_buf(),
});
}
/// 从已加载二进制收集插件(setHost → load → describe)。
///
/// # Safety
/// `handle` 必须是有效 dlopen 句柄。
unsafe fn collect_plugins(&self, handle: *mut c_void, bundle: &Path) -> Vec<Arc<Plugin>> {
let get_no: unsafe extern "C" fn() -> c_int =
match unsafe { dlsym_fn(handle, "OfxGetNumberOfPlugins") } {
Some(f) => f,
None => {
return Vec::new();
}
};
let get_plug: unsafe extern "C" fn(c_int) -> *mut OfxPlugin =
match unsafe { dlsym_fn(handle, "OfxGetPlugin") } {
Some(f) => f,
None => {
return Vec::new();
}
};
let count = unsafe { get_no() };
if count <= 0 {
return Vec::new();
}
let mut out = Vec::new();
for i in 0..count {
let ofx = unsafe { get_plug(i) };
if ofx.is_null() {
continue;
}
if let Some(p) = unsafe { self.build_plugin(ofx, handle, bundle) } {
out.push(p);
}
}
out
}
/// 读 OfxPlugin 头部并跑 setHost/load/describe。
///
/// # Safety
/// `ofx` 指向插件导出的有效 OfxPlugin 结构。
unsafe fn build_plugin(
&self,
ofx: *mut OfxPlugin,
lib: *mut c_void,
bundle: &Path,
) -> Option<Arc<Plugin>> {
let ofx_ref = unsafe { &*ofx };
// API 匹配(kOfxImageEffectPluginApi "OfxImageEffectPluginAPI" v1
// ofxImageEffect.h:28-32)。每个拒绝分支都打诊断日志(静默拒绝
// 会让效果库毫无线索地缺插件)。
let api = if ofx_ref.plugin_api.is_null() {
eprintln!("[ofx] {}: null plugin_api", bundle.display());
return None;
} else {
unsafe { CStr::from_ptr(ofx_ref.plugin_api) }
.to_str()
.ok()?
};
if api != "OfxImageEffectPluginAPI" || ofx_ref.api_version != 1 {
eprintln!(
"[ofx] {}: unsupported api {api} v{}",
bundle.display(),
ofx_ref.api_version
);
return None;
}
let identifier = unsafe { CStr::from_ptr(ofx_ref.plugin_identifier) }
.to_str()
.ok()?
.to_string();
let entry = ofx_ref.main_entry?;
// setHost 是 mandatory 的第一个调用(ofxCore.h:124-132)。宿主
// 结构体是进程级静态(ofxs 只存指针,见 global_ofx_host)。
if let Some(f) = ofx_ref.set_host {
unsafe { f(global_ofx_host()) };
}
let mut plugin = Plugin {
identifier,
version: (ofx_ref.plugin_version_major, ofx_ref.plugin_version_minor),
bundle_path: bundle.to_path_buf(),
contexts: Vec::new(),
descriptor: EffectDescriptor::new(),
lib,
entry,
ofx_plugin: ofx as *mut c_void,
unloaded: std::sync::atomic::AtomicBool::new(false),
};
// 根描述符属性(HS: effectDescriptorStuffofxhImageEffect.cpp:133-155)。
init_descriptor_props(&plugin.descriptor.props, bundle);
let empty = PropertySet::new();
// loadHS: ofxhImageEffectAPI.cpp:158-165OK/ReplyDefault 接受)。
let stat = unsafe { plugin.call_action(ACTION_LOAD, std::ptr::null_mut(), &empty, &empty) };
if stat != status::OK && stat != status::REPLY_DEFAULT {
eprintln!("[ofx] {}: load action returned {stat}", plugin.identifier);
return None;
}
// describehandle = 打标描述符;HS: ofxhImageEffectAPI.cpp:173-180)。
let desc_handle = crate::suites::tag::make(
&plugin.descriptor.props as *const PropertySet,
crate::suites::tag::DESCRIPTOR,
);
let stat = unsafe { plugin.call_action(ACTION_DESCRIBE, desc_handle, &empty, &empty) };
if stat != status::OK && stat != status::REPLY_DEFAULT {
eprintln!("[ofx] {}: describe action returned {stat}", plugin.identifier);
return None;
}
// 支持上下文(describe 产物;HS 从 props 读)。
let contexts = read_contexts(&plugin.descriptor.props);
// 只收标准上下文(Filter/Generator/Transition 之外还有
// General——kOfxImageEffectContextGeneral 同样是规范上下文,
// Roto/AppendClip/STMap 等插件只声明它)。
let contexts: Vec<String> = contexts
.into_iter()
.filter(|c| {
matches!(
c.as_str(),
"OfxImageEffectContextFilter"
| "OfxImageEffectContextGenerator"
| "OfxImageEffectContextTransition"
| "OfxImageEffectContextGeneral"
)
})
.collect();
if contexts.is_empty() {
eprintln!(
"[ofx] {}: no standard contexts after describe",
plugin.identifier
);
return None;
}
plugin.contexts = contexts;
Some(Arc::new(plugin))
}
/// 已加载插件数量。
pub fn count(&self) -> usize {
self.plugins.lock().unwrap_or_else(|e| e.into_inner()).len()
}
/// 第 `index` 个插件(扫描顺序稳定)。
pub fn at(&self, index: usize) -> Option<Arc<Plugin>> {
self.plugins
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(index)
.cloned()
}
/// 按标识查找。
pub fn find(&self, identifier: &str) -> Option<Arc<Plugin>> {
self.plugins
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.find(|p| p.identifier == identifier)
.cloned()
}
/// 卸载全部(shutdown 路径)。`scanned_paths` 一并清空——
/// 否则再 init 后的扫描会被去重短路(缓存已空但路径仍在)。
/// 卸载前先给每个插件置 `unloaded`:实例可能跨代存活(测试的
/// 串行 shutdown/重扫描),它们持有的旧代入口已随 dlclose 失效,
/// 置位后 call_action 直接失败而非跳野指针。
pub(crate) fn unload_all(&self) {
{
let plugins = self.plugins.lock().unwrap_or_else(|e| e.into_inner());
for p in plugins.iter() {
p.unloaded.store(true, std::sync::atomic::Ordering::Release);
}
}
self.plugins
.lock()
.unwrap_or_else(|e| e.into_inner())
.clear();
let mut bins = self.binaries.lock().unwrap_or_else(|e| e.into_inner());
bins.clear(); // Drop → dlclose
self.scanned_paths
.lock()
.unwrap_or_else(|e| e.into_inner())
.clear();
}
}
/// 递归枚举目录下的 bundle 目录(深度受限,防符号链接环)。
fn walk_dirs(dir: &Path, depth: usize, max_depth: usize, out: &mut Vec<PathBuf>) {
if depth > max_depth {
return;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for e in entries.flatten() {
let p = e.path();
if !p.is_dir() {
continue;
}
if is_bundle_dir(&p) {
out.push(p);
} else {
walk_dirs(&p, depth + 1, max_depth, out);
}
}
}
/// 根描述符属性表(HS: effectDescriptorStuff 的子集,
/// ofxhImageEffect.cpp:133-155)。
fn init_descriptor_props(props: &PropertySet, bundle: &Path) {
props.set_one(PROP_TYPE, Value::String(cs("OfxTypeImageEffect")));
props.set_one(PROP_LABEL, Value::String(cs("")));
props.set_one("OfxPropShortLabel", Value::String(cs("")));
props.set_one("OfxPropLongLabel", Value::String(cs("")));
props.define("OfxPropVersion", vec![Value::Int(0)]);
props.set_one("OfxPropVersionLabel", Value::String(cs("")));
props.set_one("OfxPropPluginDescription", Value::String(cs("")));
props.define(PROP_SUPPORTED_CONTEXTS, vec![]);
props.set_one("OfxImageEffectPluginPropGrouping", Value::String(cs("")));
props.set_one("OfxImageEffectPluginPropSingleInstance", Value::Int(0));
props.set_one(
"OfxImageEffectPluginRenderThreadSafety",
Value::String(cs("OfxImageEffectRenderInstanceSafe")),
);
props.set_one("OfxImageEffectPluginPropHostFrameThreading", Value::Int(1));
props.set_one(
"OfxImageEffectPluginPropOverlayInteractV1",
Value::Pointer(std::ptr::null_mut()),
);
// overlay interact V2ofxImageEffect.h:825):插件 describe 期声明
// 自定义交互入口(Pointer→OfxPluginEntryPointV2 要求 Draw suite
// 绘制);宿主预定义空指针默认(propSet 不创建属性,宿主预定义
// 属性宇宙,与 V1 同款)。
props.set_one(PROP_OVERLAY_INTERACT_V2, Value::Pointer(std::ptr::null_mut()));
props.set_one("OfxImageEffectPropSupportsMultiResolution", Value::Int(1));
props.set_one(PROP_SUPPORTS_TILES, Value::Int(1));
props.set_one("OfxImageEffectPropTemporalClipAccess", Value::Int(0));
props.define("OfxImageEffectPropSupportedPixelDepths", vec![]);
props.set_one(
"OfxImageEffectPluginPropFieldRenderTwiceAlways",
Value::Int(1),
);
props.set_one(
"OfxImageEffectPropSupportsMultipleClipDepths",
Value::Int(0),
);
props.set_one("OfxImageEffectPropSupportsMultipleClipPARs", Value::Int(0));
props.define("OfxImageEffectPropClipPreferencesSlaveParam", vec![]);
props.set_one(PROP_SEQUENTIAL, Value::Int(0));
props.set_one(
PROP_PLUGIN_FILE_PATH,
Value::String(cs(bundle.to_str().unwrap_or(""))),
);
// GL 能力声明(M11 §4ofxGPURender.h):描述符预定义
// OpenGLRenderSupported(默认 "false")与 kOfxOpenGLPropPixelDepth
// (空数组)——插件在 describe 期经属性 suite 写入(HostSupport
// 同款:propSet 不创建属性,宿主预定义属性宇宙)。
props.set_one(PROP_GL_RENDER_SUPPORTED, Value::String(cs("false")));
props.define(PROP_GL_PIXEL_DEPTH, vec![]);
// ofxColour 描述符预定义(M11 §4ofxColour.h):style(默认
// None)与可用配置(空数组),插件 describe 期声明自身能力。
props.set_one(
PROP_COLOUR_STYLE,
Value::String(cs("OfxImageEffectColourManagementNone")),
);
props.define(PROP_COLOUR_AVAILABLE_CONFIGS, vec![]);
}
/// describe 后读支持上下文。
fn read_contexts(props: &PropertySet) -> Vec<String> {
let mut out = Vec::new();
let dim = props.dimension(PROP_SUPPORTED_CONTEXTS);
for i in 0..dim {
if let Some(Value::String(s)) = props.get(PROP_SUPPORTED_CONTEXTS, i) {
out.push(s.to_string_lossy().into_owned());
}
}
out
}
// ---- Host -----------------------------------------------------------------
/// 宿主。持有宿主级属性集(kOfxPropName/Version 与能力宣告属性)
/// 与插件缓存;是 suite 的注册与分发点。
pub struct Host {
/// 宿主属性集(kOfxProp* 宿主级属性;能力宣告如
/// kOfxImageEffectPropSupportedPixelDepths——协商的宿主侧输入)。
pub props: PropertySet,
/// 插件缓存。
pub cache: PluginCache,
/// 活跃实例注册表(泄漏断言与调试)。
pub(crate) instances: Mutex<Vec<std::sync::Weak<RefBox<Instance>>>>,
}
impl Host {
/// 进程单例。首次调用构建宿主属性集(能力宣告在此写入)。
pub fn global() -> &'static Host {
static HOST: OnceLock<Host> = OnceLock::new();
HOST.get_or_init(|| {
let props = PropertySet::new();
init_host_props(&props);
Host {
props,
cache: PluginCache {
plugins: Mutex::new(Vec::new()),
scanned_paths: Mutex::new(Vec::new()),
binaries: Mutex::new(Vec::new()),
},
instances: Mutex::new(Vec::new()),
}
})
}
/// 上下文选择 + 实例化(filter 优先;实例化失败时按 general →
/// tracker → paint → 其余支持上下文的顺序回退——TrackerPM 这类
/// 插件 describe 支持 filter,但其 createInstance 在非
/// paint/tracker/general 上下文抓不到 Mask clip 会炸
/// BadHandleNatron 上它天然以 tracker 上下文实例化)。
/// 返回实际使用的上下文与实例;所有上下文都失败时返回最后一个
/// 错误。
pub fn create_instance_preferred(
&self,
identifier: &str,
) -> crate::error::Result<(String, Arc<RefBox<Instance>>)> {
let plugin = self.cache.find(identifier).ok_or(crate::error::Error::NotFound)?;
let mut order: Vec<&str> = Vec::with_capacity(plugin.contexts.len());
for pref in [
"OfxImageEffectContextFilter",
"OfxImageEffectContextGeneral",
"OfxImageEffectContextTracker",
"OfxImageEffectContextPaint",
] {
if plugin.contexts.iter().any(|c| c == pref) {
order.push(pref);
}
}
for c in &plugin.contexts {
if !order.contains(&c.as_str()) {
order.push(c);
}
}
let mut last_err = crate::error::Error::Failed("插件无支持上下文".into());
for ctx in order {
match self.create_instance(identifier, Some(ctx)) {
Ok(inst) => return Ok((ctx.to_string(), inst)),
Err(e) => last_err = e,
}
}
Err(last_err)
}
/// 按标识创建实例(describeInContext → createInstance)。
/// 参照 HS: ofxhImageEffectAPI.cpp:200-238describeInContext 的
/// in-args 只有 kOfxImageEffectPropContext)与
/// ofxhImageEffect.cpp:389-460populateclip/param 实例化)。
/// `context` 为空用插件首个支持上下文。失败返回
/// [`crate::error::Error::Failed`]/[`crate::error::Error::NotFound`]。
pub fn create_instance(
&self,
identifier: &str,
context: Option<&str>,
) -> crate::error::Result<Arc<RefBox<Instance>>> {
let plugin = self
.cache
.find(identifier)
.ok_or(crate::error::Error::NotFound)?;
let context = match context {
Some(c) => c.to_string(),
None => plugin
.contexts
.first()
.cloned()
.ok_or(crate::error::Error::Failed("插件无支持上下文".into()))?,
};
if !plugin.contexts.iter().any(|c| c == &context) {
return Err(crate::error::Error::Failed(format!(
"插件不支持上下文 {context}"
)));
}
// describeInContext:上下文描述符 = 根描述符属性拷贝 +
// 插件在 action 内重定义参数/clip。
let mut ctx_desc = EffectDescriptor::new();
ctx_desc.props = plugin.descriptor.props.clone();
let in_args = PropertySet::new();
in_args.set_one(PROP_CONTEXT, Value::String(cs(&context)));
let desc_handle = crate::suites::tag::make(
&ctx_desc.props as *const PropertySet,
crate::suites::tag::DESCRIPTOR,
);
let stat = unsafe {
plugin.call_action(ACTION_DESCRIBE_IN_CONTEXT, desc_handle, &in_args, &in_args)
};
if stat != status::OK && stat != status::REPLY_DEFAULT {
return Err(crate::error::Error::Failed(format!(
"describeInContext 失败:{stat}"
)));
}
// populateHS: Instance::populateclip 实例 + param 实例,
// ofxhImageEffect.cpp:389-460)。
let clips = ctx_desc
.clips
.iter()
.map(|c| Box::new(crate::clip::ClipInstance::from_descriptor(c.as_ref())))
.collect();
let params = crate::param::ParamSetInstance {
params: ctx_desc
.params
.iter()
.map(|d| Box::new(crate::param::ParamInstance::from_def((**d).clone())))
.collect(),
};
let instance = Instance {
props: PropertySet::new(),
plugin,
context: context.clone(),
params,
clips,
node_identity: std::sync::atomic::AtomicUsize::new(0),
destroyed: std::sync::atomic::AtomicBool::new(false),
sequence_range: std::sync::Mutex::new(None),
progress_cb: std::sync::Mutex::new(None),
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
interact: std::sync::Mutex::new(None),
};
init_instance_props(&instance.props, &instance);
let arc = Arc::new(RefBox {
refs: std::sync::atomic::AtomicU32::new(1),
value: instance,
});
// createInstance actionHS: ofxhImageEffect.cpp:675-688)。
let inst_handle = crate::suites::tag::make(
&arc.value.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let empty = PropertySet::new();
let stat = unsafe {
arc.value
.plugin
.call_action(ACTION_CREATE_INSTANCE, inst_handle, &empty, &empty)
};
if stat != status::OK && stat != status::REPLY_DEFAULT {
// 插件拒绝了 createInstance——它从没认领这个实例;把
// destroyed 门置位,让随后的 drop 跳过 destroyInstance 通知
// (否则插件对一个它没创建的实例回 BadIndex 之类的错误)。
arc.value
.destroyed
.store(true, std::sync::atomic::Ordering::Relaxed);
return Err(crate::error::Error::Failed(format!(
"createInstance 失败:{stat}"
)));
}
// 登记活跃实例表(泄漏断言用)+ param→instance 回写表。
self.instances
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(Arc::downgrade(&arc));
let inst_props = &arc.value.props as *const PropertySet as usize;
for p in &arc.value.params.params {
let p_addr = &p.props as *const PropertySet as usize;
crate::suites::param::register_param_owner(p_addr, inst_props);
}
Ok(arc)
}
/// 关闭宿主(`oakplugin_host_shutdown` 的宿主侧):
/// 1. 对全部活实例发 destroyInstance 通知(此刻插件仍加载);
/// 2. 打取消标记——shutdown 后仍被 C 侧持有的句柄不得再调
/// 已卸载的插件入口(render 入口即短路);
/// 3. 卸载插件与二进制。
///
/// 说明:C 侧句柄持有 Arc,宿主无法(也不应)强制析构——实例
/// 内存随句柄释放;destroyed 标记保证 Drop 不再碰插件入口。
pub fn shutdown(&self) {
let list = self.instances.lock().unwrap_or_else(|e| e.into_inner());
for w in list.iter() {
if let Some(arc) = w.upgrade() {
// 置 destroyed 标记后再通知:实例随后的 Drop 不得再
// 调已卸载的插件入口(二次 destroyInstance → 悬垂)。
arc.value
.destroyed
.store(true, std::sync::atomic::Ordering::Relaxed);
arc.value.notify_destroy();
arc.value
.cancel
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
self.cache.unload_all();
}
/// 当前活跃实例数(泄漏断言)。
pub fn alive_count(&self) -> usize {
let mut list = self.instances.lock().unwrap_or_else(|e| e.into_inner());
let alive = list.iter().filter(|w| w.strong_count() > 0).count();
list.retain(|w| w.strong_count() > 0);
alive
}
}
/// 宿主属性集(对照 C++ OliveHost::OliveHostolivehost.cpp:193-207
/// Name/Label/Version;能力宣告为 phase 1 最小集,协商按需扩展)。
fn init_host_props(props: &PropertySet) {
// ofxCore.h 宿主属性集的必备项:OfxType=OfxTypeHost 与
// OfxPropAPIVersionint[2],宿主实现的 API 版本)——ofxs 支持库的
// loadAction 以 throwOnFailure=true 读它们,缺失会被映射成
// kOfxStatErrMissingHostFeature 让插件加载直接失败。
props.set_one("OfxPropType", Value::String(cs("OfxTypeHost")));
props.define("OfxPropAPIVersion", vec![Value::Int(1), Value::Int(4)]);
props.set_one("OfxPropName", Value::String(cs("Oak Video Editor")));
props.set_one("OfxPropLabel", Value::String(cs("Oak Video Editor")));
props.set_one(
"OfxPropVersionLabel",
Value::String(cs(env!("CARGO_PKG_VERSION"))),
);
props.define(
"OfxPropVersion",
vec![Value::Int(0), Value::Int(0), Value::Int(0)],
);
// 能力宣告:协商的宿主侧输入(phase 1 全链路 F32+RGBA)。
props.define(
"OfxImageEffectPropSupportedPixelDepths",
vec![Value::String(cs("OfxBitDepthFloat"))],
);
// 宿主支持的上下文集(ofxs 以 throwOnFailure=true 读)。
props.define(
"OfxImageEffectPropSupportedContexts",
vec![
Value::String(cs("OfxImageEffectContextFilter")),
Value::String(cs("OfxImageEffectContextGenerator")),
Value::String(cs("OfxImageEffectContextTransition")),
Value::String(cs("OfxImageEffectContextGeneral")),
],
);
props.define(
"OfxImageEffectPropSupportedComponents",
vec![
Value::String(cs("OfxImageComponentRGBA")),
Value::String(cs("OfxImageComponentRGB")),
Value::String(cs("OfxImageComponentAlpha")),
],
);
// ofxs 支持库 fetchHostDescription 以 throwOnFailure=true 读取的
// 全部宿主能力位(IsBackground 缺一个都会让读取链中断——
// gHostDescriptionHasInit 已置位,后续插件拿到半初始化描述,
// temporalClipAccess 为 0 → 时序类插件集体拒载)。
props.set_one("OfxImageEffectHostPropIsBackground", Value::Int(0));
props.set_one("OfxParamHostPropSupportsStringAnimation", Value::Int(0));
props.set_one("OfxParamHostPropSupportsChoiceAnimation", Value::Int(0));
props.set_one("OfxParamHostPropSupportsBooleanAnimation", Value::Int(0));
props.set_one("OfxParamHostPropSupportsCustomAnimation", Value::Int(0));
// 自定义 interact(检视器叠加层)宿主支持。
props.set_one("OfxParamHostPropSupportsCustomInteract", Value::Int(1));
// 能力宣告(ofxImageEffect.h 宿主属性集;ofxs 支持库在 load 时读成
// ImageEffectHostDescriptiontemporalClipAccess 等为 0 时整类插件
// ——Retime/FrameHold/TimeOffset/SlitScan——直接在 load 里拒载)。
props.set_one("OfxImageEffectPropTemporalClipAccess", Value::Int(1));
props.set_one("OfxImageEffectPropSupportsMultiResolution", Value::Int(1));
props.set_one("OfxImageEffectPropSupportsTiles", Value::Int(1));
props.set_one("OfxImageEffectPropSupportsMultipleClipPARs", Value::Int(1));
// 像素深度只支持 Floatphase 1 全链路 F32),不做多深度协商。
props.set_one("OfxImageEffectPropSupportsMultipleClipDepths", Value::Int(0));
// 旧版 ofxs 的 fetchHostDescription 读的是不带 Supports 的同义名
// ofxImageEffect.h 的 kOfxImageEffectPropMultipleClipDepths)。
props.set_one("OfxImageEffectPropMultipleClipDepths", Value::Int(0));
props.set_one("OfxImageEffectPropSetableFrameRate", Value::Int(0));
props.set_one("OfxImageEffectPropSetableFielding", Value::Int(0));
// fetchHostDescription 的其余读取项(部分 ofxs 版本以
// throwOnFailure=true 读,缺一个首插件的 load 就炸):
// 顺序渲染状态 0=宿主不强制;Draft 渲染质量支持(播放降档);
// 参数数无上限;不支持参数化曲线动画;macOS 无 OS 窗口句柄;
// 原生坐标原点按 OFX 规范默认 BottomLeft。
props.set_one("OfxImageEffectInstancePropSequentialRender", Value::Int(0));
props.set_one("OfxImageEffectPropRenderQualityDraft", Value::Int(1));
props.set_one("OfxParamHostPropMaxParameters", Value::Int(-1));
// 参数页数无上限;页内行列 0,0 = 自动排布(这两个也是
// throwOnFailure=true 的读取项)。
props.set_one("OfxParamHostPropMaxPages", Value::Int(-1));
props.define(
"OfxParamHostPropPageRowColumnCount",
vec![Value::Int(0), Value::Int(0)],
);
props.set_one("OfxParamHostPropSupportsParametricAnimation", Value::Int(0));
props.set_one("OfxPropHostOSHandle", Value::Pointer(std::ptr::null_mut()));
props.set_one(
"OfxImageEffectHostPropNativeOrigin",
Value::String(cs("OfxHostNativeOriginBottomLeft")),
);
// 序列渲染:插件可随意选(0 = 宿主不强制)。
props.set_one("OfxImageEffectPropSequentialRenderStatus", Value::Int(0));
// 逐帧线程化程度:全帧线程安全(kOfxImageEffectRenderUnsafe 之外的
// 最强档——渲染在独立 worker 进程内串行驱动,无共享状态)。
props.set_one("OfxImageEffectPropRenderThreadSafety", Value::String(cs("OfxImageEffectRenderFullySafe")));
// GL 能力宣告(M11 §4ofxGPURender.h "OpenGL House Keeping"
// 宿主在描述符置 "true")。
props.set_one(PROP_GL_RENDER_SUPPORTED, Value::String(cs("true")));
// overlay 能力宣告(ofxImageEffect.h:801):宿主允许插件在输出
// 图像上绘制 overlayinteract draw 的前置)。
props.set_one(PROP_SUPPORTS_OVERLAYS, Value::Int(1));
// ofxColour 能力宣告(M11 §4):OCIO 模式 + native 配置列表。
props.set_one(PROP_COLOUR_STYLE, Value::String(cs(COLOUR_STYLE_OCIO)));
props.define(
PROP_COLOUR_AVAILABLE_CONFIGS,
vec![Value::String(cs(NATIVE_CONFIG_ID))],
);
}
/// 实例属性表(HS: effectInstanceStuff 的 phase 1 子集,
/// ofxhImageEffect.cpp:313-330)。
fn init_instance_props(props: &PropertySet, instance: &Instance) {
props.set_one(PROP_TYPE, Value::String(cs("OfxTypeImageEffectInstance")));
props.set_one(PROP_CONTEXT, Value::String(cs(&instance.context)));
props.set_one(
PROP_PLUGIN_HANDLE,
Value::Pointer(instance.plugin.ofx_plugin),
);
props.define(
PROP_PROJECT_SIZE,
vec![Value::Double(1920.0), Value::Double(1080.0)],
);
props.define(
PROP_PROJECT_OFFSET,
vec![Value::Double(0.0), Value::Double(0.0)],
);
props.define(
PROP_PROJECT_EXTENT,
vec![Value::Double(1920.0), Value::Double(1080.0)],
);
props.set_one(PROP_PROJECT_PAR, Value::Double(1.0));
props.set_one(PROP_EFFECT_DURATION, Value::Double(1.0));
props.set_one(PROP_SEQUENTIAL, Value::Int(0));
props.set_one(PROP_FRAME_RATE, Value::Double(24.0));
props.set_one("OfxPropIsInteractive", Value::Int(0));
props.set_one(PROP_SUPPORTS_TILES, Value::Int(1));
// ofxColour 实例期协商结果(M11 §4ofxColour.h:宿主必须在实例
// 上设置 style/config/OCIOConfig)。
props.set_one(PROP_COLOUR_STYLE, Value::String(cs(COLOUR_STYLE_OCIO)));
props.set_one(PROP_COLOUR_CONFIG, Value::String(cs(NATIVE_CONFIG_ID)));
props.set_one(PROP_OCIO_CONFIG, Value::String(cs(OCIO_CONFIG_URI)));
}
#[cfg(test)]
mod tests {
use super::*;
/// The default path table includes every home- and app-relative location
/// plus the platform-gated OFX standard locations.
#[test]
fn default_paths_cover_home_and_standard_locations() {
let home = Path::new("/home/octa");
let paths = default_plugin_paths(Some(home));
// Olive legacy + home locations.
assert!(paths.contains(&home.join(".OFX/Plugins")));
assert!(paths.contains(&home.join(".local/share/OFX/Plugins")));
assert!(paths.contains(&home.join(".local/share/olive/ofx/Plugins")));
// App-relative (C++ parity).
assert!(paths.contains(&PathBuf::from("../OFX/Plugins")));
assert!(paths.contains(&PathBuf::from("../share/olive/ofx/Plugins")));
assert!(paths.contains(&PathBuf::from("../lib/olive/ofx/Plugins")));
// OFX standard locations, gated by platform.
#[cfg(target_os = "macos")]
{
assert!(paths.contains(&home.join("Library/OFX/Plugins")));
assert!(paths.contains(&PathBuf::from("/Library/OFX/Plugins")));
}
#[cfg(target_os = "linux")]
{
assert!(paths.contains(&PathBuf::from("/usr/OFX/Plugins")));
assert!(paths.contains(&PathBuf::from("/usr/local/OFX/Plugins")));
}
#[cfg(target_os = "windows")]
{
assert!(paths
.iter()
.any(|p| p.ends_with("Common Files/OFX/Plugins") || p.ends_with("Common Files\\OFX\\Plugins")));
}
}
/// With no `$HOME` the home-based entries are skipped; the rest remain.
#[test]
fn default_paths_skip_home_entries_when_home_unset() {
let paths = default_plugin_paths(None);
assert!(!paths.iter().any(|p| p.to_string_lossy().starts_with("/home/")));
assert!(paths.contains(&PathBuf::from("../OFX/Plugins")));
assert_eq!(
paths
.iter()
.filter(|p| p.to_string_lossy().contains(".OFX"))
.count(),
0
);
}
/// The dedupe record rejects an already-seen path and keeps first-seen
/// order.
#[test]
fn record_scanned_path_dedupes() {
let mut seen: Vec<PathBuf> = Vec::new();
assert!(record_scanned_path(&mut seen, PathBuf::from("/a/Plugins")));
assert!(record_scanned_path(&mut seen, PathBuf::from("/b/Plugins")));
// A duplicate is rejected without being recorded again.
assert!(!record_scanned_path(&mut seen, PathBuf::from("/a/Plugins")));
assert_eq!(seen, vec![PathBuf::from("/a/Plugins"), PathBuf::from("/b/Plugins")]);
}
/// Scanning an existing directory twice only records it once, and a
/// non-existent directory is skipped without error.
#[test]
fn scan_path_skips_missing_and_dedupes_existing() {
let dir = std::env::temp_dir().join(format!("oakplugin-scan-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let cache = PluginCache {
plugins: Mutex::new(Vec::new()),
scanned_paths: Mutex::new(Vec::new()),
binaries: Mutex::new(Vec::new()),
};
cache.scan_path(&dir).expect("existing dir scans");
cache.scan_path(&dir).expect("duplicate scan is a no-op");
{
let seen = cache.scanned_paths.lock().unwrap_or_else(|e| e.into_inner());
assert_eq!(seen.len(), 1, "the same directory is scanned only once");
} // drop the guard before scanning again
// A missing directory is skipped (no error, no plugin scan).
let missing = dir.join("does-not-exist");
cache.scan_path(&missing).expect("missing dir scan is a no-op");
let plugins = cache.plugins.lock().unwrap_or_else(|e| e.into_inner());
assert!(plugins.is_empty(), "no bundles were loaded");
let _ = std::fs::remove_dir_all(&dir);
}
}