// 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 .
//! 协商语义专项:clip 偏好协商矩阵、RoD/RoI、isIdentity、field
//! 透传。当年调试重灾区,每条对照 HostSupport 行为(注释标行号)。
//!
//! 最小测试插件固定声明 RGBA/F32、实现 RoD(1920×1080)与 RoI
//! (回写 region)、isIdentity 恒非透传、无 field——需要插件变体
//! 的矩阵案(回退链、身份透传、field)标记 `// TODO(plugin)`,
//! 随 M11 §2.4 测试插件的变体落地。
mod common;
use std::ffi::{c_char, c_void, CString};
use oakplugin::ffi::{
oakplugin_host_init, oakplugin_host_scan, oakplugin_host_shutdown, oakplugin_instance_create,
oakplugin_instance_free,
};
use oakplugin::handle::{get, CHandle};
const OK: i32 = 0;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
/// 扫描并创建实例;不可用返回空句柄。
fn create_instance() -> CHandle {
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
return CHandle::null();
}
let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap());
let dirs = [dir.as_ptr()];
unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) };
let id = cs(TEST_PLUGIN_ID);
unsafe { oakplugin_instance_create(id.as_ptr()) }
}
/// 句柄 → Instance 引用。
fn instance_of(h: &CHandle) -> Option>> {
unsafe { get::>>(h) }
.cloned()
}
/// 协商顺序:测试插件声明 RGBA/F32、帧率 24——协商结果必须
/// 原样采纳(HS: ofxhImageEffect.cpp:1686-1740 的回灌路径)。
/// 全组合矩阵(RGBA/RGB/Alpha × Float/Half/Byte)需要插件变体:
/// `// TODO(plugin)`。
#[test]
fn clip_preferences_component_depth_matrix() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let prefs = inst.value.get_clip_preferences().expect("协商应成功");
assert_eq!(prefs.output_components, "OfxImageComponentRGBA");
assert_eq!(prefs.output_bit_depth, "OfxBitDepthFloat");
assert_eq!(prefs.frame_rate, 24.0);
// 回灌:clip 实例属性带上了协商结果(clipGetPropertySet 读)。
let output = inst.value.clips.iter().find(|c| c.name == "Output").unwrap();
assert_eq!(
output
.props
.get("OfxImageEffectPropComponents", 0)
.map(|v| format!("{v:?}")),
Some("String(\"OfxImageComponentRGBA\")".into())
);
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// 位深回退链(Half→Byte):宿主当前只宣告 F32 支持、插件声明
/// F32——直通。回退链的插件侧变体 `// TODO(plugin)`。
#[test]
fn bit_depth_fallback_chain() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let prefs = inst.value.get_clip_preferences().unwrap();
assert_eq!(prefs.output_bit_depth, "OfxBitDepthFloat");
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// RoD:插件实现 getRegionOfDefinition → 用插件值(1920×1080,
/// HS: ofxhImageEffect.cpp:1087-1130 的 out args 读取)。
/// "未实现时用输入 RoD 并集"的默认语义需要插件变体:`// TODO(plugin)`。
#[test]
fn region_of_definition_default_and_override() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let rod = inst
.value
.get_region_of_definition(0.0, oakplugin::instance::RenderScale { x: 1.0, y: 1.0 })
.expect("getRoD 应成功");
assert_eq!((rod.x1, rod.y1, rod.x2, rod.y2), (0.0, 0.0, 1920.0, 1080.0));
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// RoI:插件把 region 原样写回 Source 的 per-clip 属性
/// ("OfxImageEffectPropRegionOfInterest_Source");返回值与
/// 请求 region 一致(HS: getRegionsOfInterest 的 per-clip 前缀)。
#[test]
fn regions_of_interest_writeback() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let region = oakplugin::instance::OfxRectD { x1: 10.0, y1: 20.0, x2: 100.0, y2: 200.0 };
let rois = inst
.value
.get_regions_of_interest(
0.0,
oakplugin::instance::RenderScale { x: 1.0, y: 1.0 },
region,
)
.expect("getRoI 应成功");
assert_eq!(rois.len(), 2, "clips 数与 RoI 数一致");
// RoI 只对输入 clip 有意义:Source 回写 region;Output 是
// 宿主预定义的默认(零),渲染驱动只用输入条目。
let source = rois
.get(inst.value.clips.iter().position(|c| c.name == "Source").unwrap())
.unwrap();
assert_eq!((source.x1, source.y1, source.x2, source.y2), (10.0, 20.0, 100.0, 200.0));
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// isIdentity:测试插件恒非透传 → None(render 必须真正执行)。
/// 透传短路(Some)需要插件变体:`// TODO(plugin)`。
#[test]
fn is_identity_shortcircuit() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let identity = inst.value.is_identity(1.5).expect("isIdentity 应成功");
assert!(identity.is_none(), "测试插件恒非透传:{identity:?}");
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// field 透传:插件声明的 field order(OfxFieldNone)原样透传到
/// 协商结果(宿主只透传不处理)。多值矩阵 `// TODO(plugin)`。
#[test]
fn field_passthrough() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let prefs = inst.value.get_clip_preferences().unwrap();
assert_eq!(prefs.field, "OfxFieldNone", "插件声明的 field 应透传");
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// begin/endSequenceRender:配对调用成功;begin 后 timeline
/// getTimeBounds 返回该序列范围(sequence_range 接线)。
#[test]
fn sequence_render_brackets() {
common::with_host(|| {
use oakplugin::instance::OfxRangeD;
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let range = OfxRangeD { min: 10.0, max: 200.0 };
assert!(inst.value.begin_sequence_render(range).is_ok());
assert!(inst.value.end_sequence_render(range).is_ok());
// begin → timeline 上下文带范围(经 render 设置;单测直达
// RenderCtx 的接线见 suites::timeline 测试)。
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}