Files
oak-editor/crates/oakplugin/tests/render_driver_test.rs
T
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
2026-08-11 23:04:48 +08:00

344 lines
8.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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/>.
//! render 驱动测试(M11 §4):pluginrenderer.cpp 语义收编的 CPU 路径
//! ——序列括号、render_job 多输入/参数覆盖、isIdentity 短路、输出
//! 装配像素断言。GL 路径见 gl_render_test.rs。
mod common;
use std::ffi::{c_char, c_int, c_void, CString};
use oakplugin::ffi::{
node_value_type, oakplugin_host_scan, oakplugin_instance_create, oakplugin_instance_free,
oakplugin_instance_get_param, oakplugin_instance_render_begin_sequence,
oakplugin_instance_render_end_sequence, oakplugin_instance_render_job, OakNodeValue,
OakPluginJobTexture, OakPluginJobValue,
};
use oakplugin::handle::CHandle;
const OK: i32 = 0;
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
const IDENTITY_PLUGIN_ID: &str = "org.oak.test-plugin.identity";
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
fn fake_texture(ctx: usize) -> CHandle {
CHandle {
ctx: ctx as *mut c_void,
addref: None,
release: None,
abi_version: 1,
}
}
fn scan_and_create(id: &str) -> 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(id);
unsafe { oakplugin_instance_create(id.as_ptr()) }
}
/// 序列括号 + render_jobCPU):begin → job → end;输出像素为测试
/// 插件常量填充(0.5 RGBA F32alpha=1);RoI 计算成功。
#[cfg(feature = "test-stubs")]
#[test]
fn render_job_cpu_path_and_sequence_brackets() {
use oakplugin::bridge::render::stub;
common::with_host(|| {
stub::reset();
let mut h = scan_and_create(TEST_PLUGIN_ID);
if h.is_null() {
return;
}
stub::setup_dst(8, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let dst = fake_texture(0xA1);
// 未 begin 序列直接 job 也可用(单帧;序列括号是优化语义)。
assert_eq!(
unsafe {
oakplugin_instance_render_job(
h,
dst,
0.0,
0,
0,
std::ptr::null(),
CHandle::null(),
std::ptr::null(),
0,
std::ptr::null(),
0,
CHandle::null(),
)
},
OK
);
// begin/end 括号配对。
assert_eq!(
unsafe { oakplugin_instance_render_begin_sequence(h, 0.0, 10.0, 0) },
OK
);
assert_eq!(
unsafe { oakplugin_instance_render_end_sequence(h, 0.0, 10.0, 0) },
OK
);
// 像素断言:8×4 常量 0.5 / alpha 1。
let pixels = stub::dst_pixels();
let n = pixels.len() / 16;
assert_eq!(n, 32);
for i in 0..n {
let f = |o: usize| {
f32::from_le_bytes(
pixels[i * 16 + o * 4..i * 16 + o * 4 + 4]
.try_into()
.unwrap(),
)
};
assert_eq!(f(0), 0.5, "pixel {i} r");
assert_eq!(f(3), 1.0, "pixel {i} a");
}
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 多输入 + 参数覆盖:inputs 表按 clip 名挂输入纹理;values 覆盖
/// gain 参数(驱动注入实例参数,插件经 param 读取——测试插件 render
/// 不读 gain,故断言回写成功即可)。
#[cfg(feature = "test-stubs")]
#[test]
fn render_job_multi_input_and_param_overrides() {
use oakplugin::bridge::render::stub;
common::with_host(|| {
stub::reset();
let mut h = scan_and_create(TEST_PLUGIN_ID);
if h.is_null() {
return;
}
stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
stub::setup_src(
4,
4,
oakplugin::bridge::render::PIXEL_FORMAT_F32,
vec![0u8; 4 * 4 * 16],
);
let dst = fake_texture(0xA1);
let src = fake_texture(0xA2);
// 参数覆盖:gain = 1.5FLOAT)。
let mut value = OakNodeValue::default();
value.r#type = node_value_type::FLOAT;
value.f = [1.5, 0.0, 0.0, 0.0];
let gain = cs("gain");
let job_values = [OakPluginJobValue {
key: gain.as_ptr(),
value,
}];
// 多输入表:Source → src 纹理。
let source = cs("Source");
let job_inputs = [OakPluginJobTexture {
clip: source.as_ptr(),
texture: src,
}];
assert_eq!(
unsafe {
oakplugin_instance_render_job(
h,
dst,
0.0,
0,
0,
std::ptr::null(),
CHandle::null(),
job_inputs.as_ptr(),
1,
job_values.as_ptr(),
1,
CHandle::null(),
)
},
OK
);
// 覆盖已生效:经 C ABI 读回 gain。
let mut out = OakNodeValue::default();
let g = cs("gain");
assert_eq!(
unsafe { oakplugin_instance_get_param(h, g.as_ptr(), &mut out) },
OK
);
assert_eq!(out.f[0], 1.5, "gain 覆盖应已注入实例参数");
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// isIdentity 短路:identity 变体插件返回 "Source" → render_job 不调
/// render action,直接把 Source 的帧拷入输出(像素与输入一致)。
#[cfg(feature = "test-stubs")]
#[test]
fn render_job_is_identity_shortcircuit() {
use oakplugin::bridge::render::stub;
common::with_host(|| {
stub::reset();
let mut h = scan_and_create(IDENTITY_PLUGIN_ID);
if h.is_null() {
return;
}
// 输入帧:每像素 r=0.1,g=0.2,b=0.3,a=1.0。
let mut pixels = Vec::new();
for _ in 0..4 * 4 {
for v in [0.1f32, 0.2, 0.3, 1.0] {
pixels.extend_from_slice(&v.to_le_bytes());
}
}
stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
stub::setup_src(
4,
4,
oakplugin::bridge::render::PIXEL_FORMAT_F32,
pixels.clone(),
);
let dst = fake_texture(0xA1);
let src = fake_texture(0xA2);
assert_eq!(
unsafe {
oakplugin_instance_render_job(
h,
dst,
0.0,
0,
0,
cs("Source").as_ptr(),
src,
std::ptr::null(),
0,
std::ptr::null(),
0,
CHandle::null(),
)
},
OK
);
// 输出 == 输入(透传帧)。
let out_pixels = stub::dst_pixels();
assert_eq!(out_pixels, pixels, "isIdentity 透传应逐字节拷贝输入帧");
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 默认构建(无桩):render_job 缺桥符号 → 明确失败码而非崩溃
/// (优雅降级;GL 用例同)。
#[cfg(not(feature = "test-stubs"))]
#[test]
fn render_job_graceful_without_bridge() {
common::with_host(|| {
let mut h = scan_and_create(TEST_PLUGIN_ID);
if h.is_null() {
return;
}
// NULL 输出纹理在触达 oakrender 前被插件层校验拒绝(E_INVALID)。
let dst = CHandle::null();
let r = unsafe {
oakplugin_instance_render_job(
h,
dst,
0.0,
0,
0,
std::ptr::null(),
CHandle::null(),
std::ptr::null(),
0,
std::ptr::null(),
0,
CHandle::null(),
)
};
assert_eq!(r, -90001, "NULL 输出纹理 → E_INVALID(不崩溃)");
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 驱动错误路径:非 F32 输出 → 明确失败;取消标记 → 明确失败。
#[cfg(feature = "test-stubs")]
#[test]
fn render_job_error_paths() {
use oakplugin::bridge::render::stub;
use oakplugin::ffi::{oakplugin_instance_cancel, oakplugin_instance_render_job};
common::with_host(|| {
stub::reset();
let mut h = scan_and_create(TEST_PLUGIN_ID);
if h.is_null() {
return;
}
// 非 F32 输出(U8)→ read_dst 拒绝。
stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_U8);
let dst = fake_texture(0xA1);
let r = unsafe {
oakplugin_instance_render_job(
h,
dst,
0.0,
0,
0,
std::ptr::null(),
CHandle::null(),
std::ptr::null(),
0,
std::ptr::null(),
0,
CHandle::null(),
)
};
assert_eq!(r, -90003, "非 F32 输出 → E_FAILED");
// 取消标记 → 驱动入口短路。
stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
assert_eq!(unsafe { oakplugin_instance_cancel(h) }, OK);
let r = unsafe {
oakplugin_instance_render_job(
h,
dst,
0.0,
0,
0,
std::ptr::null(),
CHandle::null(),
std::ptr::null(),
0,
std::ptr::null(),
0,
CHandle::null(),
)
};
assert_eq!(r, -90003, "已取消 → E_FAILED");
unsafe { oakplugin_instance_free(&mut h) };
});
}