- oakcodec: full crate incl. real FFmpeg decode/encode via ffmpeg-next (162 tests, 84.4% cov); new oakcodec_encoding_* metadata family (include/codec/format.h, C++ + Rust sides) - oaktask: manager/tasks/project load-save incl. OTIO via oakotio (82 tests, 86.4% cov); concurrent render loop with reorder buffer - oakplugin: M11 phase 1+2 — self-contained OFX host, GL path, ofxColour, pluginrenderer absorbed as render_driver (99 tests, 81.7% cov); instance.h additions documented - oakotio: native serde-based OTIO read/write (24 tests) - oaknode gap fill: dragger/keyframe-helper/multicam C ABI families (113/113 gtest); fixes a latent NodeInputDragger segfault - oaknode Rust skeleton: 43 built-in node type declarations - oakrender/oakstorage: declaration skeletons (implementation pending) - notes.md: gap analysis + tech-debt ledger
231 lines
8.2 KiB
Rust
231 lines
8.2 KiB
Rust
// 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/>.
|
||
|
||
//! 协商语义专项: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<std::sync::Arc<oakplugin::handle::RefBox<oakplugin::instance::Instance>>> {
|
||
unsafe { get::<std::sync::Arc<oakplugin::handle::RefBox<oakplugin::instance::Instance>>>(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() };
|
||
});
|
||
}
|