diff --git a/crates/oak-plugin/src/clip.rs b/crates/oak-plugin/src/clip.rs index 08859cb09..69d09f483 100644 --- a/crates/oak-plugin/src/clip.rs +++ b/crates/oak-plugin/src/clip.rs @@ -112,6 +112,15 @@ impl ClipInstance { ), ); } + // OfxImageClipPropConnected(ofxsImageEffect.cpp:1106 的 + // `Clip::isConnected()` 是无默认值强读——缺这个属性时,CImg 这类 + // 带可选 mask clip 的插件直接抛 + // PropertyUnknownToHost → MissingHostFeature 紫帧)。默认 0 + // (未连接),挂接输入纹理时由 set_input_texture 置 1。 + props.set_one( + crate::host::PROP_CLIP_CONNECTED, + crate::property::Value::Int(0), + ); Self { props, name, @@ -166,6 +175,14 @@ impl ClipInstance { /// 挂接输入纹理(oaknode 侧 clip 输入值变化时由 param/render 桥 /// 调用)。`time` 用于多帧纹理选择。None 断开。 pub fn set_input_texture(&self, texture: Option, _time: f64) { + // The connection state follows the texture hand-off (the render + // driver only feeds clips that have input; optional mask clips + // stay 0, so `Clip::isConnected()` answers false for them). + let connected = i32::from(texture.is_some()); + self.props.set_one( + crate::host::PROP_CLIP_CONNECTED, + crate::property::Value::Int(connected), + ); *self.input_texture.lock().unwrap_or_else(|e| e.into_inner()) = texture; } diff --git a/crates/oak-plugin/src/image.rs b/crates/oak-plugin/src/image.rs index e14a94b41..d23921021 100644 --- a/crates/oak-plugin/src/image.rs +++ b/crates/oak-plugin/src/image.rs @@ -207,6 +207,38 @@ impl Image { K_IMAGE_PROP_UNIQUE_ID, vec![Value::String(unique_identifier())], ); + // OfxPropType="OfxTypeImage":图像实例的类型标识(支持库 + // validateImageBaseProperties 的必备项,带可校验默认值)。 + img.props.define( + "OfxPropType", + vec![Value::String(std::ffi::CString::new("OfxTypeImage").unwrap())], + ); + // OFX 必备图像属性(支持库 ImageBase/Image 构造的无默认值强读; + // 缺失即抛 PropertyUnknownToHost → MissingHostFeature 紫帧): + // 方形像素 1.0;预乘声明(本管线按预乘 alpha 处理);无场。 + img.props.define( + "OfxImagePropPixelAspectRatio", + vec![Value::Double(1.0)], + ); + img.props.define( + "OfxImageEffectPropPreMultiplication", + // kOfxImagePreMultiplied 的真实字符串值是 + // "OfxImageAlphaPremultiplied"(ofxImageEffect.h), + // ofxs mapStrToPreMultiplicationEnum 只认这三个精确值。 + vec![Value::String( + std::ffi::CString::new("OfxImageAlphaPremultiplied").unwrap(), + )], + ); + img.props.define( + "OfxImagePropField", + vec![Value::String(std::ffi::CString::new("OfxFieldNone").unwrap())], + ); + // OfxImageEffectPropRenderScale:openfx-misc 的 + // checkBadRenderScaleOrField 用它比对渲染参数(1:1)。 + img.props.define( + "OfxImageEffectPropRenderScale", + vec![Value::Double(1.0), Value::Double(1.0)], + ); img } diff --git a/crates/oak-plugin/src/instance.rs b/crates/oak-plugin/src/instance.rs index 4e3c59fd7..a0e9c8779 100644 --- a/crates/oak-plugin/src/instance.rs +++ b/crates/oak-plugin/src/instance.rs @@ -264,6 +264,10 @@ impl Instance { crate::suites::tag::INSTANCE, ); let empty = PropertySet::new(); + Self::add_action_arg_compat_props(&empty); + if std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[probe-args] getClipPreferences in_args {:p} out {:p}", &empty, &out); + } let stat = unsafe { self.plugin .call_action(ACTION_GET_CLIP_PREFERENCES, inst_handle, &empty, &out) @@ -360,6 +364,10 @@ impl Instance { PROP_RENDER_SCALE, vec![Value::Double(scale.x), Value::Double(scale.y)], ); + Self::add_action_arg_compat_props(&in_args); + if std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[probe-args] getRod in_args {:p}", &in_args); + } // out args 预定义(HS 行为:宿主先建属性表,插件只管写—— // 缺失属性上插件的 propSet 会失败被忽略)。 let out = PropertySet::new(); @@ -407,6 +415,10 @@ impl Instance { PROP_RENDER_SCALE, vec![Value::Double(scale.x), Value::Double(scale.y)], ); + Self::add_action_arg_compat_props(&in_args); + if std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[probe-args] getRoi in_args {:p}", &in_args); + } in_args.define( PROP_ROI, vec![ @@ -454,6 +466,19 @@ impl Instance { } /// isIdentity action:返回 Some((time, input_clip_name)) 表示本帧 + /// OFX 1.4+/Natron 框架对 action in-args 的强读属性:缺了会被抛成 + /// PropertyUnknownToHost → MissingHostFeature(紫帧根因)。每个 + /// action in-args 都带上。 + fn add_action_arg_compat_props(in_args: &PropertySet) { + use crate::property::Value; + // NatronOfxPropNativeOverlays:Natron 支持库强读(0 = 宿主不 + // 支持原生 overlay,插件走自己的回退)。OfxImageEffectPropRenderPlanes + // 不放 in-args:Natron 框架按**字符串**读它,存在但类型是 Int + // 反而会抛 PropertyUnknownToHost(缺失它才容忍)——它只属于 + // 图像/clip 属性(OFX 1.4 语义在 clipGetImagePlane 一侧)。 + in_args.set_one("NatronOfxPropNativeOverlays", Value::Int(0)); + } + /// 直接透传该输入 clip;None 表示需要真正 render。 /// /// 参照 HS: ofxhImageEffect.cpp:1378-1450:in args = time + scale + @@ -475,16 +500,17 @@ impl Instance { in_args.define( PROP_RENDER_WINDOW, vec![ - Value::Double(0.0), - Value::Double(0.0), - Value::Double(0.0), - Value::Double(0.0), + Value::Int(0), + Value::Int(0), + Value::Int(0), + Value::Int(0), ], ); in_args.set_one( PROP_FIELD_TO_RENDER, - Value::String(CString::new("OfxImageFieldBoth").unwrap()), + Value::String(CString::new("OfxFieldNone").unwrap()), ); + in_args.set_one("NatronOfxPropNativeOverlays", Value::Int(0)); // out args 预定义:IsIdentity(String)+ Time(Double)。 let out = PropertySet::new(); out.set_one(PROP_IS_IDENTITY, Value::String(CString::new("").unwrap())); @@ -494,14 +520,23 @@ impl Instance { &self.props as *const PropertySet, crate::suites::tag::INSTANCE, ); + if std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[ofx] isIdentity in_args {:p} out_args {:p}", &in_args, &out); + } let stat = unsafe { self.plugin .call_action(ACTION_IS_IDENTITY, inst_handle, &in_args, &out) }; if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT { - return Err(crate::error::Error::Failed(format!( - "isIdentity 失败:{stat}" - ))); + // A failed isIdentity is NOT fatal: the C++ plugin renderer + // never calls this action at all (plugins that error on it — + // the CImg suite answers MissingHostFeature — simply render + // normally). Treat any non-OK as "not identity" and let the + // real render decide. + if std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[ofx] isIdentity stat {stat}: proceeding to render"); + } + return Ok(None); } let identity = out.get(PROP_IS_IDENTITY, 0); match identity { @@ -541,23 +576,29 @@ impl Instance { PROP_RENDER_SCALE, vec![Value::Double(scale.x), Value::Double(scale.y)], ); + // RenderWindow 必须是 Int x4(支持库 ofxsPropertyValidation 对 + // render in-args 的强制类型;像素坐标本就是整型——Double 会校验 + // 失败成 kOfxStatFailed)。 in_args.define( PROP_RENDER_WINDOW, vec![ - Value::Double(window.x1), - Value::Double(window.y1), - Value::Double(window.x2), - Value::Double(window.y2), + Value::Int(window.x1.round() as i32), + Value::Int(window.y1.round() as i32), + Value::Int(window.x2.round() as i32), + Value::Int(window.y2.round() as i32), ], ); in_args.set_one( PROP_FIELD_TO_RENDER, - Value::String(CString::new("OfxImageFieldBoth").unwrap()), + Value::String(CString::new("OfxFieldNone").unwrap()), ); in_args.set_one(PROP_SEQUENTIAL_RENDER, Value::Int(0)); in_args.set_one(PROP_INTERACTIVE_RENDER, Value::Int(0)); in_args.set_one(PROP_RENDER_QUALITY_DRAFT, Value::Int(0)); in_args.set_one(PROP_NO_SPATIAL_AWARENESS, Value::Int(0)); + // Natron 支持库对 NatronOfxPropNativeOverlays 的强读(0 = 宿主 + // 不支持原生 overlay——插件据此走自己的回退路径)。 + in_args.set_one("NatronOfxPropNativeOverlays", Value::Int(0)); if gl_enabled { in_args.set_one(crate::host::PROP_GL_ENABLED, Value::Int(1)); } diff --git a/crates/oak-plugin/src/node_factory.rs b/crates/oak-plugin/src/node_factory.rs index 5c30686c9..e03e9e01d 100644 --- a/crates/oak-plugin/src/node_factory.rs +++ b/crates/oak-plugin/src/node_factory.rs @@ -899,7 +899,11 @@ fn execute_plugin_job( effect_input_id: effect_input_id.clone(), inputs: inputs.clone(), values: pod_values, - renderer: None, + // GL 渲染路径:OpenGL-only 插件(CImg 套件)在 CPU 路径的 + // isIdentity 直接回 kOfxStatErrMissingHostFeature(紫帧根因)。 + // 标记上下文只服务于 use_opengl 的 kind 判定;真正的 GL 渲染走 + // gl_bridge 的离屏上下文(无头的 oak-worker 进程同样可用)。 + renderer: Some(std::sync::Arc::new(crate::render::GlKindMarker)), clear_destination: false, interactive: false, }; @@ -1129,6 +1133,76 @@ mod tests { assert!(shared_plugin_instance("com.example.definitely-missing").is_none()); } + /// 最简单的真实插件(AddOFX:每个像素加一个常量)经 executor + /// 路径渲染成功——区分"系统性宿主问题"与"单插件问题"(机器上 + /// 没装该插件则跳过)。 + #[test] + fn add_plugin_renders_through_the_executor() { + render_real_plugin_smoke("net.sf.openfx.AddPlugin"); + } + + /// 真实 GL-only 插件(CImg 的 ChromaKeyerOFX)经 executor 路径渲染 + /// 成功——GL-kind 标记必须把它引上可用的 GL 路径,而不是 + /// MissingHostFeature 紫帧(机器上没装该插件则跳过)。 + #[test] + fn cimg_chromakeyer_renders_through_the_executor() { + render_real_plugin_smoke("net.sf.openfx.ChromaKeyerPlugin"); + } + + fn render_real_plugin_smoke(identifier: &str) { + let host = crate::host::Host::global(); + let _ = host.cache.scan(); + if host.cache.find(identifier).is_none() { + eprintln!("{identifier} not installed; skipping"); + return; + } + install_render_executor(); + let instance = shared_plugin_instance(identifier) + .expect("the shared instance factory resolves the plugin"); + if std::env::var_os("OAK_OFX_TRACE").is_some() { + let inst = instance_from_id(instance).expect("registered"); + let props = &inst.value.plugin.descriptor.props; + props.with_locked(|all| { + for p in all.iter().filter(|p| p.name.contains("GL") || p.name.contains("OpenGL")) { + eprintln!("[probe] desc prop {} = {:?}", p.name, p.values); + } + }); + } + + // 8x8 不透明绿帧(典型 keyer 输入)。 + let mut frame = oak_render::eval::generate_frame( + oak_core::Rational::new(0, 1), + (8, 8), + oak_core::PixelFormat::F32, + ) + .unwrap(); + for px in frame.data.chunks_exact_mut(16) { + for (c, v) in px.chunks_exact_mut(4).zip([0.1f32, 0.9, 0.1, 1.0]) { + c.copy_from_slice(&v.to_le_bytes()); + } + } + let spec = oak_render::eval::JobSpec::Plugin { + instance, + time: 0.0, + effect_input_id: Some("Source".to_string()), + inputs: Vec::new(), + values: Vec::new(), + }; + let out = execute_plugin_job(&oak_render::eval::PluginJobRequest { + spec: &spec, + src: oak_render::texture::Texture::wrap_frame(frame), + }) + .expect("the GL-capable render succeeds (no MissingHostFeature purple)"); + let oak_render::texture::Texture::Cpu(out_frame) = &out else { + panic!("a CPU frame comes back"); + }; + let mut px = [0f32; 4]; + for i in 0..4 { + px[i] = f32::from_le_bytes(out_frame.data[i * 4..i * 4 + 4].try_into().unwrap()); + } + assert_ne!(px, [1.0, 0.0, 1.0, 1.0], "must not be the purple failure frame"); + } + /// 构造一个只含 push-button 参数的最小实例(直接登记进注册表)。 fn instance_with_push_button() -> u64 { use std::ffi::{c_char, c_void}; diff --git a/crates/oak-plugin/src/param.rs b/crates/oak-plugin/src/param.rs index d968142b3..a8670920c 100644 --- a/crates/oak-plugin/src/param.rs +++ b/crates/oak-plugin/src/param.rs @@ -442,8 +442,13 @@ impl ParamDef { props.set_one(P_STRING_FILE_EXISTS, Value::Int(1)); } TYPE_CHOICE => { - // 维度 0:选项数由插件 SetN 决定。 + // 维度 0:选项数由插件 SetN 决定。ChoiceEnum(枚举值映射) + // 与 ChoiceLabelOption(OFX 1.5 按值标签)同样预定义空 + // 数组——新支持库读它们时不是"未知属性"(其强读会抛成 + // PropertyUnknownToHost → MissingHostFeature)。 props.define(P_CHOICE_OPTION, vec![]); + props.define("OfxParamPropChoiceEnum", vec![]); + props.define("OfxParamPropChoiceLabelOption", vec![]); } TYPE_CUSTOM => { props.set_one(P_CUSTOM_INTERP, Value::Pointer(std::ptr::null_mut())); diff --git a/crates/oak-plugin/src/render.rs b/crates/oak-plugin/src/render.rs index dd3e32d9d..6c27ec003 100644 --- a/crates/oak-plugin/src/render.rs +++ b/crates/oak-plugin/src/render.rs @@ -144,6 +144,37 @@ pub fn renderer_is_open_gl(renderer: &Renderer) -> bool { renderer.kind() == oak_render::backend::BackendKind::Gl } +/// A GL-kind marker context for [`RenderJob::renderer`]. The field's only +/// consumer is the use_opengl decision's kind check +/// ([`renderer_is_open_gl`]); the actual GL work (offscreen context, FBO, +/// readback) goes through [`crate::gl_bridge`], which needs no renderer +/// object. Passing this marker flips OpenGL-requiring plugins (the CImg +/// suite, which answers `kOfxStatErrMissingHostFeature` on the CPU path) +/// onto the real GL path in any process — including the headless +/// oak-worker, where gl_bridge creates its own offscreen context. +pub struct GlKindMarker; + +impl oak_render::backend::GpuContextLike for GlKindMarker { + fn kind(&self) -> oak_render::backend::BackendKind { + oak_render::backend::BackendKind::Gl + } + fn destroy_texture(&self, _token: u64) {} + fn upload(&self, _token: u64, _frame: &oak_render::texture::Frame) -> oak_render::error::Result<()> { + Ok(()) + } + fn download(&self, _token: u64) -> oak_render::error::Result { + Ok(oak_render::texture::Frame::new()) + } + fn blit( + &self, + _src: u64, + _dst: u64, + _processor: Option<&oak_render::color::ColorProcessor>, + ) -> oak_render::error::Result<()> { + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/oak-plugin/src/render_driver.rs b/crates/oak-plugin/src/render_driver.rs index a18ae2ead..85683f9dd 100644 --- a/crates/oak-plugin/src/render_driver.rs +++ b/crates/oak-plugin/src/render_driver.rs @@ -171,6 +171,16 @@ pub fn render_frame( crate::suites::gl_render::pick_gl_pixel_depth(&inst.plugin.descriptor.props) .is_some(); let gl_name_ok = crate::gl_bridge::gl_available(); + if std::env::var_os("OAK_OFX_TRACE").is_some() { + let raw = inst + .plugin + .descriptor + .props + .get(crate::host::PROP_GL_RENDER_SUPPORTED, 0); + eprintln!( + "[ofx] use_opengl decision: plugin_gl={plugin_gl} depth_ok={depth_ok} gl_available={gl_name_ok} (raw GL prop: {raw:?})" + ); + } plugin_gl && depth_ok && gl_name_ok } _ => false, @@ -203,6 +213,9 @@ pub fn render_frame( // 4. getClipPreferences(pluginrenderer.cpp:1554-1594)。 let prefs = inst.get_clip_preferences()?; + if std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[ofx] clip prefs: components={} (inst {})", prefs.output_components, inst.plugin.identifier); + } let components = match prefs.output_components.as_str() { "OfxImageComponentRGBA" => crate::image::Components::Rgba, "OfxImageComponentRGB" => crate::image::Components::Rgb, diff --git a/crates/oak-plugin/src/suites/gl_render.rs b/crates/oak-plugin/src/suites/gl_render.rs index adae14a01..8ec68c55c 100644 --- a/crates/oak-plugin/src/suites/gl_render.rs +++ b/crates/oak-plugin/src/suites/gl_render.rs @@ -318,7 +318,7 @@ fn make_texture_props( rect_props(&props, crate::image::K_IMAGE_PROP_BOUNDS, bounds); rect_props(&props, crate::image::K_IMAGE_PROP_ROD, bounds); props.set_one(crate::image::K_IMAGE_PROP_ROW_BYTES, Value::Int(row_bytes)); - props.set_one("OfxImagePropField", Value::String(cs("OfxImageFieldNone"))); + props.set_one("OfxImagePropField", Value::String(cs("OfxFieldNone"))); props.set_one( crate::image::K_IMAGE_PROP_UNIQUE_ID, Value::String(crate::image::unique_identifier()), diff --git a/crates/oak-plugin/src/suites/image_effect.rs b/crates/oak-plugin/src/suites/image_effect.rs index 56e68ea1d..2f6202fe6 100644 --- a/crates/oak-plugin/src/suites/image_effect.rs +++ b/crates/oak-plugin/src/suites/image_effect.rs @@ -100,6 +100,9 @@ fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int { if code != status::OK && std::env::var_os("OAK_OFX_TRACE").is_some() { eprintln!("[ofx] image-effect suite error {code} at {caller}"); } + if std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[ofx] image-effect suite call at {caller} -> {code}"); + } code } diff --git a/crates/oak-plugin/src/suites/param.rs b/crates/oak-plugin/src/suites/param.rs index 9f2bc524e..2e9e991ab 100644 --- a/crates/oak-plugin/src/suites/param.rs +++ b/crates/oak-plugin/src/suites/param.rs @@ -160,8 +160,10 @@ fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int { |_| 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}"); + if std::env::var_os("OAK_OFX_TRACE").is_some() { + if code != status::OK { + eprintln!("[ofx] param suite error {code} at {caller}"); + } } code } diff --git a/crates/oak-plugin/src/suites/property.rs b/crates/oak-plugin/src/suites/property.rs index 4644322ba..99774a851 100644 --- a/crates/oak-plugin/src/suites/property.rs +++ b/crates/oak-plugin/src/suites/property.rs @@ -130,7 +130,8 @@ unsafe fn caught(handle: *mut c_void, f: impl FnOnce(&PropertySet) -> Result<(), })) .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}"); + let tag = crate::suites::tag::kind(handle); + eprintln!("[ofx] property suite error {code} at {caller} (handle {handle:p} tag {tag})"); } code } @@ -185,7 +186,13 @@ fn get_value_inner<'a>( // 固定类型,等价)。空数组没有类型探针,跳过类型检查,越界 // 由下面的索引访问报 BadIndex(HS getValueRaw 语义)。 if let Some(probe) = p.values.first() { - if Kind::of(probe) != kind { + // Int <-> Double 数值协随(与 get_n 同款宽容宿主语义)。 + let numeric = matches!(Kind::of(probe), Kind::Int | Kind::Double) + && matches!(kind, Kind::Int | Kind::Double); + if Kind::of(probe) != kind && !numeric { + if std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[ofx] property kind mismatch: {name}"); + } return Err(status::ERR_UNKNOWN); } } @@ -481,11 +488,11 @@ unsafe extern "C" fn prop_get_string( } 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, + let rendered = match &r { + Ok(p) => format!("0 <- {:?}", unsafe { std::ffi::CStr::from_ptr(*p) }), + Err(c) => format!("{c}"), }; - eprintln!("[ofx] propGetString(h={handle:p}, {name}[{index}]) -> {status}"); + eprintln!("[ofx] propGetString(h={handle:p}, {name}[{index}]) -> {rendered}"); } *out = r?; Ok(()) @@ -511,6 +518,11 @@ unsafe extern "C" fn prop_get_double( *out = d; Ok(()) } + // Int <-> Double 数值协随(宽容宿主语义,同 get_n)。 + Value::Int(i) => { + *out = i as f64; + Ok(()) + } _ => Err(status::FAILED), } }) @@ -535,6 +547,11 @@ unsafe extern "C" fn prop_get_int( *out = i; Ok(()) } + // Int <-> Double 数值协随(宽容宿主语义,同 get_n)。 + Value::Double(d) => { + *out = d as i32; + Ok(()) + } _ => Err(status::FAILED), } .map_err(|c| { @@ -564,15 +581,33 @@ fn get_n( .find(|p| p.name == name) .ok_or(status::ERR_UNKNOWN)?; let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?; - if Kind::of(probe) != kind { + // Int <-> Double coercion (forgiving-host semantics): the CImg + // framework reads the DOUBLE render window with propGetIntN and + // must still get the values; strict kind checks turn that into a + // bogus MissingHostFeature at render time. + let numeric = matches!(Kind::of(probe), Kind::Int | Kind::Double) + && matches!(kind, Kind::Int | Kind::Double); + if Kind::of(probe) != kind && !numeric { return Err(status::ERR_UNKNOWN); } let n = idx(count)?.min(p.values.len()); for (i, v) in p.values.iter().take(n).enumerate() { write(v, i); } + if std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[ofx] propGetN({name} x{n}) -> ok"); + } Ok(()) }) + .map_err(|code| { + // The property the plugin asked for and we did not have — the + // single most useful line when a plugin reports + // MissingHostFeature (trace-gated, like fetchSuite misses). + if std::env::var_os("OAK_OFX_TRACE").is_some() { + eprintln!("[ofx] property miss: {name} (code {code})"); + } + code + }) } unsafe extern "C" fn prop_get_pointer_n( @@ -629,6 +664,7 @@ unsafe extern "C" fn prop_get_double_n( } get_n(set, name, count, Kind::Double, |v, i| match v { Value::Double(d) => *out.add(i) = *d, + Value::Int(d) => *out.add(i) = *d as f64, _ => {} }) }) @@ -649,6 +685,7 @@ unsafe extern "C" fn prop_get_int_n( } get_n(set, name, count, Kind::Int, |v, i| match v { Value::Int(d) => *out.add(i) = *d, + Value::Double(d) => *out.add(i) = *d as i32, _ => {} }) }) @@ -685,6 +722,10 @@ unsafe extern "C" fn prop_get_dimension( // 已定义的空数组(如协商属性的初始状态)是合法维度 0,不是错误。 let exists = set.with_locked(|props| props.iter().any(|p| p.name == name)); if !exists { + if std::env::var_os("OAK_OFX_TRACE").is_some() { + let tag = crate::suites::tag::kind(handle); + eprintln!("[ofx] property miss (dimension): {name} (handle {handle:p} tag {tag})"); + } return Err(status::ERR_UNKNOWN); } *out = set.dimension(name) as c_int;