plugin: instantiate more real-world plugins (FieldOrder, UIColour, context fallback)
CI / Build & test (Linux) (push) Successful in 18m55s
CI / Build & test (Windows) (push) Successful in 33m21s

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.
This commit is contained in:
2026-08-25 21:56:33 +08:00
parent 3792c49854
commit ea9b451d0b
5 changed files with 73 additions and 32 deletions
+8
View File
@@ -78,6 +78,14 @@ impl ClipDescriptor {
props.set_one(CLIP_OPTIONAL, Value::Int(0));
props.set_one(CLIP_IS_MASK, Value::Int(0));
props.set_one(CLIP_FIELD_EXTRACTION, Value::String(cs(CLIP_FIELD_DOUBLED)));
// kOfxImageClipPropFieldOrderofxhClip.cpp 的 clip 描述符必备
// 属性;ofxs `Clip::getFieldOrder()` 无默认值强读——Mirror 这类
// 场感知插件缺它直接 MissingHostFeature)。取值是
// kOfxImageField* 族(ofxImageEffect.h:1288),默认无场。
props.set_one(
"OfxImageClipPropFieldOrder",
Value::String(cs("OfxFieldNone")),
);
props.set_one(CLIP_SUPPORTS_TILES, Value::Int(1));
// ofxColourM11 §4):clip 色彩空间属性族。Colourspace 由宿主
// 在实例化时写入(输入 clip = 工作空间 ACEScg);Preferred 由
+38
View File
@@ -1232,6 +1232,44 @@ impl Host {
})
}
/// 上下文选择 + 实例化(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)与
+14 -30
View File
@@ -753,27 +753,13 @@ pub fn register_plugin_nodes() -> Vec<String> {
continue;
};
// 上下文选择:filter 优先,否则第一个(factory.cpp:171-177)。
let context = if plugin.contexts.iter().any(|c| c == "OfxImageEffectContextFilter") {
"OfxImageEffectContextFilter".to_string()
} else {
match plugin.contexts.first() {
Some(c) => c.clone(),
None => {
eprintln!(
"Skipping OFX plugin with no contexts: {}",
plugin.identifier
);
continue;
}
}
};
// 元数据实例(name/description;建完即弃)。describeInContext
// 失败的插件无法实例化(常见于只支持 Vegas 立体声等厂商套件
// 的插件)——记录原因,不静默跳过。
let inst = match host.create_instance(&plugin.identifier, Some(&context)) {
Ok(inst) => inst,
// 元数据实例(name/description;建完即弃)。上下文选择:
// filter 优先、实例化失败按序回退(TrackerPM 这类插件的
// createInstance 只在 tracker/general/paint 下能跑)。所有
// 上下文都失败的插件无法实例化(常见于只支持 Vegas 立体声
// 等厂商套件的插件)——记录原因,不静默跳过。
let (context, inst) = match host.create_instance_preferred(&plugin.identifier) {
Ok(pair) => pair,
Err(e) => {
eprintln!("[ofx] {}: instance creation failed: {e}", plugin.identifier);
continue;
@@ -955,15 +941,13 @@ fn shared_plugin_instance(identifier: &str) -> Option<u64> {
return Some(id);
}
let host = Host::global();
let plugin = host.cache.find(identifier)?;
// 上下文选择与 [`register_plugin_nodes`] 一致(filter 优先,否则
// 首个支持上下文;factory.cpp:171-177)。
let context = if plugin.contexts.iter().any(|c| c == "OfxImageEffectContextFilter") {
"OfxImageEffectContextFilter".to_string()
} else {
plugin.contexts.first()?.clone()
};
let inst = host.create_instance(identifier, Some(&context)).ok()?;
if host.cache.find(identifier).is_none() {
return None;
}
// 上下文选择:filter 优先、实例化失败按序回退(与
// [`register_plugin_nodes`] 同一策略,见
// [`Host::create_instance_preferred`])。
let (_context, inst) = host.create_instance_preferred(identifier).ok()?;
let id = register_instance(inst);
cache
.lock()
+7 -2
View File
@@ -479,7 +479,10 @@ impl ParamDef {
);
props.set_one(P_ANIMATES, Value::Int(1));
props.set_one(P_PARAMETRIC_DIMENSION, Value::Int(1));
props.define(P_PARAMETRIC_UI_COLOUR, vec![]);
// UIColour 默认未设:不预定义(插件首写时按 OFX 语义
// 隐式创建并允许逐位追加;预定义空数组会让 index >= 1
// 的写入炸 BadIndex——ColorLookup/HueCorrect 的
// describeInContext 失败根因)。
props.set_one(P_PARAMETRIC_INTERACT_BG, Value::Pointer(std::ptr::null_mut()));
props.define(
P_PARAMETRIC_RANGE,
@@ -913,7 +916,9 @@ mod tests {
d.props.get(P_PARAMETRIC_RANGE, 1),
Some(Value::Double(1.0))
));
assert_eq!(d.props.dimension(P_PARAMETRIC_UI_COLOUR), 0);
// UIColour 默认未设:属性不存在(插件首写隐式创建),而不是
// 预定义的空数组(空数组会把 index >= 1 的写入挡成 BadIndex)。
assert!(d.props.get(P_PARAMETRIC_UI_COLOUR, 0).is_none());
assert!(matches!(
d.props.get(P_PARAMETRIC_INTERACT_BG, 0),
Some(Value::Pointer(p)) if p.is_null()
+6
View File
@@ -224,6 +224,9 @@ fn set_value(set: &PropertySet, name: &str, index: c_int, value: Value) -> Resul
// 没有类型探针:按 HS 的 index == size 追加语义直接 push。
if p.values.is_empty() {
if idx != 0 {
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] propSet into empty predefined array: {name}[{idx}]");
}
return Err(status::ERR_BAD_INDEX);
}
p.values.push(value);
@@ -239,6 +242,9 @@ fn set_value(set: &PropertySet, name: &str, index: c_int, value: Value) -> Resul
p.values.push(value);
return Ok(());
}
if idx >= p.values.len() && std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] propSet out of range: {name}[{idx}] (dim {})", p.values.len());
}
let slot = p.values.get_mut(idx).ok_or(status::ERR_BAD_INDEX)?;
*slot = value;
Ok(())