diff --git a/crates/oakplugin/examples/scan_probe.rs b/crates/oakplugin/examples/scan_probe.rs new file mode 100644 index 000000000..e8bcd553a --- /dev/null +++ b/crates/oakplugin/examples/scan_probe.rs @@ -0,0 +1,28 @@ +//! Throwaway probe: scan the real system OFX directory and print what the +//! host actually discovers (diagnosing why the effect library is empty). + +fn main() { + let host = oakplugin::host::Host::global(); + match host.cache.scan() { + Ok(()) => println!("scan: ok"), + Err(e) => println!("scan: FAILED: {e}"), + } + println!("plugins discovered: {}", host.cache.count()); + // Direct probe: read the host-level props through the C suite table. + let suite = oakplugin::suites::property::suite_v1(); + unsafe { + let handle = &host.props as *const oakplugin::property::PropertySet as *mut std::ffi::c_void; + let name = std::ffi::CString::new("OfxPropName").unwrap(); + let mut out: *mut std::ffi::c_char = std::ptr::null_mut(); + let stat = (suite.get_string)(handle, name.as_ptr(), 0, &mut out); + println!("direct propGetString(OfxPropName) -> {stat}"); + if stat == 0 && !out.is_null() { + println!(" value: {:?}", std::ffi::CStr::from_ptr(out)); + } + } + let registered = oakplugin::node_factory::register_plugin_nodes(); + println!("factory node types registered: {}", registered.len()); + for id in ®istered { + println!(" type_id={id}"); + } +} diff --git a/crates/oakplugin/src/host.rs b/crates/oakplugin/src/host.rs index 02eff41e9..ecaf620a7 100644 --- a/crates/oakplugin/src/host.rs +++ b/crates/oakplugin/src/host.rs @@ -149,11 +149,42 @@ unsafe extern "C" fn host_fetch_suite( crate::suites::fetch_suite(name, version) })); match result { - Ok(Some(p)) => p, + 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 宿主没有(describe 常因此返回 + // kOfxStatErrMissingHostFeature)。 + if !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 = 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。 @@ -743,12 +774,15 @@ impl PluginCache { Ok(()) } - /// 加载一个 bundle(幂等:按 bundle 路径去重)。 + /// 加载一个 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; }; { @@ -760,6 +794,10 @@ impl PluginCache { } 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; } @@ -825,8 +863,10 @@ impl PluginCache { ) -> Option> { let ofx_ref = unsafe { &*ofx }; // API 匹配(kOfxImageEffectPluginApi "OfxImageEffectPluginAPI" v1, - // ofxImageEffect.h:28-32)。 + // 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) } @@ -834,6 +874,11 @@ impl PluginCache { .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) } @@ -842,14 +887,10 @@ impl PluginCache { .to_string(); let entry = ofx_ref.main_entry?; - // setHost 是 mandatory 的第一个调用(ofxCore.h:124-132)。 - let host = Host::global(); - let mut ofx_host = OfxHost { - host: &host.props as *const PropertySet as *mut c_void, - fetch_suite: host_fetch_suite, - }; + // setHost 是 mandatory 的第一个调用(ofxCore.h:124-132)。宿主 + // 结构体是进程级静态(ofxs 只存指针,见 global_ofx_host)。 if let Some(f) = ofx_ref.set_host { - unsafe { f(&mut ofx_host) }; + unsafe { f(global_ofx_host()) }; } let mut plugin = Plugin { @@ -871,6 +912,7 @@ impl PluginCache { // load(HS: ofxhImageEffectAPI.cpp:158-165;OK/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; } @@ -881,12 +923,15 @@ impl PluginCache { ); 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 = contexts .into_iter() .filter(|c| { @@ -895,10 +940,15 @@ impl PluginCache { "OfxImageEffectContextFilter" | "OfxImageEffectContextGenerator" | "OfxImageEffectContextTransition" + | "OfxImageEffectContextGeneral" ) }) .collect(); if contexts.is_empty() { + eprintln!( + "[ofx] {}: no standard contexts after describe", + plugin.identifier + ); return None; } plugin.contexts = contexts; @@ -1227,6 +1277,12 @@ impl Host { /// 宿主属性集(对照 C++ OliveHost::OliveHost,olivehost.cpp:193-207: /// Name/Label/Version;能力宣告为 phase 1 最小集,协商按需扩展)。 fn init_host_props(props: &PropertySet) { + // ofxCore.h 宿主属性集的必备项:OfxType=OfxTypeHost 与 + // OfxPropAPIVersion(int[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( @@ -1242,6 +1298,16 @@ fn init_host_props(props: &PropertySet) { "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![ @@ -1250,6 +1316,57 @@ fn init_host_props(props: &PropertySet) { 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 时读成 + // ImageEffectHostDescription,temporalClipAccess 等为 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)); + // 像素深度只支持 Float(phase 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 §4;ofxGPURender.h "OpenGL House Keeping": // 宿主在描述符置 "true")。 props.set_one(PROP_GL_RENDER_SUPPORTED, Value::String(cs("true"))); diff --git a/crates/oakplugin/src/node_factory.rs b/crates/oakplugin/src/node_factory.rs index 2ceb2c118..429c9bbdd 100644 --- a/crates/oakplugin/src/node_factory.rs +++ b/crates/oakplugin/src/node_factory.rs @@ -717,9 +717,15 @@ pub fn register_plugin_nodes() -> Vec { } }; - // 元数据实例(name/description;建完即弃)。 - let Ok(inst) = host.create_instance(&plugin.identifier, Some(&context)) else { - continue; + // 元数据实例(name/description;建完即弃)。describeInContext + // 失败的插件无法实例化(常见于只支持 Vegas 立体声等厂商套件 + // 的插件)——记录原因,不静默跳过。 + let inst = match host.create_instance(&plugin.identifier, Some(&context)) { + Ok(inst) => inst, + Err(e) => { + eprintln!("[ofx] {}: instance creation failed: {e}", plugin.identifier); + continue; + } }; let name = plugin_display_name(&inst.value); let description = plugin_description(&inst.value); diff --git a/crates/oakplugin/src/suites/image_effect.rs b/crates/oakplugin/src/suites/image_effect.rs index 61805be68..d146b0fe4 100644 --- a/crates/oakplugin/src/suites/image_effect.rs +++ b/crates/oakplugin/src/suites/image_effect.rs @@ -90,11 +90,17 @@ static LIVE_IMAGES: std::sync::LazyLock Result<(), c_int>) -> c_int { - catch_unwind(AssertUnwindSafe(f)).map_or_else( + let caller = std::panic::Location::caller(); + let code = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_or_else( |_| status::FAILED, |r| r.map_or_else(|c| c, |()| status::OK), - ) + ); + if code != status::OK && std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[ofx] image-effect suite error {code} at {caller}"); + } + code } /// 属性名(空指针/非 UTF-8 → ErrValue)。 diff --git a/crates/oakplugin/src/suites/multithread.rs b/crates/oakplugin/src/suites/multithread.rs index 49e519d26..94cb9ceca 100644 --- a/crates/oakplugin/src/suites/multithread.rs +++ b/crates/oakplugin/src/suites/multithread.rs @@ -25,7 +25,10 @@ //! //! 参照:HS: ofxhImageEffect.cpp gMultiThreadSuite。 +use std::collections::HashMap; use std::ffi::{c_int, c_uint, c_void}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; use crate::suites::status; @@ -50,6 +53,16 @@ pub struct MultiThreadSuiteV1 { pub index: unsafe extern "C" fn(*mut c_int) -> c_int, /// multiThreadIsSpawnedThread:当前线程是否插件线程。 pub is_spawned: unsafe extern "C" fn(*mut c_int) -> c_int, + /// mutexCreate:`count` 是初始可用计数(>1 时是信号量语义)。 + pub mutex_create: unsafe extern "C" fn(*mut *mut c_void, c_int) -> c_int, + /// mutexDestroy + pub mutex_destroy: unsafe extern "C" fn(*mut c_void) -> c_int, + /// mutexLock + pub mutex_lock: unsafe extern "C" fn(*mut c_void) -> c_int, + /// mutexUnLock + pub mutex_unlock: unsafe extern "C" fn(*mut c_void) -> c_int, + /// mutexTryLock + pub mutex_try_lock: unsafe extern "C" fn(*mut c_void) -> c_int, } /// 公共入口模板:panic 兜底。 @@ -118,6 +131,112 @@ unsafe extern "C" fn multi_thread_is_spawned(out: *mut c_int) -> c_int { }) } +// ---- 互斥锁(OfxMultiThreadSuiteV1 的 mutex* 函数组)----------------------- +// +// 句柄表:全局注册表按 usize 键发号;`count > 1` 是信号量语义(ofxMultiThread.h +// "a mutex with a count greater than 1 is a counting semaphore")。 +// 插件在多线程渲染期用它们保护共享状态——ofxs 支持库的 +// ofxsThreadSuiteCheck 在 load 时逐一检查这些函数非空,缺一个整批插件 +// 直接拒载。 + +/// 计数信号量(count==1 时即普通互斥锁)。 +struct OfxMutex { + permits: std::sync::Mutex, + released: std::sync::Condvar, +} + +/// 句柄注册表(句柄即下一个递增 id 转指针;0 保留给空)。 +static MUTEXES: std::sync::LazyLock>>> = + std::sync::LazyLock::new(|| Mutex::new(HashMap::new())); + +/// 下一个互斥锁 id。 +static NEXT_MUTEX: AtomicUsize = AtomicUsize::new(1); + +fn mutex_lookup(handle: *mut c_void) -> Option> { + if handle.is_null() { + return None; + } + MUTEXES + .lock() + .unwrap_or_else(|e| e.into_inner()) + .get(&(handle as usize)) + .cloned() +} + +unsafe extern "C" fn mutex_create(out: *mut *mut c_void, count: c_int) -> c_int { + caught(|| { + if out.is_null() { + return status::ERR_VALUE; + } + let id = NEXT_MUTEX.fetch_add(1, Ordering::Relaxed); + MUTEXES + .lock() + .unwrap_or_else(|e| e.into_inner()) + .insert(id, Arc::new(OfxMutex { + permits: std::sync::Mutex::new(count.max(1)), + released: std::sync::Condvar::new(), + })); + unsafe { *out = id as *mut c_void }; + status::OK + }) +} + +unsafe extern "C" fn mutex_destroy(handle: *mut c_void) -> c_int { + caught(|| { + if mutex_lookup(handle).is_none() { + return status::ERR_BAD_HANDLE; + } + MUTEXES + .lock() + .unwrap_or_else(|e| e.into_inner()) + .remove(&(handle as usize)); + status::OK + }) +} + +unsafe extern "C" fn mutex_lock(handle: *mut c_void) -> c_int { + caught(|| { + let Some(m) = mutex_lookup(handle) else { + return status::ERR_BAD_HANDLE; + }; + let mut permits = m.permits.lock().unwrap_or_else(|e| e.into_inner()); + while *permits <= 0 { + permits = m.released.wait(permits).unwrap_or_else(|e| e.into_inner()); + } + *permits -= 1; + status::OK + }) +} + +unsafe extern "C" fn mutex_unlock(handle: *mut c_void) -> c_int { + caught(|| { + let Some(m) = mutex_lookup(handle) else { + return status::ERR_BAD_HANDLE; + }; + { + let mut permits = m.permits.lock().unwrap_or_else(|e| e.into_inner()); + *permits += 1; + } + m.released.notify_one(); + status::OK + }) +} + +unsafe extern "C" fn mutex_try_lock(handle: *mut c_void) -> c_int { + caught(|| { + let Some(m) = mutex_lookup(handle) else { + return status::ERR_BAD_HANDLE; + }; + let mut permits = m.permits.lock().unwrap_or_else(|e| e.into_inner()); + if *permits <= 0 { + // 规范:占不到锁返回 kOfxStatFailed(不是错误)。 + return status::FAILED; + } + *permits -= 1; + status::OK + }) +} + /// 静态函数表实例。 pub fn suite_v1() -> &'static MultiThreadSuiteV1 { static SUITE: std::sync::OnceLock = std::sync::OnceLock::new(); @@ -126,6 +245,11 @@ pub fn suite_v1() -> &'static MultiThreadSuiteV1 { num_cpus: multi_thread_num_cpus, index: multi_thread_index, is_spawned: multi_thread_is_spawned, + mutex_create: mutex_create, + mutex_destroy: mutex_destroy, + mutex_lock: mutex_lock, + mutex_unlock: mutex_unlock, + mutex_try_lock: mutex_try_lock, }) } diff --git a/crates/oakplugin/src/suites/param.rs b/crates/oakplugin/src/suites/param.rs index c54a6787c..e416c0ec2 100644 --- a/crates/oakplugin/src/suites/param.rs +++ b/crates/oakplugin/src/suites/param.rs @@ -152,11 +152,17 @@ unsafe fn c_name<'a>(name: *const c_char) -> Result<&'a str, c_int> { } /// 公共入口模板:panic 兜底。 +#[track_caller] fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int { - std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_or_else( + let caller = std::panic::Location::caller(); + let code = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_or_else( |_| status::FAILED, |r| r.map_or_else(|c| c, |()| status::OK), - ) + ); + if code != status::OK && std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[ofx] param suite error {code} at {caller}"); + } + code } // ---- 变长参数实现(C shim 转发)---------------------------------------- diff --git a/crates/oakplugin/src/suites/property.rs b/crates/oakplugin/src/suites/property.rs index f318cd5c0..4644322ba 100644 --- a/crates/oakplugin/src/suites/property.rs +++ b/crates/oakplugin/src/suites/property.rs @@ -118,15 +118,21 @@ impl Kind { /// `# Safety`:`handle` 必须指向活的 `PropertySet` 或已注册对象 /// (suite 生命周期契约:宿主对象先于 suite 调用创建,后于全部调用 /// 销毁)。 +#[track_caller] unsafe fn caught(handle: *mut c_void, f: impl FnOnce(&PropertySet) -> Result<(), c_int>) -> c_int { - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let caller = std::panic::Location::caller(); + let code = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { if handle.is_null() { return status::ERR_BAD_HANDLE; } let set = unsafe { &*crate::suites::tag::strip(handle) }; f(set).map_or_else(|code| code, |()| status::OK) })) - .unwrap_or(status::FAILED) + .unwrap_or(status::FAILED); + if code != status::OK && std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[ofx] property suite error {code} at {caller}"); + } + code } /// 属性名:空指针 / 非 UTF-8 → kOfxStatErrValue(防御性;HostSupport @@ -154,36 +160,79 @@ fn get_value<'a>( name: &str, index: c_int, kind: Kind, +) -> Result<&'a Value, c_int> { + let result = get_value_inner(props, name, index, kind); + if std::env::var_os("OAK_OFX_TRACE").is_some() { + match &result { + Ok(_) => eprintln!("[ofx] propGet({name}[{index}]) -> ok"), + Err(c) => eprintln!("[ofx] propGet({name}[{index}]) -> {c}"), + } + } + result +} + +fn get_value_inner<'a>( + props: &'a [Property], + name: &str, + index: c_int, + kind: Kind, ) -> Result<&'a Value, c_int> { let p = props .iter() .find(|p| p.name == name) .ok_or(status::ERR_UNKNOWN)?; // 首元素代理整条属性的类型(宿主只定义同构数组;HS 是定义期 - // 固定类型,等价)。 - let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?; - if Kind::of(probe) != kind { - return Err(status::ERR_UNKNOWN); + // 固定类型,等价)。空数组没有类型探针,跳过类型检查,越界 + // 由下面的索引访问报 BadIndex(HS getValueRaw 语义)。 + if let Some(probe) = p.values.first() { + if Kind::of(probe) != kind { + return Err(status::ERR_UNKNOWN); + } } p.values.get(idx(index)?).ok_or(status::ERR_BAD_INDEX) } // ---- propSet(单元素)----------------------------------------------------- -/// propSet 通用实现:类型不符/未定义 → Unknown;越界 → BadIndex。 +/// propSet 通用实现:属性未定义时按 OFX 语义**隐式创建**(ofxProperty.h: +/// "If the property does not exist it is created",HS setValue 同)—— +/// 插件 describe 期写的大量描述符属性(grouping、各 capability 开关) +/// 并未由宿主预定义,拒绝创建会让 describe 直接失败。已定义但类型 +/// 不符 → Unknown;越界 → BadIndex。 fn set_value(set: &PropertySet, name: &str, index: c_int, value: Value) -> Result<(), c_int> { set.with_locked(|props| { - let p = props - .iter_mut() - .find(|p| p.name == name) - .ok_or(status::ERR_UNKNOWN)?; - let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?; + let idx = idx(index)?; + let Some(p) = props.iter_mut().find(|p| p.name == name) else { + // 隐式创建:维度 index+1,前导槽位以同值填充(HS 的新属性 + // 默认值语义——新建属性先有一个默认元素再逐位写)。 + let mut values = vec![value.clone(); idx]; + values.push(value); + props.push(crate::property::Property { + name: name.to_string(), + values, + }); + return Ok(()); + }; + // 空属性(宿主预定义的空数组,如 OfxImageEffectPropSupportedContexts) + // 没有类型探针:按 HS 的 index == size 追加语义直接 push。 + if p.values.is_empty() { + if idx != 0 { + return Err(status::ERR_BAD_INDEX); + } + p.values.push(value); + return Ok(()); + } + let probe = &p.values[0]; if Kind::of(probe) != Kind::of(&value) { return Err(status::ERR_UNKNOWN); } - // HS `setValue` 允许 index == size 时追加(ofxhPropertySuite.cpp:284); - // 本 crate 维度固定语义(扩容只能经 define)——越界一律 BadIndex。 - let slot = p.values.get_mut(idx(index)?).ok_or(status::ERR_BAD_INDEX)?; + // HS `setValue` 允许 index == size 时追加(ofxhPropertySuite.cpp:284) + // ——addSupportedContext 等协商属性正是这样逐位增长的。 + if idx == p.values.len() { + p.values.push(value); + return Ok(()); + } + let slot = p.values.get_mut(idx).ok_or(status::ERR_BAD_INDEX)?; *slot = value; Ok(()) }) @@ -262,10 +311,14 @@ fn set_values( values: Vec, ) -> Result<(), c_int> { set.with_locked(|props| { - let p = props - .iter_mut() - .find(|p| p.name == name) - .ok_or(status::ERR_UNKNOWN)?; + let Some(p) = props.iter_mut().find(|p| p.name == name) else { + // 与单元素 set_value 一致:未定义属性按 OFX 语义隐式创建。 + props.push(crate::property::Property { + name: name.to_string(), + values, + }); + return Ok(()); + }; // count == 0 时无类型可探(HS 仍会做 fetchTypedProperty)。 if let Some(first) = p.values.first() { if let Some(v) = values.first() { @@ -426,7 +479,15 @@ unsafe extern "C" fn prop_get_string( if out.is_null() { return Err(status::ERR_VALUE); } - *out = get_string(set, name, index)?; + let r = get_string(set, name, index); + if std::env::var_os("OAK_OFX_TRACE").is_some() { + let status = match &r { + Ok(_) => 0, + Err(c) => *c, + }; + eprintln!("[ofx] propGetString(h={handle:p}, {name}[{index}]) -> {status}"); + } + *out = r?; Ok(()) }) } @@ -476,6 +537,12 @@ unsafe extern "C" fn prop_get_int( } _ => Err(status::FAILED), } + .map_err(|c| { + if std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[ofx] propGetInt({name}) -> {c}"); + } + c + }) }) } } @@ -614,12 +681,13 @@ unsafe extern "C" fn prop_get_dimension( if out.is_null() { return Err(status::ERR_VALUE); } - // HS: fetchProperty 失败(未定义)→ Unknown(ofxhPropertySuite.cpp:992)。 - let dim = set.dimension(name); - if dim == 0 { + // HS: fetchProperty 失败(未定义)→ Unknown(ofxhPropertySuite.cpp:992); + // 已定义的空数组(如协商属性的初始状态)是合法维度 0,不是错误。 + let exists = set.with_locked(|props| props.iter().any(|p| p.name == name)); + if !exists { return Err(status::ERR_UNKNOWN); } - *out = dim as c_int; + *out = set.dimension(name) as c_int; Ok(()) }) }