refactor: workspace layout — crates/, app at root, legacy C++ removed

Single mechanical restructure commit:
- root Cargo.toml = oakapp bin + workspace; one cargo build produces
  oakapp, oak-cli, oak-worker, liboakengine.dylib
- app/rust/src -> src/ (app at repo root, no rust/ nesting)
- src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore;
  src/bindings/oakotio -> crates/oakotio; src/engine/rust ->
  crates/oakengine (keeps cdylib+staticlib+rlib)
- public C headers include/<mod>/ -> crates/oakengine/include/<mod>/
- OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone)
- legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/,
  app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets
  submodule, otio-install, all build-* output (~40GB)
- oakstorage kept but excluded from the workspace (skeleton w/ todos);
  gpui excluded (own workspace)
- verified: cargo build green, cargo test --workspace 1845/0
  (with the documented OCIO_RS_* env override for the homebrew OCIO)
This commit is contained in:
2026-08-10 20:24:25 +08:00
parent f8540e3892
commit 013a175707
4212 changed files with 8331 additions and 2274987 deletions
+34
View File
@@ -0,0 +1,34 @@
// 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/>.
//! bridge:oak 其余模块的 C ABI 调用——直接 Rust 调用(单库化,见
//! `docs/zh/plans/riir/single-lib.md`)。
//!
//! 每个子模块(node/render/undo)调用对应 crate 的 `ffi` 导出(同名
//! `#[no_mangle]` 符号仍从 dylib 导出供外部 C ABI 使用;内部调用绕过
//! 它们)。句柄全部是共享的 [`crate::handle::CHandle`]。
//!
//! ## 双态实现(所有子模块统一)
//!
//! - 默认:直接调用 oaknode/oakrender/oakundo 的 ffi;
//! - `--features test-stubs`:桥走库内状态桩(各子模块的
//! [`node::stub`]/[`render::stub`]/[`undo::stub`],纯 Rust、无
//! `#[no_mangle]`,与真实 crate 的导出不冲突)——像素路径可在无
//! 真实 GL/GPU 的环境下跑通。两种形态的调用面完全一致。
pub mod node;
pub mod render;
pub mod undo;
+425
View File
@@ -0,0 +1,425 @@
// 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/>.
//! oaknode C ABI 导入(include/node/node.h 中 param 桥用到的子集)。
//!
//! ## Value 布局冻结(M11 第 1 期)
//!
//! [`Value`] 即 include/node/node.h:93 的 `oaknode_value` POD,字段
//! 逐字一致(type/num/den/f[4];`type` 取值见 [`node_value_type`])。
//! 字符串族输入(k_file/k_text/k_font/k_str_combo,node.h:48-52)没有
//! POD 表示——走 `*_input_string_*` 专用函数(本桥的
//! [`set_input_string_undoable`]);`OAKNODE_VALUE_STRING` 的 POD 里
//! 不携带字符串数据。`crate::ffi::OakNodeValue` 与此同布局(出口层
//! 的镜像,两处独立声明避免模块环)。
//!
//! ## 双态实现(同 [`crate::bridge::render`])
//!
//! - 默认:直接 Rust 调用 oaknode 的 `ffi`(单库化,见
//! `docs/zh/plans/riir/single-lib.md`);
//! - `--features test-stubs`:库内状态桩([`stub`],纯 Rust、无
//! `#[no_mangle]`,与真实 oaknode 的导出不冲突)——节点值、undo
//! 命令全链路可在 cargo test 跑通。两种形态的调用面完全一致。
use std::ffi::c_char;
use crate::bridge::undo::CommandHandle;
use crate::handle::CHandle;
/// oaknode 节点句柄(值型)。
pub type NodeHandle = crate::handle::CHandle;
/// oaknode_value_type 的取值(node.h:74;与
/// `crate::ffi::node_value_type` 逐值一致)。
pub mod node_value_type {
/// OAKNODE_VALUE_NONE。
pub const NONE: i32 = 0;
/// OAKNODE_VALUE_INT。
pub const INT: i32 = 1;
/// OAKNODE_VALUE_FLOAT。
pub const FLOAT: i32 = 2;
/// OAKNODE_VALUE_BOOL。
pub const BOOL: i32 = 3;
/// OAKNODE_VALUE_RATIONAL。
pub const RATIONAL: i32 = 4;
/// OAKNODE_VALUE_COLOR。
pub const COLOR: i32 = 5;
/// OAKNODE_VALUE_VEC2。
pub const VEC2: i32 = 6;
/// OAKNODE_VALUE_VEC3。
pub const VEC3: i32 = 7;
/// OAKNODE_VALUE_VEC4。
pub const VEC4: i32 = 8;
/// OAKNODE_VALUE_COMBO。
pub const COMBO: i32 = 9;
/// OAKNODE_VALUE_STRING。
pub const STRING: i32 = 10;
}
/// oaknode_value(include/node/node.h:93,字段逐字一致)。
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Value {
/// 类型([`node_value_type`])。
pub r#type: i32,
/// INT/COMBO 值、BOOL 0/1、RATIONAL 分子。
pub num: i64,
/// RATIONAL 分母。
pub den: i64,
/// FLOAT f[0];VEC2/3/4 f[0..n-1];COLOR r,g,b,a。
pub f: [f64; 4],
}
impl Value {
/// 类型化构造:整数 / choice 索引。
pub const fn int(v: i64) -> Self {
Self {
r#type: node_value_type::INT,
num: v,
den: 0,
f: [0.0; 4],
}
}
/// 类型化构造:浮点。
pub const fn float(v: f64) -> Self {
Self {
r#type: node_value_type::FLOAT,
num: 0,
den: 0,
f: [v, 0.0, 0.0, 0.0],
}
}
/// 类型化构造:布尔。
pub const fn bool_(v: bool) -> Self {
Self {
r#type: node_value_type::BOOL,
num: v as i64,
den: 0,
f: [0.0; 4],
}
}
/// 类型化构造:choice(COMBO)。
pub const fn combo(v: i64) -> Self {
Self {
r#type: node_value_type::COMBO,
num: v,
den: 0,
f: [0.0; 4],
}
}
/// 类型化构造:颜色。
pub const fn color(r: f64, g: f64, b: f64, a: f64) -> Self {
Self {
r#type: node_value_type::COLOR,
num: 0,
den: 0,
f: [r, g, b, a],
}
}
/// 类型化构造:vec2/3/4(长度按 f 数组尾部 0 判定)。
pub const fn vec(v: &[f64]) -> Self {
let t = match v.len() {
2 => node_value_type::VEC2,
3 => node_value_type::VEC3,
_ => node_value_type::VEC4,
};
let mut f = [0.0; 4];
let mut i = 0;
while i < v.len() && i < 4 {
f[i] = v[i];
i += 1;
}
Self {
r#type: t,
num: 0,
den: 0,
f,
}
}
/// 类型化构造:字符串族(POD 不携带数据;值经
/// [`set_input_string_undoable`] 传递)。
pub const fn string() -> Self {
Self {
r#type: node_value_type::STRING,
num: 0,
den: 0,
f: [0.0; 4],
}
}
}
// ---- 桥调用面 ------------------------------------------------------------
/// 按身份取节点句柄(M9 身份注册表;`oaknode_node_from_identity`)。
/// 身份未登记 / 符号缺失 → 空句柄。
pub(crate) unsafe fn node_from_identity(id: usize) -> NodeHandle {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::node_from_identity_impl(id) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oaknode::ffi::node::oaknode_node_from_identity(id) }
}
}
/// 写输入值的标准值并产出一条 undo 命令(`*out` 收到拥有型命令句柄;
/// `oaknode_node_set_input_undoable`,node.h:327)。字符串族输入走
/// [`set_input_string_undoable`]。失败(含符号缺失)返回负错误码。
///
/// # Safety
/// `input`/`value`/`out` 必须指向有效内存;`node` 是有效句柄。
pub(crate) unsafe fn set_input_undoable(
node: NodeHandle,
input: *const c_char,
value: *const Value,
out: *mut CommandHandle,
) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::set_input_undoable_impl(node, input, value, out) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe {
oaknode::ffi::node::oaknode_node_set_input_undoable(
node,
input,
value as *const oaknode::value::OakNodeValue,
out,
)
}
}
}
/// 写字符串族输入的标准值并产出一条 undo 命令
/// (`oaknode_node_set_input_string_undoable`,node.h:346)。
///
/// # Safety
/// `input`/`value`/`out` 必须指向有效内存;`node` 是有效句柄。
pub(crate) unsafe fn set_input_string_undoable(
node: NodeHandle,
input: *const c_char,
value: *const c_char,
out: *mut CommandHandle,
) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::set_input_string_undoable_impl(node, input, value, out) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe {
oaknode::ffi::node::oaknode_node_set_input_string_undoable(
node,
input,
value,
out,
)
}
}
}
// ---- 测试桩(--features test-stubs)--------------------------------------
/// oaknode 测试桩:库内符号 + 节点值状态。
///
/// 节点句柄的 ctx 约定(桩内约定):`ctx = 身份 id`。undo 命令的
/// 登记与 undo/redo 语义在 [`crate::bridge::undo::stub`](两边共享
/// 同一张命令表)。
#[cfg(feature = "test-stubs")]
pub mod stub {
use super::*;
use std::collections::HashMap;
use std::ffi::CStr;
use std::sync::LazyLock;
use std::sync::Mutex;
/// 一个输入的标准值(数值走 [`Value`] POD;字符串族走 `string`)。
#[derive(Clone, Debug, PartialEq)]
pub struct StubInput {
/// 数值值(字符串族为 STRING 类型空 POD)。
pub value: Value,
/// 字符串值(仅字符串族输入)。
pub string: Option<String>,
}
/// 全部桩节点:身份 → 输入名 → 值。
static NODES: LazyLock<Mutex<HashMap<usize, HashMap<String, StubInput>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
fn lock() -> std::sync::MutexGuard<'static, HashMap<usize, HashMap<String, StubInput>>> {
NODES.lock().unwrap_or_else(|e| e.into_inner())
}
/// 重置全部桩节点(测试隔离)。
pub fn reset() {
lock().clear();
}
/// 登记一个可被 [`super::node_from_identity`] 找到的节点。
pub fn register_node(id: usize) {
lock().entry(id).or_default();
}
/// 直接设置输入值(测试前置)。
pub fn set_input(id: usize, input: &str, value: Value) {
lock().entry(id).or_default().insert(
input.to_string(),
StubInput {
value,
string: None,
},
);
}
/// 直接设置字符串输入值(测试前置)。
pub fn set_input_string(id: usize, input: &str, value: &str) {
lock().entry(id).or_default().insert(
input.to_string(),
StubInput {
value: Value::string(),
string: Some(value.to_string()),
},
);
}
/// 读输入当前值(断言用)。
pub fn input(id: usize, input: &str) -> Option<StubInput> {
lock().get(&id)?.get(input).cloned()
}
/// 命令回写用的应用入口(redo/undo 落值;由
/// [`crate::bridge::undo::stub`] 调用)。
pub(crate) fn apply(node: usize, input: &str, value: Value, string: Option<String>) {
let mut m = lock();
if let Some(n) = m.get_mut(&node) {
n.insert(
input.to_string(),
StubInput {
value,
string,
},
);
}
}
/// 读输入当前值(命令创建时取 prev)。
pub(crate) fn current(node: usize, input: &str) -> StubInput {
lock().get(&node).and_then(|n| n.get(input)).cloned().unwrap_or(StubInput {
value: Value::default(),
string: None,
})
}
pub(super) unsafe fn node_from_identity_impl(id: usize) -> NodeHandle {
if lock().contains_key(&id) {
CHandle {
ctx: id as *mut std::ffi::c_void,
addref: None,
release: None,
abi_version: crate::handle::OAKPLUGIN_ABI_VERSION,
}
} else {
CHandle::null()
}
}
pub(super) unsafe fn set_input_undoable_impl(
node: NodeHandle,
input: *const c_char,
value: *const Value,
out: *mut CommandHandle,
) -> i32 {
if node.is_null() || input.is_null() || value.is_null() || out.is_null() {
return -30001;
}
let id = node.ctx as usize;
let name = unsafe { CStr::from_ptr(input) }.to_str().map_err(|_| ()).unwrap_or_default();
// 输入名必须已存在(node.h: 未知输入 id → OAKNODE_E_NOT_FOUND)。
if !lock().get(&id).is_some_and(|n| n.contains_key(name)) {
return -30004;
}
let next = unsafe { *value };
let prev = current(id, &name).value;
let h = crate::bridge::undo::stub::create_numeric(id, name.to_string(), prev, next);
unsafe { *out = h };
0
}
pub(super) unsafe fn set_input_string_undoable_impl(
node: NodeHandle,
input: *const c_char,
value: *const c_char,
out: *mut CommandHandle,
) -> i32 {
if node.is_null() || input.is_null() || value.is_null() || out.is_null() {
return -30001;
}
let id = node.ctx as usize;
let name = unsafe { CStr::from_ptr(input) }.to_str().map_err(|_| ()).unwrap_or_default();
// 输入名必须已存在(node.h: 未知输入 id → OAKNODE_E_NOT_FOUND)。
if !lock().get(&id).is_some_and(|n| n.contains_key(name)) {
return -30004;
}
let next = unsafe { CStr::from_ptr(value) }.to_string_lossy().into_owned();
let prev = current(id, &name).string.unwrap_or_default();
let h = crate::bridge::undo::stub::create_string(
id,
name.to_string(),
prev.clone(),
next.clone(),
);
unsafe { *out = h };
0
}
}
// ---- 默认模式单测(无桩;空/NULL 句柄经真实 crate 的可解释失败路径)----------------------
#[cfg(all(test, not(feature = "test-stubs")))]
mod tests {
use super::*;
/// 空/NULL 句柄经真实 oaknode 容错:未登记身份 → 空句柄;空句柄
/// 与空指针参数被拒(OAKNODE_E_INVALID)。
#[test]
fn empty_handle_error_paths() {
let mut cmd = CommandHandle::null();
unsafe {
assert!(node_from_identity(0xDEAD).is_null(), "未登记身份 → 空句柄");
assert_eq!(
set_input_undoable(NodeHandle::null(), std::ptr::null(), std::ptr::null(), &mut cmd),
-30001
);
assert_eq!(
set_input_string_undoable(
NodeHandle::null(),
std::ptr::null(),
std::ptr::null(),
&mut cmd
),
-30001
);
}
}
}
+885
View File
@@ -0,0 +1,885 @@
// 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/>.
//! oakrender C ABI 导入(clip↔纹理桥用到的子集)。
//!
//! 声明以 `include/render/renderer.h` 为准(骨架的
//! `oakrender_texture_get_frame`/`oakrender_texture_wrap_native`/
//! `oakrender_texture_is_dummy` 与头文件不符,弃用;真实符号为
//! `oakrender_display_texture_*` 与 `oakrender_codec_frame_*`)。
//! `oakrender_display_texture_wrap_native` 是 C++ 专属符号
//! (TexturePtr 引用),Rust 不可调用——输出纹理由 oakrender 侧
//! 创建并经句柄传入,宿主只写其帧。
//!
//! ## 双态实现
//!
//! - 默认:直接 Rust 调用 oakrender 的 `ffi`(单库化,见
//! `docs/zh/plans/riir/single-lib.md`);
//! - `--features test-stubs`:库内状态桩 + 状态访问器([`stub`],纯
//! Rust、无 `#[no_mangle]`,与真实 oakrender 的导出不冲突)——像素
//! 路径在 cargo test 里全链路可跑。两种形态的调用面
//! (`texture_get_frame` 等)完全一致。
use std::ffi::c_void;
/// `oakrender_video_params` POD — single-lib unification: aliases the
/// oakrender crate's struct (identical layout; include/render/renderer.h:78).
pub type VideoParams = oakrender::ffi::OakRenderVideoParams;
/// olive::PixelFormat::Format 的 f32 值。
pub const PIXEL_FORMAT_F32: i32 = 4;
/// olive::PixelFormat::Format 的 u8 值。
pub const PIXEL_FORMAT_U8: i32 = 0;
/// oakrender 渲染器句柄(`OakRenderRenderer`,值型)。
pub type RendererHandle = crate::handle::CHandle;
/// oakrender 纹理句柄(`OakRenderTexture`,值型)。
pub type TextureHandle = crate::handle::CHandle;
/// oakrender 帧句柄(`OakCodecFrame`,值型;布局与 CHandle 一致)。
pub type FrameHandle = crate::handle::CHandle;
// ---- 桥调用面 ------------------------------------------------------------
/// 纹理的 CPU 帧(Texture::frame());`*out` 收到保留引用。
pub(crate) unsafe fn texture_get_frame(texture: TextureHandle, out: *mut FrameHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::texture_get_frame(texture, out) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_display_texture_get_frame(texture, out) }
}
}
/// 纹理是否占位(dummy);符号缺失 → 1(视为占位)。
pub(crate) unsafe fn texture_is_dummy(texture: TextureHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::texture_is_dummy(texture) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_display_texture_is_dummy(texture) }
}
}
/// 帧宽(空帧为 0)。
pub(crate) unsafe fn frame_width(frame: FrameHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::frame_width(frame) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_width(frame) }
}
}
/// 帧高。
pub(crate) unsafe fn frame_height(frame: FrameHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::frame_height(frame) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_height(frame) }
}
}
/// 借用的像素数据指针(最终 release 前有效)。
pub(crate) unsafe fn frame_data(frame: FrameHandle) -> *mut c_void {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::frame_data(frame) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_data(frame) }
}
}
/// 帧的视频参数。
pub(crate) unsafe fn frame_get_params(frame: FrameHandle, out: *mut VideoParams) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::frame_get_params(frame, out) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_get_params(frame, out) }
}
}
/// 按参数分配像素缓冲。
pub(crate) unsafe fn frame_allocate(frame: FrameHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::frame_allocate(frame) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_allocate(frame) }
}
}
/// 释放一次帧引用并清空句柄(NULL/空句柄 no-op)。
pub(crate) unsafe fn frame_free(frame: *mut FrameHandle) {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::frame_free(frame) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_free(frame) }
}
}
/// 帧的行跨度(字节;空帧 0)。M11 第 2 期新增导入
/// (`oakrender_codec_frame_linesize_bytes`,renderer.h:311):CPU
/// 拷贝路径(fetch/store/驱动输出装配)用它兼容真实 oakrender 的
/// 行填充,不再假设紧凑行布局。
pub(crate) unsafe fn frame_linesize_bytes(frame: FrameHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::frame_linesize_bytes(frame) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_linesize_bytes(frame) }
}
}
// ---- 渲染器族(GL 路径;M11 §4)--------------------------------------------
/// 按后端名创建渲染器(`oakrender_display_renderer_create_dynamic`,
/// renderer.h:155)。空指针/空串 → 空句柄。
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
pub(crate) unsafe fn renderer_create_dynamic(backend: *const std::ffi::c_char) -> RendererHandle {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::renderer_create_dynamic(backend) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_create_dynamic(backend) }
}
}
/// 初始化渲染器(`oakrender_display_renderer_init`,renderer.h:175)。
/// `gl_context` 为借用指针(NULL = 后端默认上下文路径)。
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
pub(crate) unsafe fn renderer_init(
renderer: RendererHandle,
gl_context: *mut std::ffi::c_void,
) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::renderer_init(renderer, gl_context) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_init(renderer, gl_context) }
}
}
/// 渲染器是否 OpenGL 后端(`oakrender_display_renderer_is_open_gl`,
/// renderer.h:189)。
pub(crate) unsafe fn renderer_is_open_gl(renderer: RendererHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::renderer_is_open_gl(renderer) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_is_open_gl(renderer) }
}
}
/// 释放一次渲染器引用并清空句柄(`oakrender_display_renderer_destroy`,
/// renderer.h:183)。
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
pub(crate) unsafe fn renderer_destroy(renderer: *mut RendererHandle) {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::renderer_destroy(renderer) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_destroy(renderer) }
}
}
/// 在渲染器上创建纹理(`oakrender_display_texture_create`,
/// renderer.h:204)。`pixels` 可空(未初始化);`linesize` 为行跨度
/// 字节数(0 = 紧凑行;pixels 为空时 0)。
///
/// 单位约定(M11 第 2 期):**字节**——以 renderer.h:206 的明文
/// 契约为准(`Stride of pixels in bytes`)。C++ 调用点传像素行跨度,
/// 由 oakrender 侧实现 C ABI 时换算;本 crate 侧一律传字节。
pub(crate) unsafe fn texture_create(
renderer: RendererHandle,
params: *const VideoParams,
pixels: *const std::ffi::c_void,
linesize: i32,
) -> TextureHandle {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::texture_create(renderer, params, pixels, linesize) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe {
oakrender::ffi::renderer::oakrender_display_texture_create(
renderer, params, pixels, linesize,
)
}
}
}
/// 纹理的原生 GL id(`oakrender_display_texture_id`,renderer.h:245;
/// 空/占位/无 id 纹理为 0)。
pub(crate) unsafe fn texture_id(texture: TextureHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::texture_id(texture) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_display_texture_id(texture) }
}
}
/// 纹理参数(`oakrender_display_texture_get_params`,renderer.h:233)。
pub(crate) unsafe fn texture_get_params(texture: TextureHandle, out: *mut VideoParams) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::texture_get_params(texture, out) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_display_texture_get_params(texture, out) }
}
}
/// 从纹理下载像素(`oakrender_display_texture_download`,
/// renderer.h:228;`linesize` 行跨度字节数,0 = 紧凑行)。
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
pub(crate) unsafe fn texture_download(
texture: TextureHandle,
pixels: *mut std::ffi::c_void,
linesize: i32,
) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::texture_download(texture, pixels, linesize) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_display_texture_download(texture, pixels, linesize) }
}
}
/// 再取一次引用(`oakrender_display_texture_retain`,renderer.h:215)。
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
pub(crate) unsafe fn texture_retain(texture: TextureHandle) -> TextureHandle {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::texture_retain(texture) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_display_texture_retain(texture) }
}
}
/// 释放一次纹理引用并清空句柄(`oakrender_display_texture_free`,
/// renderer.h:223)。
pub(crate) unsafe fn texture_free(texture: *mut TextureHandle) {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::texture_free(texture) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_display_texture_free(texture) }
}
}
/// 上传像素到纹理(`oakrender_display_texture_upload`,renderer.h:225;
/// `linesize` 行跨度字节数,0 = 紧凑行)。
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
pub(crate) unsafe fn texture_upload(
texture: TextureHandle,
pixels: *const std::ffi::c_void,
linesize: i32,
) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::texture_upload(texture, pixels, linesize) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakrender::ffi::renderer::oakrender_display_texture_upload(texture, pixels, linesize) }
}
}
/// 按 GL id 从渲染器下载像素(`oakrender_display_renderer_download_from_texture`,
/// renderer.h:333;`linesize` 行跨度字节数,0 = 紧凑行)。
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
pub(crate) unsafe fn renderer_download_from_texture(
renderer: RendererHandle,
texture_id: i32,
params: *const VideoParams,
dst: *mut std::ffi::c_void,
linesize: i32,
) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::renderer_download_from_texture(renderer, texture_id, params, dst, linesize) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe {
oakrender::ffi::renderer::oakrender_display_renderer_download_from_texture(
renderer, texture_id, params, dst, linesize,
)
}
}
}
// ---- 测试桩(--features test-stubs)--------------------------------------
/// oakrender 测试桩:库内 no_mangle 符号 + 状态访问器。
///
/// 纹理/帧句柄的 ctx 约定(桩内约定,与真实句柄无冲突):
/// - dst 纹理 ctx = 0xA1 → 输出帧 ctx = 0xB1;
/// - src 纹理 ctx = 0xA2 → 输入帧 ctx = 0xB2。
#[cfg(feature = "test-stubs")]
pub mod stub {
use super::*;
use crate::handle::CHandle;
use std::sync::Mutex;
/// 测试帧(数据 + 参数)。
#[derive(Clone)]
pub struct StubFrame {
/// 视频参数。
pub params: VideoParams,
/// 像素缓冲(行优先)。
pub data: Vec<u8>,
}
impl StubFrame {
fn new(width: i32, height: i32, format: i32) -> Self {
let len = width as usize * height as usize * 4 * bytes_per_pixel(format);
Self {
params: VideoParams {
width,
height,
format,
..Default::default()
},
data: vec![0u8; len],
}
}
}
/// 桩 GL 纹理(渲染器上的 GPU 纹理的 CPU 镜像;GL 纹理同时
/// 扮演 CPU 帧载体——与真实 olive Texture 包装 CPU 帧同构)。
#[derive(Clone)]
struct StubGlTexture {
/// 原生 id(texture_id 的返回值)。
id: i32,
/// CPU 镜像帧(数据 + 参数)。
frame: StubFrame,
}
fn bytes_per_pixel(format: i32) -> usize {
match format {
PIXEL_FORMAT_F32 => 4,
_ => 1,
}
}
/// 按参数分配紧凑像素缓冲(宽×高×4 通道×每分量字节)。
fn tight_len(params: &VideoParams) -> usize {
params.width as usize * params.height as usize * 4 * bytes_per_pixel(params.format)
}
struct StubState {
dst: StubFrame,
src: StubFrame,
/// 标记为占位(dummy)的纹理 ctx(texture_is_dummy 用)。
dummy: std::collections::HashSet<usize>,
/// GL 渲染器是否可用(renderer_is_open_gl 的返回值)。
gl_available: bool,
/// GL 纹理注册表(ctx = GL_TEX_BASE + id)。
gl_textures: Vec<StubGlTexture>,
/// 下一个 GL 纹理 id(从 1 起)。
next_gl_id: i32,
}
static STATE: std::sync::LazyLock<Mutex<StubState>> = std::sync::LazyLock::new(|| {
Mutex::new(StubState {
dst: StubFrame::new(0, 0, 0),
src: StubFrame::new(0, 0, 0),
dummy: std::collections::HashSet::new(),
gl_available: false,
gl_textures: Vec::new(),
next_gl_id: 1,
})
});
fn lock() -> std::sync::MutexGuard<'static, StubState> {
STATE.lock().unwrap_or_else(|e| e.into_inner())
}
/// 重置全部桩状态(测试隔离)。
pub fn reset() {
let mut s = lock();
s.dst = StubFrame::new(0, 0, 0);
s.src = StubFrame::new(0, 0, 0);
s.dummy.clear();
s.gl_available = false;
s.gl_textures.clear();
s.next_gl_id = 1;
}
/// 把某纹理 ctx 标记为占位(dummy);`dummy=false` 取消标记。
/// clip 桥把 dummy 输入视作空输入(NotFound)。
pub fn set_dummy(ctx: usize, dummy: bool) {
let mut s = lock();
if dummy {
s.dummy.insert(ctx);
} else {
s.dummy.remove(&ctx);
}
}
/// 配置输出帧(宽/高/格式;F32 = 全链路主路径)。
pub fn setup_dst(width: i32, height: i32, format: i32) {
lock().dst = StubFrame::new(width, height, format);
}
/// 配置输入帧并填充像素。
pub fn setup_src(width: i32, height: i32, format: i32, pixels: Vec<u8>) {
let mut s = lock();
s.src = StubFrame::new(width, height, format);
s.src.data = pixels;
}
/// 输出帧像素(断言用)。
pub fn dst_pixels() -> Vec<u8> {
lock().dst.data.clone()
}
/// 输入帧像素(断言用)。
pub fn src_pixels() -> Vec<u8> {
lock().src.data.clone()
}
/// 输出帧参数。
pub fn dst_params() -> VideoParams {
lock().dst.params
}
/// GL 渲染器可用标记(renderer_is_open_gl 的桩返回值)。
pub fn set_gl_available(available: bool) {
lock().gl_available = available;
}
/// 构造桩 GL 渲染器句柄(ctx = 0xD1)。
pub fn make_gl_renderer() -> RendererHandle {
magic_handle(GL_RENDERER)
}
/// 已注册的 GL 纹理 id 列表(断言用)。
pub fn gl_texture_ids() -> Vec<i32> {
lock().gl_textures.iter().map(|t| t.id).collect()
}
/// 某 GL 纹理 id 的像素(断言用;未知 id 返回空)。
pub fn gl_texture_data(id: i32) -> Vec<u8> {
lock()
.gl_textures
.iter()
.find(|t| t.id == id)
.map(|t| t.frame.data.clone())
.unwrap_or_default()
}
/// 某 GL 纹理 id 的参数(断言用)。
pub fn gl_texture_params(id: i32) -> Option<VideoParams> {
lock()
.gl_textures
.iter()
.find(|t| t.id == id)
.map(|t| t.frame.params)
}
/// 直接构造一个 GL 纹理(GL 测试的目标纹理/输入纹理模拟;
/// oakrender 侧创建纹理的 C ABI 等价物)。返回句柄(ctx =
/// GL_TEX_BASE + id)。
pub fn make_gl_texture(width: i32, height: i32, format: i32) -> TextureHandle {
let mut s = lock();
let id = s.next_gl_id;
s.next_gl_id += 1;
s.gl_textures.push(StubGlTexture {
id,
frame: StubFrame::new(width, height, format),
});
magic_handle(GL_TEX_BASE + id as usize)
}
fn frame_of(state: &mut StubState, ctx: usize) -> &mut StubFrame {
if ctx == SRC_FRAME {
&mut state.src
} else if let Some(id) = gl_id_of_ctx(ctx) {
match state.gl_textures.iter_mut().find(|t| t.id == id) {
Some(t) => &mut t.frame,
None => &mut state.dst,
}
} else {
&mut state.dst
}
}
fn magic_handle(ctx: usize) -> CHandle {
CHandle {
ctx: ctx as *mut c_void,
addref: None,
release: None,
abi_version: 1,
}
}
const DST_TEX: usize = 0xA1;
const SRC_TEX: usize = 0xA2;
const DST_FRAME: usize = 0xB1;
const SRC_FRAME: usize = 0xB2;
/// 桩 GL 渲染器 ctx。
const GL_RENDERER: usize = 0xD1;
/// 桩 GL 纹理 ctx 基址(ctx = GL_TEX_BASE + id)。
const GL_TEX_BASE: usize = 0xC0;
/// GL 纹理 ctx → id。
fn gl_id_of_ctx(ctx: usize) -> Option<i32> {
if ctx > GL_TEX_BASE {
Some((ctx - GL_TEX_BASE) as i32)
} else {
None
}
}
/// 行拷贝:`src`(行跨度 src_linesize 字节)→ `dst`(行跨度
/// dst_linesize 字节),共 `rows` 行、每行 `row_bytes` 字节。
fn copy_rows(
src: &[u8],
src_linesize: usize,
dst: &mut [u8],
dst_linesize: usize,
row_bytes: usize,
rows: usize,
) {
for y in 0..rows {
let s = y * src_linesize;
let d = y * dst_linesize;
if row_bytes == src_linesize && src_linesize == dst_linesize {
dst[d..d + row_bytes].copy_from_slice(&src[s..s + row_bytes]);
} else {
dst[d..d + row_bytes].copy_from_slice(&src[s..s + row_bytes.min(src.len().saturating_sub(s))]);
}
}
}
pub(super) unsafe fn texture_get_frame(texture: TextureHandle, out: *mut FrameHandle) -> i32 {
if out.is_null() {
return -1;
}
// GL 纹理:帧句柄即纹理自身 ctx(frame_of 按 ctx 反查镜像帧)。
let frame_ctx = if texture.ctx as usize == SRC_TEX {
SRC_FRAME
} else if gl_id_of_ctx(texture.ctx as usize).is_some() {
texture.ctx as usize
} else {
DST_FRAME
};
unsafe { *out = magic_handle(frame_ctx) };
0
}
pub(super) unsafe fn texture_is_dummy(texture: TextureHandle) -> i32 {
lock().dummy.contains(&(texture.ctx as usize)) as i32
}
pub(super) unsafe fn frame_width(frame: FrameHandle) -> i32 {
let mut s = lock();
frame_of(&mut s, frame.ctx as usize).params.width
}
pub(super) unsafe fn frame_height(frame: FrameHandle) -> i32 {
let mut s = lock();
frame_of(&mut s, frame.ctx as usize).params.height
}
pub(super) unsafe fn frame_data(frame: FrameHandle) -> *mut c_void {
let mut s = lock();
let f = frame_of(&mut s, frame.ctx as usize);
f.data.as_mut_ptr() as *mut c_void
}
pub(super) unsafe fn frame_get_params(frame: FrameHandle, out: *mut VideoParams) -> i32 {
if out.is_null() {
return -1;
}
let mut s = lock();
unsafe { *out = frame_of(&mut s, frame.ctx as usize).params };
0
}
pub(super) unsafe fn frame_allocate(frame: FrameHandle) -> i32 {
let mut s = lock();
let f = frame_of(&mut s, frame.ctx as usize);
let len = f.params.width as usize
* f.params.height as usize
* 4
* bytes_per_pixel(f.params.format);
f.data.resize(len, 0);
0
}
pub(super) unsafe fn frame_free(frame: *mut FrameHandle) {
if !frame.is_null() {
unsafe { (*frame).ctx = std::ptr::null_mut() };
}
}
pub(super) unsafe fn frame_linesize_bytes(frame: FrameHandle) -> i32 {
let mut s = lock();
let f = frame_of(&mut s, frame.ctx as usize);
(f.params.width * 4 * bytes_per_pixel(f.params.format) as i32) as i32
}
// ---- 渲染器族桩 ---------------------------------------------------------
pub(super) unsafe fn renderer_create_dynamic(backend: *const std::ffi::c_char) -> RendererHandle {
if backend.is_null() {
return RendererHandle::null();
}
let id = unsafe { std::ffi::CStr::from_ptr(backend) }.to_str().unwrap_or("");
if id != "opengl" {
return RendererHandle::null();
}
magic_handle(GL_RENDERER)
}
pub(super) unsafe fn renderer_init(
_renderer: RendererHandle,
_gl_context: *mut std::ffi::c_void,
) -> i32 {
0
}
pub(super) unsafe fn renderer_is_open_gl(renderer: RendererHandle) -> i32 {
if renderer.ctx as usize != GL_RENDERER {
return 0;
}
lock().gl_available as i32
}
pub(super) unsafe fn renderer_destroy(renderer: *mut RendererHandle) {
if !renderer.is_null() {
unsafe { (*renderer).ctx = std::ptr::null_mut() };
}
}
// ---- 纹理族桩(GL 纹理注册表)------------------------------------------
pub(super) unsafe fn texture_create(
renderer: RendererHandle,
params: *const VideoParams,
pixels: *const std::ffi::c_void,
linesize: i32,
) -> TextureHandle {
if renderer.ctx as usize != GL_RENDERER || params.is_null() {
return TextureHandle::null();
}
let params = unsafe { *params };
if params.width <= 0 || params.height <= 0 {
return TextureHandle::null();
}
let mut s = lock();
let id = s.next_gl_id;
s.next_gl_id += 1;
let mut data = vec![0u8; tight_len(&params)];
if !pixels.is_null() {
// linesize 字节/行(renderer.h 的 bytes 契约;0 → 紧凑行)。
let bpp = bytes_per_pixel(params.format);
let row_bytes = (params.width as usize) * 4 * bpp;
let src_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
let src = unsafe { std::slice::from_raw_parts(pixels as *const u8, src_linesize * params.height as usize) };
copy_rows(src, src_linesize, &mut data, row_bytes, row_bytes.min(src_linesize), params.height as usize);
}
s.gl_textures.push(StubGlTexture {
id,
frame: StubFrame {
params,
data,
},
});
magic_handle(GL_TEX_BASE + id as usize)
}
pub(super) unsafe fn texture_id(texture: TextureHandle) -> i32 {
gl_id_of_ctx(texture.ctx as usize).unwrap_or(0)
}
pub(super) unsafe fn texture_get_params(texture: TextureHandle, out: *mut VideoParams) -> i32 {
if out.is_null() {
return -1;
}
let mut s = lock();
let Some(id) = gl_id_of_ctx(texture.ctx as usize) else {
return -1;
};
let Some(t) = s.gl_textures.iter().find(|t| t.id == id) else {
return -1;
};
unsafe { *out = t.frame.params };
0
}
pub(super) unsafe fn texture_download(
texture: TextureHandle,
pixels: *mut std::ffi::c_void,
linesize: i32,
) -> i32 {
let mut s = lock();
let Some(id) = gl_id_of_ctx(texture.ctx as usize) else {
return -1;
};
let Some(t) = s.gl_textures.iter().find(|t| t.id == id) else {
return -1;
};
if pixels.is_null() {
return -1;
}
let bpp = bytes_per_pixel(t.frame.params.format);
let row_bytes = (t.frame.params.width as usize) * 4 * bpp;
let dst_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
let dst = unsafe { std::slice::from_raw_parts_mut(pixels as *mut u8, dst_linesize * t.frame.params.height as usize) };
let src = t.frame.data.clone();
copy_rows(&src, row_bytes, dst, dst_linesize, row_bytes, t.frame.params.height as usize);
0
}
pub(super) unsafe fn texture_upload(
texture: TextureHandle,
pixels: *const std::ffi::c_void,
linesize: i32,
) -> i32 {
let mut s = lock();
let Some(id) = gl_id_of_ctx(texture.ctx as usize) else {
return -1;
};
let Some(t) = s.gl_textures.iter_mut().find(|t| t.id == id) else {
return -1;
};
if pixels.is_null() {
return -1;
}
let bpp = bytes_per_pixel(t.frame.params.format);
let row_bytes = (t.frame.params.width as usize) * 4 * bpp;
let src_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
let src = unsafe { std::slice::from_raw_parts(pixels as *const u8, src_linesize * t.frame.params.height as usize) };
let mut data = vec![0u8; tight_len(&t.frame.params)];
copy_rows(src, src_linesize, &mut data, row_bytes, row_bytes.min(src_linesize), t.frame.params.height as usize);
t.frame.data = data;
0
}
pub(super) unsafe fn texture_retain(texture: TextureHandle) -> TextureHandle {
texture
}
pub(super) unsafe fn texture_free(texture: *mut TextureHandle) {
if texture.is_null() {
return;
}
let ctx = unsafe { (*texture).ctx as usize };
if let Some(id) = gl_id_of_ctx(ctx) {
let mut s = lock();
s.gl_textures.retain(|t| t.id != id);
}
unsafe { (*texture).ctx = std::ptr::null_mut() };
}
pub(super) unsafe fn renderer_download_from_texture(
renderer: RendererHandle,
texture_id: i32,
params: *const VideoParams,
dst: *mut std::ffi::c_void,
linesize: i32,
) -> i32 {
if renderer.ctx as usize != GL_RENDERER || params.is_null() || dst.is_null() {
return -1;
}
let req = unsafe { *params };
let s = lock();
let Some(t) = s.gl_textures.iter().find(|t| t.id == texture_id) else {
return -1;
};
let bpp = bytes_per_pixel(req.format);
let row_bytes = (req.width as usize) * 4 * bpp;
let dst_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
let out = unsafe { std::slice::from_raw_parts_mut(dst as *mut u8, dst_linesize * req.height as usize) };
let src = t.frame.data.clone();
copy_rows(&src, row_bytes, out, dst_linesize, row_bytes, req.height as usize);
0
}
}
// ---- 默认模式单测(无桩;空/NULL 句柄经真实 crate 的可解释失败路径)----------------------
#[cfg(all(test, not(feature = "test-stubs")))]
mod tests {
use super::*;
/// 空/NULL 句柄经真实 oakrender 全部容错不崩:返回
/// `OAKRENDER_E_INVALID`(-70001)或空值。
#[test]
fn empty_handle_error_paths() {
let mut frame = FrameHandle::null();
let tex = TextureHandle::null();
unsafe {
assert_eq!(texture_get_frame(tex, &mut frame), -70001);
// 空句柄不是占位纹理:真实 oakrender 返回 0。
assert_eq!(texture_is_dummy(tex), 0);
assert_eq!(frame_width(frame), 0);
assert_eq!(frame_height(frame), 0);
assert!(frame_data(frame).is_null());
assert_eq!(frame_get_params(frame, &mut VideoParams::default()), -70001);
assert_eq!(frame_allocate(frame), -70001);
frame_free(&mut frame); // 空句柄 no-op
frame_free(std::ptr::null_mut()); // NULL no-op
}
}
}
+378
View File
@@ -0,0 +1,378 @@
// 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/>.
//! oakundo C ABI 导入(参数回写打包用到的子集;include/undo/
//! undocommand.h)。
//!
//! ## 语义(对照 C++ 的 oliveplugininstance.cpp `submit_undo_command`)
//!
//! 写回一律以"命令"为单位:数值/字符串 set 各产出一条命令
//! (node 桥的 `*_undoable`),立即 `redo_now` 生效;编辑事务
//! (paramEditBegin/End)内多条命令并入一条 multi
//! ([`command_init_multi`] + [`command_multi_add_child`]),
//! editEnd 时整体 `redo_now` 后释放。
//!
//! ## 双态实现(同 [`crate::bridge::render`])
//!
//! - 默认:直接 Rust 调用 oakundo 的 `ffi`(单库化,见
//! `docs/zh/plans/riir/single-lib.md`);
//! - `--features test-stubs`:库内状态桩([`stub`],纯 Rust、无
//! `#[no_mangle]`,与真实 oakundo 的导出不冲突)——命令表 + undo/redo
//! 语义在 cargo test 里全链路可跑。两种形态的调用面完全一致。
use crate::handle::CHandle;
/// oakundo 命令句柄(值型)。
pub type CommandHandle = crate::handle::CHandle;
// ---- 桥调用面 ------------------------------------------------------------
/// 创建 multi 命令(`oakundo_command_init_multi`,undocommand.h:85)。
pub(crate) unsafe fn command_init_multi() -> CommandHandle {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::command_init_multi_impl() }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakundo::ffi::command::oakundo_command_init_multi() }
}
}
/// 直接 redo(不进栈的立即执行路径;`oakundo_command_redo_now`)。
pub(crate) unsafe fn command_redo_now(command: CommandHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::command_redo_now_impl(command) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) }
}
}
/// 把子命令并入 multi(`oakundo_command_multi_add_child`)。
pub(crate) unsafe fn command_multi_add_child(multi: CommandHandle, child: CommandHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::command_multi_add_child_impl(multi, child) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) }
}
}
/// 释放命令句柄(`oakundo_command_free`;NULL/空句柄 no-op)。
pub(crate) unsafe fn command_free(command: *mut CommandHandle) {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::command_free_impl(command) }
}
#[cfg(not(feature = "test-stubs"))]
{
unsafe { oakundo::ffi::command::oakundo_command_free(command) }
}
}
// ---- 测试桩(--features test-stubs)--------------------------------------
/// oakundo 测试桩:命令表 + undo/redo 语义。
///
/// 命令句柄的 ctx 约定(桩内约定):`ctx = 命令 id`。记录在
/// [`command_free`] 后仍保留(`freed` 标记),供测试检查命令捕获的
/// prev/next 并驱动 undo/redo——真实 oakundo 的 free 会销毁命令,
/// 这是桩的刻意简化(记录仅为测试保留)。
#[cfg(feature = "test-stubs")]
pub mod stub {
use super::*;
use crate::bridge::node;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{LazyLock, Mutex};
/// 一条命令的记录(redo 应用 next、undo 恢复 prev)。
#[derive(Clone, Debug)]
pub struct CommandRecord {
/// 命令 id(= 句柄 ctx)。
pub id: usize,
/// 目标节点身份。
pub node: usize,
/// 目标输入名。
pub input: String,
/// 数值回写的旧值。
pub prev: node::Value,
/// 数值回写的新值。
pub next: node::Value,
/// 字符串回写的旧值。
pub prev_string: Option<String>,
/// 字符串回写的新值。
pub next_string: Option<String>,
/// redo 是否已应用。
pub applied: bool,
/// 是否 multi 命令。
pub is_multi: bool,
/// multi 的子命令 id。
pub children: Vec<usize>,
/// free 是否已调用(记录保留给测试检查)。
pub freed: bool,
}
static COMMANDS: LazyLock<Mutex<HashMap<usize, CommandRecord>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
fn lock() -> std::sync::MutexGuard<'static, HashMap<usize, CommandRecord>> {
COMMANDS.lock().unwrap_or_else(|e| e.into_inner())
}
fn magic(id: usize) -> CommandHandle {
CHandle {
ctx: id as *mut std::ffi::c_void,
addref: None,
release: None,
abi_version: crate::handle::OAKPLUGIN_ABI_VERSION,
}
}
fn next_id() -> usize {
NEXT_ID.fetch_add(1, Ordering::Relaxed)
}
/// 重置命令表(测试隔离)。
pub fn reset() {
lock().clear();
}
/// 全部命令记录快照(含已 free 的;id 升序)。
pub fn records() -> Vec<CommandRecord> {
let mut v: Vec<CommandRecord> = lock().values().cloned().collect();
v.sort_by_key(|r| r.id);
v
}
/// 最近一次创建的命令记录。
pub fn last_command() -> Option<CommandRecord> {
let m = lock();
m.values().max_by_key(|r| r.id).cloned()
}
/// 撤销一条命令(测试助手:验证命令捕获的 prev 正确)。multi 按
/// 子命令逆序撤销;单命令仅在其已应用时恢复 prev。
pub fn undo(id: usize) {
let (is_multi, children) = lock()
.get(&id)
.map(|r| (r.is_multi, r.children.clone()))
.unwrap_or((false, Vec::new()));
if is_multi {
for c in children.iter().rev() {
undo(*c);
}
if let Some(r) = lock().get_mut(&id) {
r.applied = false;
}
return;
}
let mut m = lock();
let Some(r) = m.get_mut(&id) else { return };
if r.applied {
node::stub::apply(r.node, &r.input, r.prev, r.prev_string.clone());
r.applied = false;
}
}
/// 重做一条命令(测试助手)。redo 幂等(已应用则 no-op)。
pub fn redo(id: usize) {
let (is_multi, children) = lock()
.get(&id)
.map(|r| (r.is_multi, r.children.clone()))
.unwrap_or((false, Vec::new()));
if is_multi {
for c in &children {
redo(*c);
}
if let Some(r) = lock().get_mut(&id) {
r.applied = true;
}
return;
}
let mut m = lock();
let Some(r) = m.get_mut(&id) else { return };
if !r.applied {
node::stub::apply(r.node, &r.input, r.next, r.next_string.clone());
r.applied = true;
}
}
/// 登记一条数值写回命令(node 桥调用)。
pub(crate) fn create_numeric(
node_id: usize,
input: String,
prev: node::Value,
next: node::Value,
) -> CommandHandle {
let id = next_id();
lock().insert(
id,
CommandRecord {
id,
node: node_id,
input,
prev,
next,
prev_string: None,
next_string: None,
applied: false,
is_multi: false,
children: Vec::new(),
freed: false,
},
);
magic(id)
}
/// 登记一条字符串写回命令(node 桥调用)。
pub(crate) fn create_string(
node_id: usize,
input: String,
prev: String,
next: String,
) -> CommandHandle {
let id = next_id();
lock().insert(
id,
CommandRecord {
id,
node: node_id,
input,
prev: node::Value::string(),
next: node::Value::string(),
prev_string: Some(prev),
next_string: Some(next),
applied: false,
is_multi: false,
children: Vec::new(),
freed: false,
},
);
magic(id)
}
pub(super) unsafe fn command_init_multi_impl() -> CommandHandle {
let id = next_id();
lock().insert(
id,
CommandRecord {
id,
node: 0,
input: String::new(),
prev: node::Value::default(),
next: node::Value::default(),
prev_string: None,
next_string: None,
applied: false,
is_multi: true,
children: Vec::new(),
freed: false,
},
);
magic(id)
}
pub(super) unsafe fn command_redo_now_impl(command: CommandHandle) -> i32 {
if command.is_null() {
return -40001;
}
// 先取 multi 的子命令列表,再逐条递归(避免持锁递归)。
let (is_multi, children) = lock()
.get(&(command.ctx as usize))
.map(|r| (r.is_multi, r.children.clone()))
.unwrap_or((false, Vec::new()));
if is_multi {
for c in &children {
let h = magic(*c);
unsafe { command_redo_now_impl(h) };
}
if let Some(r) = lock().get_mut(&(command.ctx as usize)) {
r.applied = true;
}
return 0;
}
let mut m = lock();
let Some(r) = m.get_mut(&(command.ctx as usize)) else {
return -40004;
};
if !r.applied {
node::stub::apply(r.node, &r.input, r.next, r.next_string.clone());
r.applied = true;
}
0
}
pub(super) unsafe fn command_multi_add_child_impl(multi: CommandHandle, child: CommandHandle) -> i32 {
if multi.is_null() || child.is_null() {
return -40001;
}
let mut m = lock();
let Some(r) = m.get_mut(&(multi.ctx as usize)) else {
return -40004;
};
if !r.is_multi {
return -40002;
}
r.children.push(child.ctx as usize);
0
}
pub(super) unsafe fn command_free_impl(command: *mut CommandHandle) {
if command.is_null() {
return;
}
let h = unsafe { &mut *command };
if h.is_null() {
return;
}
if let Some(r) = lock().get_mut(&(h.ctx as usize)) {
// 记录保留给测试检查(undo/redo 仍可按 id 驱动)。
r.freed = true;
}
h.ctx = std::ptr::null_mut();
}
}
// ---- 默认模式单测(无桩;空/NULL 句柄经真实 crate 的可解释失败路径)----------------------
#[cfg(all(test, not(feature = "test-stubs")))]
mod tests {
use super::*;
/// 空/NULL 句柄经真实 oakundo 全部容错不崩:`init_multi` 产生
/// 真实命令,空句柄操作返回 `OAKUNDO_E_INVALID`(-20001)。
#[test]
fn empty_handle_error_paths() {
unsafe {
let mut m = command_init_multi();
assert!(!m.is_null(), "init_multi 应产生真实命令句柄");
assert_eq!(command_redo_now(CommandHandle::null()), -20001);
assert_eq!(
command_multi_add_child(CommandHandle::null(), CommandHandle::null()),
-20001
);
command_free(&mut m); // 空句柄 no-op
command_free(std::ptr::null_mut()); // NULL no-op
}
}
}
+323
View File
@@ -0,0 +1,323 @@
// 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 实例:clip ↔ oakrender 纹理桥。
//!
//! 对应 C++ 的 `OliveClipInstance`。纹理数据经
//! [`crate::bridge::render`] 的 oakrender C ABI 流动;OFX 侧只看到
//! [`crate::image::Image`](CPU 路径)。
//!
//! `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`];
//! clip handle 即 `&props`)。
//!
//! `// TODO(bridge)`:`fetch_image`/`store_output_image` 依赖
//! bridge::render 的帧访问 C ABI(声明未冻结),保留 todo!()。
//! **已落地(M11 第 1 期)**:帧访问 C ABI 在 [`crate::bridge::render`]
//! 冻结(`oakrender_display_texture_*`/`oakrender_codec_frame_*`),
//! 两处桥实现完成。
use crate::instance::{OfxRangeD, OfxRectD, RenderScale};
use crate::property::PropertySet;
/// clip 实例。
#[repr(C)]
pub struct ClipInstance {
/// 实例级 clip 属性(当前分量/位深/像素比,协商结果写入;
/// 偏移 0,句柄约定)。
pub props: PropertySet,
/// clip 名。
pub name: String,
/// 当前输入纹理(oakrender 句柄的借用拷贝;输出 clip 为 None)。
input_texture: std::sync::Mutex<Option<crate::bridge::render::TextureHandle>>,
/// 当前输出纹理(C++ `output_textures_` 的 phase 1 单槽;
/// [`store_output_image`](Self::store_output_image) 的回写目标;
/// 输入 clip 为 None)。
output_texture: std::sync::Mutex<Option<crate::bridge::render::TextureHandle>>,
}
/// 从 clip 属性读协商分量(getClipPreferences 写入)。
fn components_from_props(props: &PropertySet) -> Option<crate::image::Components> {
use crate::property::Value;
match props.get(crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS, 0)? {
Value::String(s) => match s.to_string_lossy().as_ref() {
"OfxImageComponentRGBA" => Some(crate::image::Components::Rgba),
"OfxImageComponentRGB" => Some(crate::image::Components::Rgb),
"OfxImageComponentAlpha" => Some(crate::image::Components::Alpha),
_ => None,
},
_ => None,
}
}
impl ClipInstance {
/// 按描述符实例化(createInstance 路径调用;公开:宿主与测试
/// 都需要构造 clip 实例)。实例 props 是描述符 props 的深拷贝
/// (HS: ClipBase 的实例构造,ofxhClip.cpp:57-70——插件在实例期
/// 读 supported components 等)。
///
/// ofxColour(M11 §4):输入 clip 的 kOfxImageClipPropColourspace
/// 由宿主写为工作空间(ACEScg,ofxColour.h "Hosts should set this
/// property to the colourspace of the input clip. Typically it will
/// be set to the working colourspace");输出 clip 由
/// GetOutputColourspace action 后写。
pub fn from_descriptor(desc: &crate::descriptor::ClipDescriptor) -> Self {
let props = desc.props.clone();
let name = desc.name.clone();
if name != "Output" {
props.set_one(
crate::host::PROP_CLIP_COLOURSPACE,
crate::property::Value::String(
std::ffi::CString::new(crate::host::WORKING_COLOURSPACE).unwrap(),
),
);
}
Self {
props,
name,
input_texture: std::sync::Mutex::new(None),
output_texture: std::sync::Mutex::new(None),
}
}
/// 写协商后的像素格式(C++ `OliveClipInstance::setParams` 的 props
/// 侧:setPixelDepth/setComponents,oliveclip.cpp:700-709)。
/// `format` 为 olive::PixelFormat::Format(0=u8, 2=u16, 3=f16,
/// 4=f32);`channels` 为分量数。
pub fn set_video_params(&self, format: i32, channels: i32) {
let depth = match format {
0 => "OfxBitDepthByte",
2 => "OfxBitDepthShort",
3 => "OfxBitDepthHalf",
_ => "OfxBitDepthFloat",
};
let comps = match channels {
1 => "OfxImageComponentAlpha",
3 => "OfxImageComponentRGB",
_ => "OfxImageComponentRGBA",
};
self.props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
crate::property::Value::String(std::ffi::CString::new(depth).unwrap()),
);
self.props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS,
crate::property::Value::String(std::ffi::CString::new(comps).unwrap()),
);
}
/// 写协商 RoD(C++ `setRegionOfDefinition` 的单槽版,
/// oliveclip.cpp:674-678——C++ 按 time 存 map,本驱动一帧一槽;
/// 落点与 image effect suite 的 clipGetRegionOfDefinition 读取处
/// 一致)。
pub fn set_region_of_definition(&self, rod: OfxRectD, _time: f64) {
use crate::property::Value;
self.props.define(
"OfxImageEffectPropRegionOfDefinition",
vec![
Value::Double(rod.x1),
Value::Double(rod.y1),
Value::Double(rod.x2),
Value::Double(rod.y2),
],
);
}
/// 挂接输入纹理(oaknode 侧 clip 输入值变化时由 param/render 桥
/// 调用)。`time` 用于多帧纹理选择。空句柄断开。
pub fn set_input_texture(
&self,
texture: crate::bridge::render::TextureHandle,
_time: f64,
) {
let mut slot = self.input_texture.lock().unwrap_or_else(|e| e.into_inner());
if texture.is_null() {
*slot = None;
} else {
*slot = Some(texture);
}
}
/// 挂接输出纹理(render 驱动创建并经句柄传入;C++
/// `setOutputTexture` 的 phase 1 单槽版)。`time` 用于多帧纹理
/// 选择(`// [P2]`)。空句柄断开。
pub fn set_output_texture(
&self,
texture: crate::bridge::render::TextureHandle,
_time: f64,
) {
let mut slot = self
.output_texture
.lock()
.unwrap_or_else(|e| e.into_inner());
if texture.is_null() {
*slot = None;
} else {
*slot = Some(texture);
}
}
/// 抓取本 clip 在 `time` 的图像(OFX clipGetImage 的宿主侧)。
/// CPU 路径:把 oakrender 纹理 readback 成 [`crate::image::Image`]
/// (像素格式按协商结果,全链路 F32)。
/// `// [P2]` GL 路径:clipLoadTexture 语义在此扩展。
///
/// 第 1 期约束:帧必须是 f32 格式(全链路 F32);`region` 只支持
/// None(整帧)——转换(u8→f32 等)与子区域随 renderer 桥落地。
pub fn fetch_image(
&self,
time: f64,
scale: RenderScale,
region: Option<OfxRectD>,
) -> crate::error::Result<crate::image::Image> {
use crate::bridge::render::*;
use crate::error::Error;
let _ = (time, scale);
if region.is_some() {
return Err(Error::Failed("fetch_image 子区域第 1 期不支持".into()));
}
let texture = self
.input_texture
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
.ok_or(Error::NotFound)?;
// 占位纹理(dummy):视作无输入。
if unsafe { crate::bridge::render::texture_is_dummy(texture) } != 0 {
return Err(Error::NotFound);
}
let mut frame = FrameHandle::null();
let r = unsafe { crate::bridge::render::texture_get_frame(texture, &mut frame) };
if r != 0 || frame.is_null() {
return Err(Error::Failed("纹理无 CPU 帧".into()));
}
let mut params = VideoParams::default();
unsafe { crate::bridge::render::frame_get_params(frame, &mut params) };
if params.format != PIXEL_FORMAT_F32 {
unsafe { crate::bridge::render::frame_free(&mut frame) };
return Err(Error::Failed(format!(
"输入帧格式 {} 非 F32(第 1 期约束)",
params.format
)));
}
let (w, h) = (params.width as f64, params.height as f64);
// 分量按协商结果(getClipPreferences 已写入 clip.props)。
let components = components_from_props(&self.props).unwrap_or(crate::image::Components::Rgba);
let mut image = crate::image::Image::allocate(
crate::image::BitDepth::Float,
components,
OfxRectD { x1: 0.0, y1: 0.0, x2: w, y2: h },
);
let src = unsafe { crate::bridge::render::frame_data(frame) };
if src.is_null() {
unsafe { crate::bridge::render::frame_free(&mut frame) };
return Err(Error::Failed("帧无数据".into()));
}
// 行优先拷贝(帧行跨度经 linesize 读取——真实 oakrender 帧可
// 有行填充;目标 Image 恒紧凑。M11 §4 修复:phase 1 假设紧凑
// 行,对真实 oakrender 的填充帧会写错列)。
let channels = components.channel_count();
let tight = (w as usize) * channels * 4;
let row = unsafe { crate::bridge::render::frame_linesize_bytes(frame) } as usize;
let row = if row > 0 { row } else { tight };
let src_bytes = unsafe { std::slice::from_raw_parts(src as *const u8, row * h as usize) };
let dst = image.pixels_mut();
for y in 0..h as usize {
let s = y * row;
let d = y * tight;
dst[d..d + tight].copy_from_slice(&src_bytes[s..s + tight]);
}
unsafe { crate::bridge::render::frame_free(&mut frame) };
Ok(image)
}
/// 把输出图像回写为 oakrender 纹理(render 完成后由
/// [`crate::instance::Instance::render`] 的调用方使用)。
///
/// 输出纹理由 oakrender 侧创建并经 [`Self::set_output_texture`]
/// 挂入——本函数取该纹理的 CPU 帧(`texture_get_frame`),按帧
/// 参数校验 F32 与尺寸后整帧拷贝图像像素(全链路 F32;C++
/// pluginrenderer 的 `readback/wrap` 路径第 1 期以 CPU 拷贝表达,
/// GL 走 [`crate::bridge::render`] 的 `// [P2]`)。未挂输出纹理
/// 或纹理为占位(dummy)→ [`crate::error::Error::NotFound`]。
/// 成功返回纹理句柄(借用拷贝,调用方负责其生命周期)。
pub fn store_output_image(
&self,
image: &crate::image::Image,
) -> crate::error::Result<crate::bridge::render::TextureHandle> {
use crate::bridge::render::*;
use crate::error::Error;
let texture = self
.output_texture
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
.ok_or(Error::NotFound)?;
if unsafe { texture_is_dummy(texture) } != 0 {
return Err(Error::NotFound);
}
let mut frame = FrameHandle::null();
let r = unsafe { texture_get_frame(texture, &mut frame) };
if r != 0 || frame.is_null() {
return Err(Error::Failed("输出纹理无 CPU 帧".into()));
}
let mut params = VideoParams::default();
unsafe { frame_get_params(frame, &mut params) };
if params.format != PIXEL_FORMAT_F32 {
unsafe { frame_free(&mut frame) };
return Err(Error::Failed(format!(
"输出帧格式 {} 非 F32(第 1 期约束)",
params.format
)));
}
let (w, h) = (params.width as usize, params.height as usize);
// 图像与帧必须同尺寸(全链路 F32;宽高/行宽/总长逐项校验)。
let tight = w * image.components().channel_count() * 4;
if tight != image.row_bytes() || tight * h != image.pixels().len() {
unsafe { frame_free(&mut frame) };
return Err(Error::Failed("图像尺寸与输出帧不一致".into()));
}
let dst = unsafe { frame_data(frame) };
if dst.is_null() {
unsafe { frame_free(&mut frame) };
return Err(Error::Failed("输出帧无数据".into()));
}
// 行优先拷贝(目标帧行跨度经 linesize 读取——真实 oakrender
// 帧可有行填充;M11 §4 修复同 fetch_image)。
let row = unsafe { frame_linesize_bytes(frame) } as usize;
let row = if row > 0 { row } else { tight };
let dst_bytes = unsafe { std::slice::from_raw_parts_mut(dst as *mut u8, row * h) };
let pixels = image.pixels();
for y in 0..h {
let d = y * row;
let s = y * tight;
dst_bytes[d..d + tight].copy_from_slice(&pixels[s..s + tight]);
}
unsafe { frame_free(&mut frame) };
Ok(texture)
}
/// 本 clip 的时间域(clipGetFrameRange)。
///
/// `// TODO(bridge)`:输入范围经 oakrender 帧的时间基推导
/// (time_base)——随 renderer 桥落地。
pub fn frame_range(&self) -> crate::error::Result<OfxRangeD> {
let _ = OfxRangeD::default();
Err(crate::error::Error::Failed("frame_range 待 renderer 桥".into()))
}
}
+130
View File
@@ -0,0 +1,130 @@
// 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/>.
//! describe 的产物:效果描述符与 clip 描述符。
//!
//! 参照:HS: ofxhImageEffect.cpp `Descriptor`(describe action 期间
//! 插件可读写的属性容器)。
//!
//! 句柄约定([`crate::suites::tag`]):`props` 在偏移 0 且
//! `#[repr(C)]`;`params`/`clips` 元素装箱(Vec 重分配不移动对象)。
use std::ffi::CString;
use crate::param::ParamDef;
use crate::property::{PropertySet, Value};
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
// ---- clip 描述符属性(ofxImageEffect.h / ofxCore.h)----
/// kOfxImageClipPropOptional。
pub(crate) const CLIP_OPTIONAL: &str = "OfxImageClipPropOptional";
/// kOfxImageClipPropIsMask。
pub(crate) const CLIP_IS_MASK: &str = "OfxImageClipPropIsMask";
/// kOfxImageClipPropFieldExtraction。
pub(crate) const CLIP_FIELD_EXTRACTION: &str = "OfxImageClipPropFieldExtraction";
/// kOfxImageEffectPropSupportedComponents。
pub(crate) const CLIP_SUPPORTED_COMPONENTS: &str = "OfxImageEffectPropSupportedComponents";
/// kOfxImageEffectPropTemporalClipAccess。
pub(crate) const CLIP_TEMPORAL_ACCESS: &str = "OfxImageEffectPropTemporalClipAccess";
/// kOfxImageEffectPropSupportsTiles。
pub(crate) const CLIP_SUPPORTS_TILES: &str = "OfxImageEffectPropSupportsTiles";
/// kOfxImageFieldDoubled(FieldExtraction 默认值)。
pub(crate) const CLIP_FIELD_DOUBLED: &str = "OfxImageFieldDoubled";
/// kOfxTypeClip。
pub(crate) const CLIP_TYPE: &str = "OfxTypeClip";
/// clip 描述符(describe 期间由插件定义)。
/// `#[repr(C)]` + props 在偏移 0(句柄约定;describe 期 clip handle
/// 即 &props,与实例期 [`crate::clip::ClipInstance`] 共用 CLIP 标签)。
#[repr(C)]
pub struct ClipDescriptor {
/// 描述属性:label、可选性(kOfxImageClipPropOptional)、支持分量
/// (kOfxImageEffectPropSupportedComponents)、field 支持等。
pub props: PropertySet,
/// clip 名(如 "Source"/"Output")。
pub name: String,
}
impl ClipDescriptor {
/// 按名构建(镜像 HS ofxhClip.cpp `ClipDescriptor::ClipDescriptor`
/// + clipDescriptorStuffs 属性表,ofxhClip.cpp:22-38)。公开:
/// 测试构造 clip 实例(GL suite 往返)。
pub fn new(name: &str) -> Self {
let props = PropertySet::new();
props.set_one(crate::param::PROP_TYPE, Value::String(cs(CLIP_TYPE)));
props.set_one(crate::param::PROP_NAME, Value::String(cs(name)));
props.set_one(crate::param::PROP_LABEL, Value::String(cs(name)));
props.set_one(crate::param::PROP_SHORT_LABEL, Value::String(cs("")));
props.set_one(crate::param::PROP_LONG_LABEL, Value::String(cs("")));
props.define(CLIP_SUPPORTED_COMPONENTS, vec![]);
props.set_one(CLIP_TEMPORAL_ACCESS, Value::Int(0));
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)));
props.set_one(CLIP_SUPPORTS_TILES, Value::Int(1));
// ofxColour(M11 §4):clip 色彩空间属性族。Colourspace 由宿主
// 在实例化时写入(输入 clip = 工作空间 ACEScg);Preferred 由
// 插件在 GetClipPreferences 写(宿主侧预置空数组)。
props.set_one(
crate::host::PROP_CLIP_COLOURSPACE,
Value::String(cs("")),
);
props.define(crate::host::PROP_CLIP_PREFERRED_COLOURSPACES, vec![]);
Self {
props,
name: name.to_string(),
}
}
}
/// 效果描述符。
/// `#[repr(C)]` + props 在偏移 0(句柄约定:describe 期 effect/
/// param-set handle 即 &props)。
#[repr(C)]
pub struct EffectDescriptor {
/// 效果级属性:label、描述、分组、单帧/时间域标记等。
pub props: PropertySet,
/// 参数定义集(describe 期间插件经 Param suite 填充;
/// 值类型见 [`crate::param::ParamDef`];Box 保证句柄地址稳定)。
pub params: Vec<Box<ParamDef>>,
/// clip 定义集(含 "Output";Box 保证句柄地址稳定)。
pub clips: Vec<Box<ClipDescriptor>>,
}
impl EffectDescriptor {
/// 空描述符(describe action 前由 host 创建并预置根属性)。
pub fn new() -> Self {
Self {
props: PropertySet::new(),
params: Vec::new(),
clips: Vec::new(),
}
}
/// 按名找 clip。
pub fn clip(&self, name: &str) -> Option<&ClipDescriptor> {
self.clips.iter().find(|c| c.name == name).map(|b| b.as_ref())
}
/// 按名找参数定义。
pub fn param(&self, name: &str) -> Option<&ParamDef> {
self.params.iter().find(|p| p.name == name).map(|b| b.as_ref())
}
}
+62
View File
@@ -0,0 +1,62 @@
// 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/>.
//! 错误码。与 `include/*/error.h` 逐字对应;项目统一 -MMCCCC 方案
//! (模块号注册表见 include/common/error.h),跨模块透传不翻译。
/// 成功。
pub const OAKPLUGIN_OK: i32 = 0;
/// 空句柄或非法参数。
pub const OAKPLUGIN_E_INVALID: i32 = -90001;
/// 当前状态不允许该调用(如未扫描就创建实例)。
pub const OAKPLUGIN_E_STATE: i32 = -90002;
/// 底层操作失败(OFX action 返回非 kOfxStatOK、插件入口拒绝等)。
pub const OAKPLUGIN_E_FAILED: i32 = -90003;
/// 索引越界 / 指定标识的插件不存在。
pub const OAKPLUGIN_E_NOT_FOUND: i32 = -90004;
/// 分配失败。
pub const OAKPLUGIN_E_NOMEM: i32 = -90005;
/// crate 内部统一的结果类型;FFI 层把它映射为上述 i32 码。
pub type Result<T> = std::result::Result<T, Error>;
/// crate 内部错误。`code()` 给出对外错误码。
#[derive(Debug)]
pub enum Error {
/// 空句柄或非法参数。
Invalid,
/// 状态不允许。
State,
/// 底层失败,附人类可读上下文(仅日志,不出界)。
Failed(String),
/// 未找到。
NotFound,
/// 分配失败。
NoMem,
}
impl Error {
/// 映射为 `include/plugin/error.h` 的错误码。
pub fn code(&self) -> i32 {
match self {
Error::Invalid => OAKPLUGIN_E_INVALID,
Error::State => OAKPLUGIN_E_STATE,
Error::Failed(_) => OAKPLUGIN_E_FAILED,
Error::NotFound => OAKPLUGIN_E_NOT_FOUND,
Error::NoMem => OAKPLUGIN_E_NOMEM,
}
}
}
File diff suppressed because it is too large Load Diff
+226
View File
@@ -0,0 +1,226 @@
// 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/>.
//! 引用计数句柄脚手架。
//!
//! 对应 C 侧布局(`include/plugin/instance.h`,与 oak 全项目约定一致):
//!
//! ```c
//! typedef struct OakPluginInstance {
//! void *ctx;
//! void (*addref)(void *ctx);
//! void (*release)(void *ctx);
//! uint32_t abi_version;
//! } OakPluginInstance;
//! ```
//!
//! 句柄按值传;`ctx` 指向本 crate 堆上的 [`RefBox<T>`]。`addref`/
//! `release` 函数指针永远指向本 crate 的代码(所有权不出 DLL)。
use std::any::Any;
use std::collections::HashMap;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex, Weak};
use crate::error::OAKPLUGIN_E_FAILED;
/// 当前 ABI 版本,写进每个句柄的 `abi_version` 字段。
pub const OAKPLUGIN_ABI_VERSION: u32 = 1;
/// 句柄背后的堆盒子。`owns == false` 的盒子(借用包装)在计数归零时
/// 只释放盒子本身,不销毁内含对象。
pub struct RefBox<T: ?Sized> {
/// 引用计数(原子;release 可在任意线程发生)。
pub refs: AtomicU32,
/// 内含对象。
pub value: T,
}
/// C 句柄的 Rust 镜像。`#[repr(C)]`,与 C 头文件布局一致。
///
/// 生命周期:`*_init`/`*_create` 返回计数 1 的拥有型句柄;
/// The shared ABI value-handle type (single-lib unification, see
/// `docs/zh/plans/riir/single-lib.md`): one canonical
/// `{ctx, addref, release, abi_version}` type in `oakcore-rs`, re-exported
/// here so the crate's `ffi.rs` signatures and handle scaffolding stay
/// source-compatible. `Send + Sync` come from the shared type.
pub use oakcore_rs::handle::CHandle;
/// addref 的实现:原子 +1。拥有型与借用型共用——借用型只延长盒子
/// 的寿命,不延长被借用对象。
unsafe extern "C" fn refbox_addref<T: Any + Send>(ctx: *mut std::ffi::c_void) {
unsafe {
let rb = ctx as *const RefBox<T>;
// 调用方保证句柄在借用期内有效(ctx 非空且未被释放)。
(*rb).refs.fetch_add(1, Ordering::Relaxed);
}
}
/// release 的实现(拥有型):原子 -1,归零时回收盒子并销毁内含对象。
unsafe extern "C" fn refbox_release_owned<T: Any + Send>(ctx: *mut std::ffi::c_void) {
unsafe {
let rb = ctx as *mut RefBox<T>;
// AcqRel:归零这一侧要能看见最后一次引用前的全部写(含对象
// 析构所需的内部状态)。
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
drop(Box::from_raw(rb));
}
}
}
/// release 的实现(借用型,[`make_borrowed`] 的产物):归零时只回收
/// 盒子内存,把内含对象原样忘掉——其所有权仍在借用方手里。
unsafe extern "C" fn refbox_release_borrowed<T: Any + Send>(ctx: *mut std::ffi::c_void) {
unsafe {
let rb = ctx as *mut RefBox<T>;
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
// 部分 move:把 value 移出临时 Box,Box 析构只释放分配;
// value 用 forget 放弃析构(double-free 防线)。
std::mem::forget((Box::from_raw(rb)).value);
}
}
}
/// 为 `T` 制作拥有型句柄(计数 1)。分配失败返回空句柄并销毁对象。
///
/// 注:Rust 默认分配失败(OOM)直接 abort,不会走到"返回空句柄"
/// 路径;此处语义保留给未来接入自定义分配器的场景。
pub fn make_owned<T: Any + Send>(value: T) -> CHandle {
let rb = Box::into_raw(Box::new(RefBox {
refs: AtomicU32::new(1),
value,
}));
CHandle {
ctx: rb as *mut std::ffi::c_void,
addref: Some(refbox_addref::<T>),
release: Some(refbox_release_owned::<T>),
abi_version: OAKPLUGIN_ABI_VERSION,
}
}
/// 为已有对象制作借用句柄(计数归零只释放盒子)。`ptr` 必须在本
/// 句柄被释放前保持有效。
///
/// 语义:按位拷贝("借用拷贝",如纹理句柄的快照);被借用对象
/// 的析构完全由调用方负责,盒子从不碰它。拷贝即快照——借出后
/// 修改 `*ptr` 不会反映到句柄内。
///
/// # Safety
/// 调用方保证 `ptr` 的生命周期覆盖所有派生句柄,且其值在借用期内
/// 不被 move/析构。
pub unsafe fn make_borrowed<T: Any + Send>(ptr: *mut T) -> CHandle {
if ptr.is_null() {
return CHandle::null();
}
let rb = Box::into_raw(Box::new(RefBox {
refs: AtomicU32::new(1),
value: unsafe { std::ptr::read(ptr) },
}));
CHandle {
ctx: rb as *mut std::ffi::c_void,
addref: Some(refbox_addref::<T>),
release: Some(refbox_release_borrowed::<T>),
abi_version: OAKPLUGIN_ABI_VERSION,
}
}
/// 取回盒子内对象的不可变引用;空句柄返回 `None`。
///
/// # Safety
/// 调用方必须保证 `T` 与创建句柄时的类型一致。
pub unsafe fn get<T: Any>(h: &CHandle) -> Option<&T> {
if h.is_null() {
return None;
}
unsafe { Some(&(*(h.ctx as *const RefBox<T>)).value) }
}
/// FFI 兜底:捕获 panic,把 `Result<i32>` 映射为对外错误码
/// ([`crate::error`])。所有返回 i32 的导出函数必须经它。
///
/// panic 路径返回 `OAKPLUGIN_E_FAILED`;panic 详情暂不落日志
/// (message 桥接入后补 TODO)。
pub fn guard<F>(f: F) -> i32
where
F: FnOnce() -> crate::error::Result<()>,
{
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(())) => crate::error::OAKPLUGIN_OK,
Ok(Err(e)) => e.code(),
Err(_) => OAKPLUGIN_E_FAILED,
}
}
/// 指针/句柄返回值版本的 [`guard`]:panic 或 Err 时返回空句柄
/// (指针类返回 NULL)。
pub fn guard_handle<F>(f: F) -> CHandle
where
F: FnOnce() -> crate::error::Result<CHandle>,
{
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(h)) => h,
Ok(Err(_)) | Err(_) => CHandle::null(),
}
}
/// 无返回值版本:panic 被吞并记录日志(经 bridge 的日志回调)。
pub fn guard_void<F>(f: F)
where
F: FnOnce(),
{
let _ = catch_unwind(AssertUnwindSafe(f));
}
/// 句柄身份注册表:`usize` 身份 ↔ 弱引用。供 param↔node 等需要
/// "按身份找回对象"的桥使用(替代 M9 C++ 版的
/// `oaknode_node_identity()` 注册表)。
pub struct Registry<T: Any + Send> {
map: Mutex<HashMap<usize, Weak<RefBox<T>>>>,
}
impl<T: Any + Send> Registry<T> {
/// 空注册表。
pub fn new() -> Self {
Self {
map: Mutex::new(HashMap::new()),
}
}
/// 登记对象,返回其身份(地址语义,进程内唯一)。
pub fn register(&self, obj: &Arc<RefBox<T>>) -> usize {
// Arc 分配地址即身份:同一 RefBox 恒稳定,进程内唯一。
let id = Arc::as_ptr(obj) as *const () as usize;
lock(&self.map).insert(id, Arc::downgrade(obj));
id
}
/// 按身份取对象;对象已销毁或身份未知返回 `None`。
pub fn lookup(&self, id: usize) -> Option<Arc<RefBox<T>>> {
lock(&self.map).get(&id).and_then(|w| w.upgrade())
}
/// 摘除身份(对象销毁路径调用)。未知身份是 no-op。
pub fn unregister(&self, id: usize) {
lock(&self.map).remove(&id);
}
}
/// 取锁。毒锁(本 crate 代码持锁时 panic)时接管内部状态继续——
/// 一次 panic 不级联成后续所有 FFI 调用失败。
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(|e| e.into_inner())
}
File diff suppressed because it is too large Load Diff
+256
View File
@@ -0,0 +1,256 @@
// 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/>.
//! OFX image: an OFX view of a frame buffer (CPU path).
//!
//! Counterpart of the C++ `OliveImage`. Pixel memory is owned by this
//! crate (this was historically a hotspot of memory bugs: ownership
//! must be single, and lifetime is guaranteed by this type).
//! GL texture views are deferred to phase 2 (`// [P2]`).
//!
//! Property writes mirror the C++ `Image::allocate`
//! (image.cpp:132-172) and the HostSupport image property table
//! (HS: ofxhClip.cpp:458-472): Data/RowBytes/Bounds/
//! RegionOfDefinition/Components/PixelDepth/UniqueIdentifier.
//! This struct does not distinguish bounds from ROD (unified), and
//! does not model field/renderScale/pixelAspect/premultiplication
//! (`// [P2]`).
use std::ffi::{c_void, CString};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::instance::OfxRectD;
use crate::property::{PropertySet, Value};
// ---- OFX property names (macro strings of ofxCore.h / ofxImageEffect.h) ----
/// kOfxImagePropData (ofxCore.h:1275): pixel data pointer.
pub(crate) const K_IMAGE_PROP_DATA: &str = "OfxImagePropData";
/// kOfxImagePropRowBytes (ofxCore.h:1322): row byte count.
pub(crate) const K_IMAGE_PROP_ROW_BYTES: &str = "OfxImagePropRowBytes";
/// kOfxImagePropBounds (ofxCore.h:1291): pixel coordinates, Int x 4.
pub(crate) const K_IMAGE_PROP_BOUNDS: &str = "OfxImagePropBounds";
/// kOfxImagePropRegionOfDefinition (ofxCore.h:1307): pixel coordinates, Int x 4.
pub(crate) const K_IMAGE_PROP_ROD: &str = "OfxImagePropRegionOfDefinition";
/// kOfxImageEffectPropComponents (ofxImageEffect.h:915).
pub(crate) const K_IMAGE_EFFECT_PROP_COMPONENTS: &str = "OfxImageEffectPropComponents";
/// kOfxImageEffectPropPixelDepth (ofxImageEffect.h:901).
pub(crate) const K_IMAGE_EFFECT_PROP_PIXEL_DEPTH: &str = "OfxImageEffectPropPixelDepth";
/// kOfxImagePropUniqueIdentifier (ofxCore.h:927): host-assigned unique id.
pub(crate) const K_IMAGE_PROP_UNIQUE_ID: &str = "OfxImagePropUniqueIdentifier";
/// Process-wide monotonically increasing id (zero-dependency
/// replacement for HostSupport's UUID generation).
static NEXT_IMAGE_ID: AtomicU64 = AtomicU64::new(0);
/// Pixel bit depth (OFX kOfxBitDepth*; the full pipeline only uses
/// Float, the rest are kept for compatibility).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BitDepth {
/// 8-bit integer (compat).
Byte,
/// 16-bit integer (compat).
Short,
/// 16-bit half float (compat).
Half,
/// 32-bit float (main path).
Float,
}
impl BitDepth {
/// Bytes per component.
pub(crate) fn bytes_per_component(self) -> usize {
match self {
BitDepth::Byte => 1,
BitDepth::Short | BitDepth::Half => 2,
BitDepth::Float => 4,
}
}
/// OFX bit depth string (kOfxBitDepth*, ofxCore.h:866-880).
pub(crate) fn to_ofx(self) -> &'static str {
match self {
BitDepth::Byte => "OfxBitDepthByte",
BitDepth::Short => "OfxBitDepthShort",
BitDepth::Half => "OfxBitDepthHalf",
BitDepth::Float => "OfxBitDepthFloat",
}
}
}
/// Component layout (OFX kOfxImageComponent*).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Components {
/// RGBA.
Rgba,
/// RGB.
Rgb,
/// Single-channel Alpha.
Alpha,
}
impl Components {
/// Channel count.
pub(crate) fn channel_count(self) -> usize {
match self {
Components::Rgba => 4,
Components::Rgb => 3,
Components::Alpha => 1,
}
}
/// OFX component string (kOfxImageComponent*, ofxImageEffect.h:46-55).
pub(crate) fn to_ofx(self) -> &'static str {
match self {
Components::Rgba => "OfxImageComponentRGBA",
Components::Rgb => "OfxImageComponentRGB",
Components::Alpha => "OfxImageComponentAlpha",
}
}
}
/// A single frame. `data` is row-major; the row stride may be padded
/// for alignment; bounds are in pixel coordinates.
/// `#[repr(C)]` with `props` at offset 0 (handle convention,
/// see [`crate::suites::tag`]).
#[repr(C)]
pub struct Image {
/// Image-level properties (bounds, row bytes, depth, components,
/// unique identifier).
pub props: PropertySet,
/// Pixel buffer (length = row_bytes * height).
data: Vec<u8>,
/// Bit depth.
depth: BitDepth,
/// Components.
components: Components,
/// Pixel bounds.
bounds: OfxRectD,
/// Row byte count.
row_bytes: usize,
}
impl Image {
/// Allocate by format (uninitialized pixels).
///
/// Property writes mirror the C++ `Image::allocate`
/// (image.cpp:156-172): Data/RowBytes/Bounds/RegionOfDefinition/
/// Components/PixelDepth; UniqueIdentifier is also written per the
/// image property table (HS: ofxhClip.cpp:470).
/// ROD is unified with bounds (this struct does not distinguish
/// the two; both are pixel coordinates). A zero-size or inverted
/// rectangle yields an empty buffer (mirrors the
/// `buffer_size < 0 -> 0` guard in image.cpp:147-149).
pub fn allocate(depth: BitDepth, components: Components, bounds: OfxRectD) -> Self {
let (w, h) = {
let width = (bounds.x2 - bounds.x1).round();
let height = (bounds.y2 - bounds.y1).round();
if width > 0.0 && height > 0.0 {
(width as usize, height as usize)
} else {
(0, 0)
}
};
let row_bytes = w * components.channel_count() * depth.bytes_per_component();
let mut img = Self {
props: PropertySet::new(),
data: vec![0u8; row_bytes * h],
depth,
components,
bounds,
row_bytes,
};
// `data` is a heap buffer; moving/borrowing the vec does not move
// the buffer, and it is never resized after allocation, so the
// pointer stays valid for the whole Image lifetime (ownership
// discipline: this is the only place that holds the buffer).
img.props.define(
K_IMAGE_PROP_DATA,
vec![Value::Pointer(img.data.as_mut_ptr() as *mut c_void)],
);
img.props.define(
K_IMAGE_PROP_ROW_BYTES,
vec![Value::Int(row_bytes as i32)],
);
let b = |v: f64| Value::Int(v.round() as i32);
img.props.define(
K_IMAGE_PROP_BOUNDS,
vec![b(bounds.x1), b(bounds.y1), b(bounds.x2), b(bounds.y2)],
);
img.props.define(
K_IMAGE_PROP_ROD,
vec![b(bounds.x1), b(bounds.y1), b(bounds.x2), b(bounds.y2)],
);
img.props.define(
K_IMAGE_EFFECT_PROP_COMPONENTS,
vec![Value::String(CString::new(components.to_ofx()).unwrap())],
);
img.props.define(
K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
vec![Value::String(CString::new(depth.to_ofx()).unwrap())],
);
img.props.define(
K_IMAGE_PROP_UNIQUE_ID,
vec![Value::String(unique_identifier())],
);
img
}
/// Mutable pixel slice (for writing plugin output). The length is
/// consistent with the format by type construction.
pub fn pixels_mut(&mut self) -> &mut [u8] {
&mut self.data
}
/// Read-only pixel slice.
pub fn pixels(&self) -> &[u8] {
&self.data
}
/// Pixel bounds (canonical coordinates; the Image keeps bounds and
/// ROD unified).
pub fn bounds(&self) -> OfxRectD {
self.bounds
}
/// Bit depth.
pub fn depth(&self) -> BitDepth {
self.depth
}
/// Components.
pub fn components(&self) -> Components {
self.components
}
/// Row byte count.
pub fn row_bytes(&self) -> usize {
self.row_bytes
}
}
/// Unique identifier string (monotonically increasing per process,
/// hexadecimal; corresponds to the `uniqueIdentifier` parameter of
/// HS: ofxhClip.cpp:537).
pub(crate) fn unique_identifier() -> CString {
let n = NEXT_IMAGE_ID.fetch_add(1, Ordering::Relaxed);
// Hexadecimal ASCII, no NUL; unwrap cannot fail.
CString::new(format!("{:x}", n)).unwrap()
}
+968
View File
@@ -0,0 +1,968 @@
// 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/>.
//! 插件实例:action 调用面。
//!
//! 对应 C++ 的 `OlivePluginInstance`。语义参照:
//! HS: ofxhImageEffect.cpp `Instance`(action 调用序列与参数集组装)。
use std::ffi::CString;
use std::sync::Arc;
use crate::clip::ClipInstance;
use crate::host::Plugin;
use crate::param::ParamSetInstance;
use crate::property::PropertySet;
/// paramEditBegin/End 的编辑事务状态(undo 分组)。对应 C++
/// oliveplugininstance.cpp 的 `edit_depth_`/`edit_command_`:
/// 事务内的多次参数回写合并为一条 multi 命令,editEnd 时整体
/// redo 后释放。
#[repr(C)]
pub struct EditTransaction {
/// 嵌套深度(editBegin/End 必须配对)。
depth: i32,
/// 事务累积的 multi 命令(空句柄 = 尚无子命令)。
multi: crate::bridge::undo::CommandHandle,
/// 事务内回写次数(标签计数)。
param_count: i32,
/// 第一条回写的标签。
first_label: String,
}
impl EditTransaction {
/// 空事务(无嵌套、无累积命令)。
pub fn new() -> Self {
Self {
depth: 0,
multi: crate::bridge::undo::CommandHandle::null(),
param_count: 0,
first_label: String::new(),
}
}
}
/// 时间范围(OFX 规范双精度秒)。
#[derive(Clone, Copy, Debug, Default)]
pub struct OfxRangeD {
/// 起始(含)。
pub min: f64,
/// 结束(含)。
pub max: f64,
}
/// 矩形(规范坐标,double)。
#[derive(Clone, Copy, Debug, Default)]
pub struct OfxRectD {
/// 左。
pub x1: f64,
/// 上。
pub y1: f64,
/// 右。
pub x2: f64,
/// 下。
pub y2: f64,
}
/// 像素比/场序等渲染参量(render action 的 in_args 子集)。
#[derive(Clone, Copy, Debug)]
pub struct RenderScale {
/// x 方向渲染比例。
pub x: f64,
/// y 方向渲染比例。
pub y: f64,
}
/// 插件实例。`Arc<RefBox<Instance>>` 管理生命周期;身份注册见
/// [`crate::handle::Registry`](param 桥按身份反查)。
///
/// `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`];
/// 实例期 effect/param-set handle 即 `&props`)。
#[repr(C)]
pub struct Instance {
/// 实例级属性集(偏移 0,句柄约定)。
pub props: PropertySet,
/// 所属插件。
pub plugin: Arc<Plugin>,
/// 上下文(kOfxImageEffectContext*)。
pub context: String,
/// 参数实例集(createInstance 后由 describe 产物实例化)。
pub params: ParamSetInstance,
/// clip 实例集(含 "Output";Box 保证句柄地址稳定)。
pub clips: Vec<Box<ClipInstance>>,
/// 绑定的 oaknode 节点身份(param 桥写入;0 = 未绑定)。
pub node_identity: std::sync::atomic::AtomicUsize,
/// destroyInstance action 是否已通知(析构幂等门;Drop 驱动;
/// 公开:宿主 shutdown 与测试都要置位)。
pub destroyed: std::sync::atomic::AtomicBool,
/// beginSequenceRender 的时间域(timeline getTimeBounds 用;
/// endSequenceRender 清除)。
pub sequence_range: std::sync::Mutex<Option<OfxRangeD>>,
/// facade 注册的进度回调(oakplugin_instance_set_progress_cb)。
pub progress_cb: std::sync::Mutex<Option<(crate::progress::ProgressFn, usize)>>,
/// facade 取消标记(oakplugin_instance_cancel)。
pub cancel: std::sync::atomic::AtomicBool,
/// paramEditBegin/End 的编辑事务(undo 分组)。
pub edit: std::sync::Mutex<EditTransaction>,
/// 实例级渲染串行化(对应 C++ `OlivePluginInstance::mutex`;
/// pluginrenderer.cpp:1436-1444 的 instance_lock——渲染路径非
/// 线程安全,并发 render 必须互斥)。
pub render_lock: std::sync::Mutex<()>,
}
/// 实例销毁路径:先通知 destroyInstance action,再摘除身份登记。
/// (RefBox 归零时 Drop 触发——action 通知必须在对象析构前发出。)
impl Drop for Instance {
fn drop(&mut self) {
if !self.destroyed.swap(true, std::sync::atomic::Ordering::Relaxed) {
self.notify_destroy();
}
crate::suites::param::unregister_params_of(&self.props as *const _ as usize);
crate::host::instance_registry().unregister(
&self.props as *const crate::property::PropertySet as usize,
);
}
}
impl Instance {
/// 绑定 oaknode 节点(C++ `set_node_handle` 的 Rust 侧;装配期由
/// facade/测试调用)。`identity` 为 [`crate::host::instance_registry`]
/// 无关的 oaknode 节点身份(`oaknode_node_identity` 的地址语义);
/// 0 解除绑定。
pub fn bind_node(&self, identity: usize) {
self.node_identity
.store(identity, std::sync::atomic::Ordering::Relaxed);
}
/// paramEditBegin:进入编辑事务(可嵌套;首次进入重置事务状态)。
pub fn edit_begin(&self) {
let mut e = self.edit.lock().unwrap_or_else(|e| e.into_inner());
e.depth += 1;
if e.depth == 1 {
e.multi = crate::bridge::undo::CommandHandle::null();
e.param_count = 0;
e.first_label.clear();
}
}
/// paramEditEnd:退出编辑事务;最外层结束时把累积的 multi 命令
/// redo 生效并释放(本 crate 无 undo 栈——命令在 oakundo 侧
/// 提交,见 C++ `submit_undo_command` 的 fallback 分支)。
pub fn edit_end(&self) {
let mut e = self.edit.lock().unwrap_or_else(|e| e.into_inner());
if e.depth > 0 {
e.depth -= 1;
}
if e.depth == 0 && !e.multi.is_null() {
unsafe { crate::bridge::undo::command_redo_now(e.multi) };
unsafe { crate::bridge::undo::command_free(&mut e.multi) };
e.param_count = 0;
e.first_label.clear();
}
}
/// 是否处于编辑事务内(param 桥回写据此决定打包成 multi)。
pub fn in_edit(&self) -> bool {
self.edit
.lock()
.unwrap_or_else(|e| e.into_inner())
.depth
> 0
}
/// 事务感知的 undo 提交(param 桥回写用;对应 C++
/// oliveplugininstance.cpp:390-414 `submit_undo_command`):
/// 编辑事务内并入 multi(子命令立即 redo 生效,值即时可见),
/// 否则单命令 redo 后释放。
pub(crate) fn submit_undo_command(
&self,
mut cmd: crate::bridge::undo::CommandHandle,
label: &str,
) {
if cmd.is_null() {
return;
}
if self.in_edit() {
let mut e = self.edit.lock().unwrap_or_else(|e| e.into_inner());
if e.multi.is_null() {
e.multi = unsafe { crate::bridge::undo::command_init_multi() };
}
e.param_count += 1;
if e.first_label.is_empty() {
e.first_label = label.to_string();
}
unsafe { crate::bridge::undo::command_redo_now(cmd) };
unsafe { crate::bridge::undo::command_multi_add_child(e.multi, cmd) };
} else {
unsafe { crate::bridge::undo::command_redo_now(cmd) };
unsafe { crate::bridge::undo::command_free(&mut cmd) };
}
}
/// getClipPreferences action,返回协商结果(输出 clip 的分量/
/// 位深/像素比/field 与帧率)。协商顺序逐行对照
/// HS: ofxhImageEffect.cpp `Instance::getClipPreferences`——
/// 当年调试重灾区,实现时必须附行号注释。
///
/// 协商流程(HS: ofxhImageEffect.cpp:1686-1740):
/// 1. out args 预置 clipPrefsStuffs(frameRate=1、premult、
/// fieldOrder、continuousSamples、frameVarying,HS:1697-1710);
/// 2. 预置 per-clip 的 "OfxImageClipPropComponents_<name>" 等
/// (默认 RGBA/Float/PAR=1,HS:1717-1727);
/// 3. 调 action(out args 即协商产物,HS:1708-1718);
/// 4. 回灌:per-clip 分量/位深/像素比写回 clip 实例属性,
/// 输出帧率/fielding/premult 记入实例(HS:1721-1740)。
pub fn get_clip_preferences(&self) -> crate::error::Result<ClipPreferences> {
use crate::property::Value;
use crate::host::{
ACTION_GET_CLIP_PREFERENCES, CLIP_PREF_COMPONENTS, CLIP_PREF_DEPTH, CLIP_PREF_PAR,
PROP_CONTINUOUS_SAMPLES, PROP_FIELD_ORDER, PROP_FRAME_RATE, PROP_FRAME_VARYING,
PROP_PREMULT,
};
// 1. clipPrefsStuffs(HS:1697-1706 的默认值)。
let out = PropertySet::new();
out.set_one(PROP_FRAME_RATE, Value::Double(1.0));
out.set_one(PROP_PREMULT, Value::String(CString::new("").unwrap()));
out.set_one(PROP_FIELD_ORDER, Value::String(CString::new("").unwrap()));
out.set_one(PROP_CONTINUOUS_SAMPLES, Value::Int(0));
out.set_one(PROP_FRAME_VARYING, Value::Int(0));
// 2. per-clip 预置(HS:1717-1727;phase 1 全链路 F32+RGBA)。
for clip in &self.clips {
out.set_one(&crate::host::clip_pref_prop(CLIP_PREF_COMPONENTS, &clip.name), Value::String(CString::new("OfxImageComponentRGBA").unwrap()));
out.set_one(&crate::host::clip_pref_prop(CLIP_PREF_DEPTH, &clip.name), Value::String(CString::new("OfxBitDepthFloat").unwrap()));
out.set_one(&crate::host::clip_pref_prop(CLIP_PREF_PAR, &clip.name), Value::Double(1.0));
}
// 3. action。
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let empty = PropertySet::new();
let stat = unsafe {
self.plugin
.call_action(ACTION_GET_CLIP_PREFERENCES, inst_handle, &empty, &out)
};
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
return Err(crate::error::Error::Failed(format!(
"getClipPreferences 失败:{stat}"
)));
}
// 4. 回灌(HS:1721-1740:每 clip 的协商结果写回实例属性)。
let mut output_components = "OfxImageComponentRGBA".to_string();
let mut output_bit_depth = "OfxBitDepthFloat".to_string();
let mut output_par = 1.0;
for clip in &self.clips {
let comp_name = crate::host::clip_pref_prop(CLIP_PREF_COMPONENTS, &clip.name);
let depth_name = crate::host::clip_pref_prop(CLIP_PREF_DEPTH, &clip.name);
let par_name = crate::host::clip_pref_prop(CLIP_PREF_PAR, &clip.name);
let comps = out
.get(&comp_name, 0)
.map(|v| match v {
Value::String(s) => s.to_string_lossy().into_owned(),
_ => "OfxImageComponentRGBA".to_string(),
})
.unwrap_or_else(|| "OfxImageComponentRGBA".to_string());
let depth = out
.get(&depth_name, 0)
.map(|v| match v {
Value::String(s) => s.to_string_lossy().into_owned(),
_ => "OfxBitDepthFloat".to_string(),
})
.unwrap_or_else(|| "OfxBitDepthFloat".to_string());
let par = out
.get(&par_name, 0)
.and_then(|v| match v {
Value::Double(d) => Some(d),
_ => None,
})
.unwrap_or(1.0);
clip.props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS,
Value::String(CString::new(comps.clone()).unwrap()),
);
clip.props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
Value::String(CString::new(depth.clone()).unwrap()),
);
clip.props.set_one(
"OfxImagePropPixelAspectRatio",
Value::Double(par),
);
if clip.name == "Output" {
output_components = comps;
output_bit_depth = depth;
output_par = par;
}
}
let frame_rate = out
.get(PROP_FRAME_RATE, 0)
.and_then(|v| match v {
Value::Double(d) => Some(d),
_ => None,
})
.unwrap_or(24.0);
let field = out
.get(PROP_FIELD_ORDER, 0)
.map(|v| match v {
Value::String(s) => s.to_string_lossy().into_owned(),
_ => String::new(),
})
.unwrap_or_default();
Ok(ClipPreferences {
output_components,
output_bit_depth,
pixel_aspect_ratio: output_par,
frame_rate,
field,
})
}
/// getRegionOfDefinition action(HS: ofxhImageEffect.cpp:1087-1130:
/// in args = time + renderScale;out args = RegionOfDefinition)。
pub fn get_region_of_definition(
&self,
time: f64,
scale: RenderScale,
) -> crate::error::Result<OfxRectD> {
use crate::property::Value;
use crate::host::{ACTION_GET_ROD, PROP_RENDER_SCALE, PROP_ROD, PROP_TIME};
let in_args = PropertySet::new();
in_args.set_one(PROP_TIME, Value::Double(time));
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(scale.x), Value::Double(scale.y)]);
// out args 预定义(HS 行为:宿主先建属性表,插件只管写——
// 缺失属性上插件的 propSet 会失败被忽略)。
let out = PropertySet::new();
out.define(
PROP_ROD,
vec![
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
],
);
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let stat = unsafe {
self.plugin
.call_action(ACTION_GET_ROD, inst_handle, &in_args, &out)
};
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
return Err(crate::error::Error::Failed(format!("getRoD 失败:{stat}")));
}
read_rect(&out, PROP_ROD).ok_or(crate::error::Error::Failed("getRoD 无输出".into()))
}
/// getRegionsOfInterest action:输入 clip 的 RoI 写入 out_args
/// 属性集,返回值即各 clip 的 RoI 列表(与 `clips` 顺序一致)。
///
/// 约定名 "OfxImageEffectPropRegionOfInterest_<clipname>"(HS:
/// ofxhImageEffect.cpp getRegionsOfInterest 的 per-clip 前缀)。
pub fn get_regions_of_interest(
&self,
time: f64,
scale: RenderScale,
region: OfxRectD,
) -> crate::error::Result<Vec<OfxRectD>> {
use crate::property::Value;
use crate::host::{ACTION_GET_ROI, PROP_RENDER_SCALE, PROP_ROI, PROP_TIME};
let in_args = PropertySet::new();
in_args.set_one(PROP_TIME, Value::Double(time));
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(scale.x), Value::Double(scale.y)]);
in_args.define(
PROP_ROI,
vec![
Value::Double(region.x1),
Value::Double(region.y1),
Value::Double(region.x2),
Value::Double(region.y2),
],
);
// out args 预定义 per-clip ROI 属性(HS 约定名前缀)。
let out = PropertySet::new();
for clip in &self.clips {
let name = format!("OfxImageEffectPropRegionOfInterest_{}", clip.name);
out.define(
&name,
vec![
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
],
);
}
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let stat = unsafe {
self.plugin
.call_action(ACTION_GET_ROI, inst_handle, &in_args, &out)
};
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
return Err(crate::error::Error::Failed(format!("getRoI 失败:{stat}")));
}
let mut rois = Vec::with_capacity(self.clips.len());
for clip in &self.clips {
let name = format!("OfxImageEffectPropRegionOfInterest_{}", clip.name);
let rect = read_rect(&out, &name)
// 未设置 → 默认整个 region(OFX 语义:插件可不写)。
.unwrap_or(region);
rois.push(rect);
}
Ok(rois)
}
/// isIdentity action:返回 Some((time, input_clip_name)) 表示本帧
/// 直接透传该输入 clip;None 表示需要真正 render。
///
/// 参照 HS: ofxhImageEffect.cpp:1378-1450:in args = time + scale +
/// renderWindow + fieldToRender;out args = kOfxImageEffectPropIsIdentity
/// (输入 clip 名)+ kOfxPropTime(透传时间,可改)。
pub fn is_identity(&self, time: f64) -> crate::error::Result<Option<(f64, String)>> {
use crate::property::Value;
use crate::host::{
ACTION_IS_IDENTITY, PROP_FIELD_TO_RENDER, PROP_IS_IDENTITY, PROP_RENDER_SCALE,
PROP_RENDER_WINDOW, PROP_TIME,
};
let in_args = PropertySet::new();
in_args.set_one(PROP_TIME, Value::Double(time));
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(1.0), Value::Double(1.0)]);
in_args.define(
PROP_RENDER_WINDOW,
vec![
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
],
);
in_args.set_one(PROP_FIELD_TO_RENDER, Value::String(CString::new("OfxImageFieldBoth").unwrap()));
// out args 预定义:IsIdentity(String)+ Time(Double)。
let out = PropertySet::new();
out.set_one(PROP_IS_IDENTITY, Value::String(CString::new("").unwrap()));
out.set_one(PROP_TIME, Value::Double(time));
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
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}")));
}
let identity = out.get(PROP_IS_IDENTITY, 0);
match identity {
Some(Value::String(s)) if !s.is_empty() => {
let clip_name = s.to_string_lossy().into_owned();
let t = out
.get(PROP_TIME, 0)
.and_then(|v| match v {
Value::Double(d) => Some(d),
_ => None,
})
.unwrap_or(time);
Ok(Some((t, clip_name)))
}
_ => Ok(None),
}
}
/// render action 的 in args 组装(CPU/GL 共用;GL 模式加
/// kOfxImageEffectPropOpenGLEnabled=1,ofxGPURender.h:117)。
fn render_in_args(time: f64, scale: RenderScale, window: OfxRectD, gl_enabled: bool) -> PropertySet {
use crate::property::Value;
use crate::host::{
PROP_FIELD_TO_RENDER, PROP_INTERACTIVE_RENDER, PROP_NO_SPATIAL_AWARENESS,
PROP_RENDER_QUALITY_DRAFT, PROP_RENDER_SCALE, PROP_RENDER_WINDOW,
PROP_SEQUENTIAL_RENDER, PROP_TIME,
};
let in_args = PropertySet::new();
in_args.set_one(PROP_TIME, Value::Double(time));
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(scale.x), Value::Double(scale.y)]);
in_args.define(
PROP_RENDER_WINDOW,
vec![
Value::Double(window.x1),
Value::Double(window.y1),
Value::Double(window.x2),
Value::Double(window.y2),
],
);
in_args.set_one(PROP_FIELD_TO_RENDER, Value::String(CString::new("OfxImageFieldBoth").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));
if gl_enabled {
in_args.set_one(crate::host::PROP_GL_ENABLED, Value::Int(1));
}
in_args
}
/// render action(CPU 路径)。`output` 为已按协商格式分配的输出
/// 图像(Arc:插件经 clipGetImage(Output) 取用,render 驱动与
/// suite 各自持强引用);输入 clip 的图像由调用方经
/// [`ClipInstance::fetch_image`] 备好。GL 路径见
/// [`Instance::render_gl`]。
///
/// 声明原为 `&mut Image`:输出图像需与 suite 共享强引用(TLS +
/// LIVE_IMAGES 表),&mut 无法表达——改为 `Arc<Image>`。
pub fn render(
&self,
time: f64,
scale: RenderScale,
window: OfxRectD,
output: std::sync::Arc<crate::image::Image>,
) -> crate::error::Result<()> {
use crate::host::ACTION_RENDER;
let in_args = Self::render_in_args(time, scale, window, false);
// facade 取消标记:render 入口即短路(多帧循环的帧间取消)。
if self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
return Err(crate::error::Error::Failed("已取消".into()));
}
// 渲染上下文(TLS):timeline/progress/clipGetImage 读取。
let range = self
.sequence_range
.lock()
.unwrap_or_else(|e| e.into_inner())
.unwrap_or(OfxRangeD { min: 0.0, max: 0.0 });
crate::suites::set_render_ctx(Some(crate::suites::RenderCtx {
time,
scale,
range,
}));
crate::suites::set_current_output(Some(output.clone()));
// 进度报告器(facade 回调 → Progress suite)。
if let Some((cb, userdata)) = self
.progress_cb
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
{
crate::suites::progress::set_current(Some(unsafe {
crate::progress::ProgressReporter::new(cb, userdata as *mut std::ffi::c_void)
}));
}
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let out = PropertySet::new();
let stat = unsafe {
self.plugin
.call_action(ACTION_RENDER, inst_handle, &in_args, &out)
};
// 清理 TLS(action 返回后渲染上下文必须消失)。
crate::suites::set_current_output(None);
crate::suites::set_render_ctx(None);
crate::suites::progress::set_current(None);
if stat != crate::suites::status::OK {
return Err(crate::error::Error::Failed(format!("render 失败:{stat}")));
}
Ok(())
}
/// GL render action(M11 §4;ofxGPURender.h 的 Render 动作 GL
/// 模式)。与 [`Instance::render`](CPU)并存。
///
/// 前置契约(render 驱动遵守):调用方已把 `renderer` 的 GL
/// 上下文置为 current(ofxGPURender.h "OpenGL Current Context":
/// 宿主只在 Render/Begin/EndSequenceRender/Attach/Detach 期间
/// 要求上下文 current——本实现的约定是 oakrender 的 PluginJob
/// 路径在进入前做好),且 `output_texture` 已附着为渲染器输出
/// 目标(等价 C++ `PluginRenderer::attach_output_texture`)。
///
/// action 序列:kOfxActionOpenGLContextAttached → render(in args
/// 带 kOfxImageEffectPropOpenGLEnabled=1)→
/// kOfxActionOpenGLContextDetached(ofxGPURender.h:345-371;
/// attach/detach 必须配对)。渲染结果留在 GL 输出纹理上(插件
/// 直接画进附着目标),宿主不做 CPU 回读;GL 模式下
/// clipGetImage(Output) 不可用(插件按规范走 OpenGL suite——
/// ofxGPURender.h "the effect SHOULD access all its images through
/// the OpenGL suite")。render 返回前对未释放的输入 GL 纹理做
/// 兜底清理([`crate::suites::gl_render::purge_leftovers`])。
pub fn render_gl(
&self,
time: f64,
scale: RenderScale,
window: OfxRectD,
renderer: crate::bridge::render::RendererHandle,
output_texture: crate::bridge::render::TextureHandle,
) -> crate::error::Result<()> {
use crate::host::ACTION_RENDER;
let in_args = Self::render_in_args(time, scale, window, true);
if self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
return Err(crate::error::Error::Failed("已取消".into()));
}
let range = self
.sequence_range
.lock()
.unwrap_or_else(|e| e.into_inner())
.unwrap_or(OfxRangeD { min: 0.0, max: 0.0 });
crate::suites::set_render_ctx(Some(crate::suites::RenderCtx {
time,
scale,
range,
}));
// GL 纹理位深协商(插件 kOfxOpenGLPropPixelDepth → 管线 F32;
// 调用方已按协商门(render 驱动)决定走 GL)。
let gl_pixel_depth =
crate::suites::gl_render::pick_gl_pixel_depth(&self.plugin.descriptor.props)
.unwrap_or("OfxBitDepthFloat");
crate::suites::set_gl_ctx(Some(crate::suites::GlCtx {
renderer,
output_texture,
gl_pixel_depth,
}));
// GL 模式无 CPU 输出图像(current_output 保持 None)。
if let Some((cb, userdata)) = self
.progress_cb
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
{
crate::suites::progress::set_current(Some(unsafe {
crate::progress::ProgressReporter::new(cb, userdata as *mut std::ffi::c_void)
}));
}
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let empty = PropertySet::new();
// attach(失败仍继续——插件可忽略;规范允许 ReplyDefault)。
let stat = unsafe {
self.plugin
.call_action(crate::host::ACTION_GL_CONTEXT_ATTACHED, inst_handle, &empty, &empty)
};
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
crate::suites::set_gl_ctx(None);
crate::suites::set_render_ctx(None);
crate::suites::progress::set_current(None);
return Err(crate::error::Error::Failed(format!(
"OpenGLContextAttached 失败:{stat}"
)));
}
let out = PropertySet::new();
let stat = unsafe {
self.plugin
.call_action(ACTION_RENDER, inst_handle, &in_args, &out)
};
// detach(必须与 attach 配对;ofxGPURender.h:345-346)。
unsafe {
self.plugin
.call_action(crate::host::ACTION_GL_CONTEXT_DETACHED, inst_handle, &empty, &empty)
};
// 兜底:插件遗漏 clipFreeTexture 的输入纹理在此释放。
crate::suites::gl_render::purge_leftovers();
crate::suites::set_gl_ctx(None);
crate::suites::set_render_ctx(None);
crate::suites::progress::set_current(None);
if stat != crate::suites::status::OK {
return Err(crate::error::Error::Failed(format!("GL render 失败:{stat}")));
}
Ok(())
}
/// GetOutputColourspace action(M11 §4;ofxColour.h:243-283):
/// 宿主给插件一份偏好色彩空间列表,插件回写输出 clip 的色彩
/// 空间(可为 "OfxColourspace_<clip>" 交叉引用)。
///
/// 返回解析后的输出色彩空间:交叉引用解析为所引 clip 的实际
/// 色彩空间(ofxColour.h:142-141 的跨 clip 引用约定);
/// REPLY_DEFAULT → 第一个输入 clip 的色彩空间(ofxColour.h:278-279)。
/// 调用方(render 驱动)随后把结果写回输出 clip(
/// [`Instance::set_output_colourspace`])。
pub fn get_output_colourspace(&self, preferred: &[String]) -> crate::error::Result<String> {
use crate::property::Value;
use crate::host::{ACTION_GET_OUTPUT_COLOURSPACE, PROP_CLIP_COLOURSPACE, PROP_CLIP_PREFERRED_COLOURSPACES};
let in_args = PropertySet::new();
let values: Vec<Value> = preferred
.iter()
.map(|s| Value::String(CString::new(s.as_str()).unwrap()))
.collect();
in_args.define(PROP_CLIP_PREFERRED_COLOURSPACES, values);
let out = PropertySet::new();
out.set_one(PROP_CLIP_COLOURSPACE, Value::String(CString::new("").unwrap()));
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let stat = unsafe {
self.plugin
.call_action(ACTION_GET_OUTPUT_COLOURSPACE, inst_handle, &in_args, &out)
};
if stat == crate::suites::status::OK {
let value = out
.get(PROP_CLIP_COLOURSPACE, 0)
.map(|v| match v {
Value::String(s) => s.to_string_lossy().into_owned(),
_ => String::new(),
})
.unwrap_or_default();
if value.is_empty() {
return Err(crate::error::Error::Failed(
"GetOutputColourspace 未回写色彩空间".into(),
));
}
Ok(self.resolve_colourspace(value))
} else if stat == crate::suites::status::REPLY_DEFAULT {
// 插件未实现 → 用第一个输入 clip 的色彩空间(规范默认)。
let first_input = self
.clips
.iter()
.find(|c| c.name != "Output")
.map(|c| {
c.props
.get(PROP_CLIP_COLOURSPACE, 0)
.map(|v| match v {
Value::String(s) => s.to_string_lossy().into_owned(),
_ => String::new(),
})
.unwrap_or_default()
})
.unwrap_or_default();
Ok(first_input)
} else {
Err(crate::error::Error::Failed(format!(
"GetOutputColourspace 失败:{stat}"
)))
}
}
/// 把输出 clip 的色彩空间写回(GetOutputColourspace 后的落点;
/// ofxColour.h:262-264 "the host must set kOfxImageClipPropColourspace
/// on the instance's output clip to the value from outArgs")。
pub fn set_output_colourspace(&self, colourspace: &str) {
use crate::property::Value;
if let Some(output) = self.clips.iter().find(|c| c.name == "Output") {
output.props.set_one(
crate::host::PROP_CLIP_COLOURSPACE,
Value::String(CString::new(colourspace).unwrap()),
);
}
}
/// 解析 "OfxColourspace_<clip>" 交叉引用为所引 clip 的实际色彩
/// 空间;非交叉引用原样返回。
pub fn resolve_colourspace(&self, value: String) -> String {
const PREFIX: &str = "OfxColourspace_";
if let Some(clip_name) = value.strip_prefix(PREFIX) {
self.clips
.iter()
.find(|c| c.name == clip_name)
.map(|c| {
c.props
.get(crate::host::PROP_CLIP_COLOURSPACE, 0)
.map(|v| match v {
crate::property::Value::String(s) => s.to_string_lossy().into_owned(),
_ => value.clone(),
})
.unwrap_or(value.clone())
})
.unwrap_or(value)
} else {
value
}
}
/// begin/endSequenceRender 配对(帧序列渲染的前后括号;
/// HS: ofxhImageEffect.cpp:473-513 的 in args:frameRange +
/// frameStep + renderScale + sequentialRenderStatus)。GL 模式
/// 变体 [`Instance::begin_sequence_render_gl`] 额外带
/// kOfxImageEffectPropOpenGLEnabled(ofxGPURender.h:117)。
pub fn begin_sequence_render(&self, range: OfxRangeD) -> crate::error::Result<()> {
self.begin_sequence_inner(range, false)
}
/// GL 模式的 beginSequenceRender(in args 带 OpenGLEnabled=1)。
pub fn begin_sequence_render_gl(&self, range: OfxRangeD) -> crate::error::Result<()> {
self.begin_sequence_inner(range, true)
}
fn begin_sequence_inner(&self, range: OfxRangeD, gl_enabled: bool) -> crate::error::Result<()> {
use crate::host::ACTION_BEGIN_SEQUENCE;
let in_args = Self::sequence_in_args(range, gl_enabled);
self.sequence_range
.lock()
.unwrap_or_else(|e| e.into_inner())
.replace(range);
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let out = PropertySet::new();
let stat = unsafe {
self.plugin
.call_action(ACTION_BEGIN_SEQUENCE, inst_handle, &in_args, &out)
};
if stat != crate::suites::status::OK {
return Err(crate::error::Error::Failed(format!(
"beginSequenceRender 失败:{stat}"
)));
}
Ok(())
}
/// 序列 action 的 in args 组装(begin/end 共用;GL 模式加
/// OpenGLEnabled=1)。
fn sequence_in_args(range: OfxRangeD, gl_enabled: bool) -> PropertySet {
use crate::property::Value;
use crate::host::{PROP_FRAME_RANGE, PROP_FRAME_STEP, PROP_RENDER_SCALE, PROP_SEQUENTIAL_RENDER};
let in_args = PropertySet::new();
in_args.define(PROP_FRAME_RANGE, vec![Value::Double(range.min), Value::Double(range.max)]);
in_args.set_one(PROP_FRAME_STEP, Value::Double(1.0));
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(1.0), Value::Double(1.0)]);
in_args.set_one(PROP_SEQUENTIAL_RENDER, Value::Int(1));
if gl_enabled {
in_args.set_one(crate::host::PROP_GL_ENABLED, Value::Int(1));
}
in_args
}
/// 见 [`Instance::begin_sequence_render`]。
pub fn end_sequence_render(&self, range: OfxRangeD) -> crate::error::Result<()> {
self.end_sequence_inner(range, false)
}
/// GL 模式的 endSequenceRender。
pub fn end_sequence_render_gl(&self, range: OfxRangeD) -> crate::error::Result<()> {
self.end_sequence_inner(range, true)
}
fn end_sequence_inner(&self, range: OfxRangeD, gl_enabled: bool) -> crate::error::Result<()> {
use crate::host::ACTION_END_SEQUENCE;
let in_args = Self::sequence_in_args(range, gl_enabled);
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let out = PropertySet::new();
let stat = unsafe {
self.plugin
.call_action(ACTION_END_SEQUENCE, inst_handle, &in_args, &out)
};
self.sequence_range
.lock()
.unwrap_or_else(|e| e.into_inner())
.take();
if stat != crate::suites::status::OK {
return Err(crate::error::Error::Failed(format!(
"endSequenceRender 失败:{stat}"
)));
}
Ok(())
}
/// 销毁(destroyInstance action)。析构由 RefBox 驱动;
/// 此处只做 action 通知,幂等([`Instance::drop`] 的
/// `destroyed` 门保证只发一次)。
pub(crate) fn notify_destroy(&self) {
use crate::host::ACTION_DESTROY_INSTANCE;
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let empty = PropertySet::new();
// 通知失败只记日志(销毁路径不可回滚)。
let stat = unsafe {
self.plugin
.call_action(ACTION_DESTROY_INSTANCE, inst_handle, &empty, &empty)
};
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
eprintln!("destroyInstance 通知失败:{stat}");
}
}
}
/// 从属性集读矩形(Double×4)。
fn read_rect(props: &PropertySet, name: &str) -> Option<OfxRectD> {
use crate::property::Value;
let x1 = match props.get(name, 0)? {
Value::Double(d) => d,
_ => return None,
};
let y1 = match props.get(name, 1)? {
Value::Double(d) => d,
_ => return None,
};
let x2 = match props.get(name, 2)? {
Value::Double(d) => d,
_ => return None,
};
let y2 = match props.get(name, 3)? {
Value::Double(d) => d,
_ => return None,
};
Some(OfxRectD { x1, y1, x2, y2 })
}
/// getClipPreferences 的协商结果。
#[derive(Clone, Debug)]
pub struct ClipPreferences {
/// 输出分量(kOfxImageComponentRGBA 等)。
pub output_components: String,
/// 输出位深(kOfxBitDepthFloat 等;全链路 F32 下恒为 float)。
pub output_bit_depth: String,
/// 像素比。
pub pixel_aspect_ratio: f64,
/// 帧率。
pub frame_rate: f64,
/// field 处理模式(kOfxImageField*)。
pub field: String,
}
+63
View File
@@ -0,0 +1,63 @@
// 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/>.
//! # oakplugin — Oak 的插件模块(自研 OFX 宿主)
//!
//! 本 crate 是 oakplugin 模块的全部实现(M11):
//!
//! - **OFX 宿主**:扫描 bundle、加载插件、实现八张 suite
//! (property/memory/image_effect/param/message/progress/timeline/
//! multithread),驱动 describe/createInstance/render 等 action。
//! - **桥**:把 OFX 实例的参数接到 oaknode([`bridge::node`])、把
//! clip 的输入输出接到 oakrender 纹理([`bridge::render`])、把
//! 参数修改包成 undo 命令([`bridge::undo`])。
//! - **C ABI 出口**([`ffi`]):逐字实现 `include/plugin/*.h`。
//!
//! ## FFI 纪律(全 crate 最高优先级约定)
//!
//! 1. 每个 `extern "C"` 导出函数体必须包
//! [`handle::guard`]/[`handle::guard_ptr`](catch_unwind + 错误码
//! 映射)。panic 越过 FFI 边界是 release 阻断级缺陷。
//! 2. 句柄一律 [`handle::RefBox`];`ctx` 是不透明指针,含义只在本
//! crate 内解释。
//! 3. 插件可在任意自起线程回调 suite(multithread suite 存活期
//! 内);一切共享状态走 `Mutex`,句柄注册表见 [`handle::Registry`]。
//! 4. OFX 语义以 openfx HostSupport 为参照系;协商与时序实现点必须
//! 注释对应 HostSupport 文件与行号(格式:`// HS: ofxhImageEffect.cpp:2776`)。
//!
//! ## 第 1 期范围
//!
//! filter/generator/transition 上下文;CPU 渲染路径。GL 纹理 suite
//! 与 ofxColour 为第 2 期([`clip`]/[`image`] 内以 `// [P2]` 标记
//! 预留点)。
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
pub mod bridge;
pub mod clip;
pub mod descriptor;
pub mod error;
pub mod ffi;
pub mod handle;
pub mod host;
pub mod image;
pub mod instance;
pub mod param;
pub mod progress;
pub mod property;
pub mod render_driver;
pub mod suites;
+812
View File
@@ -0,0 +1,812 @@
// 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/>.
//! 参数体系:12 种参数实例 + param ↔ oaknode 桥。
//!
//! 对应 C++ 的 `ParamInstance`/`OliveParamInstance`。桥的语义
//! (M9 已定):节点输入值变化 → 写回 OFX 参数;OFX 参数被插件
//! 改动 → 经 oaknode C ABI 回写节点(undoable 经
//! [`crate::bridge::undo`])。
//!
//! 句柄约定([`crate::suites::tag`]):`ParamDef`/`ParamInstance`
//! `#[repr(C)]` 且 `props` 在偏移 0;元素装箱(`Vec<Box<..>>`)保证
//! Vec 重分配不移动对象、句柄不悬垂。
//!
//! `// TODO(bridge)`:`set_from_node`/`notify_instance_changed` 依赖
//! bridge::node 的 `Value` 布局(声明尚未冻结),保留 todo!()。
//! **已落地(M11 第 1 期)**:Value 布局随 [`crate::bridge::node`]
//! 冻结(`include/node/node.h` 的 `oaknode_value` POD),两处 todo
//! 已实现。
use std::ffi::CString;
use crate::property::{PropertySet, Value};
// ---- OFX 参数类型字符串(ofxParam.h)----
/// kOfxParamTypeInteger。
pub const TYPE_INTEGER: &str = "OfxParamTypeInteger";
/// kOfxParamTypeInteger2D。
pub const TYPE_INTEGER2D: &str = "OfxParamTypeInteger2D";
/// kOfxParamTypeInteger3D。
pub const TYPE_INTEGER3D: &str = "OfxParamTypeInteger3D";
/// kOfxParamTypeDouble。
pub const TYPE_DOUBLE: &str = "OfxParamTypeDouble";
/// kOfxParamTypeDouble2D。
pub const TYPE_DOUBLE2D: &str = "OfxParamTypeDouble2D";
/// kOfxParamTypeDouble3D。
pub const TYPE_DOUBLE3D: &str = "OfxParamTypeDouble3D";
/// kOfxParamTypeBoolean。
pub const TYPE_BOOLEAN: &str = "OfxParamTypeBoolean";
/// kOfxParamTypeChoice。
pub const TYPE_CHOICE: &str = "OfxParamTypeChoice";
/// kOfxParamTypeString。
pub const TYPE_STRING: &str = "OfxParamTypeString";
/// kOfxParamTypeStrChoice。
pub const TYPE_STRCHOICE: &str = "OfxParamTypeStrChoice";
/// kOfxParamTypeRGB。
pub const TYPE_RGB: &str = "OfxParamTypeRGB";
/// kOfxParamTypeRGBA。
pub const TYPE_RGBA: &str = "OfxParamTypeRGBA";
/// kOfxParamTypeBytes。
pub const TYPE_BYTES: &str = "OfxParamTypeBytes";
/// kOfxParamTypeCustom。
pub const TYPE_CUSTOM: &str = "OfxParamTypeCustom";
/// kOfxParamTypePushButton。
pub const TYPE_PUSHBUTTON: &str = "OfxParamTypePushButton";
/// kOfxParamTypeGroup。
pub const TYPE_GROUP: &str = "OfxParamTypeGroup";
/// kOfxParamTypePage。
pub const TYPE_PAGE: &str = "OfxParamTypePage";
/// kOfxParamTypeParametric(第 1 期不支持,paramDefine 拒绝)。
pub const TYPE_PARAMETRIC: &str = "OfxParamTypeParametric";
// ---- 属性名(ofxParam.h / ofxCore.h)----
/// kOfxParamPropSecret。
pub(crate) const P_SECRET: &str = "OfxParamPropSecret";
/// kOfxParamPropHint。
pub(crate) const P_HINT: &str = "OfxParamPropHint";
/// kOfxParamPropScriptName。
pub(crate) const P_SCRIPT_NAME: &str = "OfxParamPropScriptName";
/// kOfxParamPropParent。
pub(crate) const P_PARENT: &str = "OfxParamPropParent";
/// kOfxParamPropEnabled。
pub(crate) const P_ENABLED: &str = "OfxParamPropEnabled";
/// kOfxParamPropDataPtr。
pub(crate) const P_DATA_PTR: &str = "OfxParamPropDataPtr";
/// kOfxParamPropType。
pub(crate) const P_TYPE: &str = "OfxParamPropType";
/// kOfxParamPropIsAnimating。
pub(crate) const P_IS_ANIMATING: &str = "OfxParamPropIsAnimating";
/// kOfxParamPropIsAutoKeying。
pub(crate) const P_IS_AUTO_KEYING: &str = "OfxParamPropIsAutoKeying";
/// kOfxParamPropPersistant。
pub(crate) const P_PERSISTANT: &str = "OfxParamPropPersistant";
/// kOfxParamPropEvaluateOnChange。
pub(crate) const P_EVALUATE_ON_CHANGE: &str = "OfxParamPropEvaluateOnChange";
/// kOfxParamPropCanUndo。
pub(crate) const P_CAN_UNDO: &str = "OfxParamPropCanUndo";
/// kOfxParamPropCacheInvalidation。
pub(crate) const P_CACHE_INVALIDATION: &str = "OfxParamPropCacheInvalidation";
/// kOfxParamInvalidateValueChange(P_CACHE_INVALIDATION 的默认值)。
pub(crate) const V_INVALIDATE_VALUE_CHANGE: &str = "OfxParamInvalidateValueChange";
/// kOfxParamPropAnimates。
pub(crate) const P_ANIMATES: &str = "OfxParamPropAnimates";
/// kOfxParamPropDefault。
pub(crate) const P_DEFAULT: &str = "OfxParamPropDefault";
/// kOfxParamPropDisplayMin。
pub(crate) const P_DISPLAY_MIN: &str = "OfxParamPropDisplayMin";
/// kOfxParamPropDisplayMax。
pub(crate) const P_DISPLAY_MAX: &str = "OfxParamPropDisplayMax";
/// kOfxParamPropMin。
pub(crate) const P_MIN: &str = "OfxParamPropMin";
/// kOfxParamPropMax。
pub(crate) const P_MAX: &str = "OfxParamPropMax";
/// kOfxParamPropIncrement。
pub(crate) const P_INCREMENT: &str = "OfxParamPropIncrement";
/// kOfxParamPropDigits。
pub(crate) const P_DIGITS: &str = "OfxParamPropDigits";
/// kOfxParamPropDoubleType。
pub(crate) const P_DOUBLE_TYPE: &str = "OfxParamPropDoubleType";
/// kOfxParamDoubleTypePlain。
pub(crate) const V_DOUBLE_TYPE_PLAIN: &str = "OfxParamDoubleTypePlain";
/// kOfxParamPropDefaultCoordinateSystem。
pub(crate) const P_DEFAULT_COORD_SYS: &str = "OfxParamPropDefaultCoordinateSystem";
/// kOfxParamCoordinatesCanonical。
pub(crate) const V_COORD_CANONICAL: &str = "OfxParamCoordinatesCanonical";
/// kOfxParamPropShowTimeMarker。
pub(crate) const P_SHOW_TIME_MARKER: &str = "OfxParamPropShowTimeMarker";
/// kOfxParamPropDimensionLabel。
pub(crate) const P_DIMENSION_LABEL: &str = "OfxParamPropDimensionLabel";
/// kOfxParamPropStringMode。
pub(crate) const P_STRING_MODE: &str = "OfxParamPropStringMode";
/// kOfxParamStringIsSingleLine。
pub(crate) const V_STRING_SINGLE_LINE: &str = "OfxParamStringIsSingleLine";
/// kOfxParamPropStringFilePathExists。
pub(crate) const P_STRING_FILE_EXISTS: &str = "OfxParamPropStringFilePathExists";
/// kOfxParamPropChoiceOption。
pub(crate) const P_CHOICE_OPTION: &str = "OfxParamPropChoiceOption";
/// kOfxParamPropCustomInterpCallbackV1。
pub(crate) const P_CUSTOM_INTERP: &str = "OfxParamPropCustomCallbackV1";
/// kOfxParamPropPageChild。
pub(crate) const P_PAGE_CHILD: &str = "OfxParamPropPageChild";
/// kOfxParamPropGroupOpen。
pub(crate) const P_GROUP_OPEN: &str = "OfxParamPropGroupOpen";
/// kOfxParamPropInteractV1。
pub(crate) const P_INTERACT_V1: &str = "OfxParamPropInteractV1";
/// kOfxParamPropInteractSize。
pub(crate) const P_INTERACT_SIZE: &str = "OfxParamPropInteractSize";
/// kOfxParamPropInteractSizeAspect。
pub(crate) const P_INTERACT_ASPECT: &str = "OfxParamPropInteractSizeAspect";
/// kOfxParamPropInteractMinimumSize。
pub(crate) const P_INTERACT_MIN_SIZE: &str = "OfxParamPropInteractMinimumSize";
/// kOfxParamPropInteractPreferedSize。
pub(crate) const P_INTERACT_PREF_SIZE: &str = "OfxParamPropInteractPreferedSize";
/// kOfxPropType。
pub(crate) const PROP_TYPE: &str = "OfxPropType";
/// kOfxPropName。
pub(crate) const PROP_NAME: &str = "OfxPropName";
/// kOfxPropLabel。
pub(crate) const PROP_LABEL: &str = "OfxPropLabel";
/// kOfxPropShortLabel。
pub(crate) const PROP_SHORT_LABEL: &str = "OfxPropShortLabel";
/// kOfxPropLongLabel。
pub(crate) const PROP_LONG_LABEL: &str = "OfxPropLongLabel";
/// kOfxPropIcon。
pub(crate) const PROP_ICON: &str = "OfxPropIcon";
/// kOfxTypeParameter。
pub(crate) const TYPE_PARAMETER: &str = "OfxTypeParameter";
fn cs(s: &str) -> CString {
// 静态 ASCII 常量,无内嵌 NUL。
CString::new(s).unwrap()
}
/// 参数值类别(param shim 的变长参数分发;与
/// cbits/ofx_param_shim.c 的 KIND_* 枚举逐字对应)。
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ParamKind {
/// Integer。
Int = 1,
/// Integer2D。
Int2 = 2,
/// Integer3D。
Int3 = 3,
/// Double。
Double = 4,
/// Double2D。
Double2 = 5,
/// Double3D。
Double3 = 6,
/// Boolean。
Bool = 7,
/// Choice。
Choice = 8,
/// RGB。
Rgb = 9,
/// RGBA。
Rgba = 10,
/// String。
Str = 11,
/// StrChoice。
StrChoice = 12,
}
/// OFX 类型字符串 → 值类别 + 维度(HS: ofxhParam.cpp `findType`)。
/// 无值类(PushButton/Group/Page/Unknown)返回 None。
pub(crate) fn kind_of_type(ofx_type: &str) -> Option<(ParamKind, usize)> {
match ofx_type {
TYPE_INTEGER => Some((ParamKind::Int, 1)),
TYPE_INTEGER2D => Some((ParamKind::Int2, 2)),
TYPE_INTEGER3D => Some((ParamKind::Int3, 3)),
TYPE_DOUBLE => Some((ParamKind::Double, 1)),
TYPE_DOUBLE2D => Some((ParamKind::Double2, 2)),
TYPE_DOUBLE3D => Some((ParamKind::Double3, 3)),
TYPE_BOOLEAN => Some((ParamKind::Bool, 1)),
TYPE_CHOICE => Some((ParamKind::Choice, 1)),
TYPE_STRING => Some((ParamKind::Str, 1)),
TYPE_STRCHOICE => Some((ParamKind::StrChoice, 1)),
TYPE_RGB => Some((ParamKind::Rgb, 3)),
TYPE_RGBA => Some((ParamKind::Rgba, 4)),
_ => None,
}
}
/// 数值类参数(Min/Max/DisplayMin/Max 表格适用;HS
/// `addNumericParamProps` 的 isDoubleParam || isIntParam ||
/// isColourParam)。
fn is_numeric_type(ofx_type: &str) -> bool {
matches!(
ofx_type,
TYPE_INTEGER
| TYPE_INTEGER2D
| TYPE_INTEGER3D
| TYPE_DOUBLE
| TYPE_DOUBLE2D
| TYPE_DOUBLE3D
| TYPE_RGB
| TYPE_RGBA
)
}
/// 该类型的默认 ParamValue(describe 期基线;HS 的属性默认在 props
/// 的 kOfxParamPropDefault,值语义等价)。
fn type_default(ofx_type: &str) -> ParamValue {
match kind_of_type(ofx_type) {
Some((ParamKind::Int, d)) | Some((ParamKind::Int2, d)) | Some((ParamKind::Int3, d)) => {
ParamValue::Int([0; 3], d)
}
Some((ParamKind::Double, d))
| Some((ParamKind::Double2, d))
| Some((ParamKind::Double3, d)) => ParamValue::Double([0.0; 3], d),
Some((ParamKind::Rgb, d)) | Some((ParamKind::Rgba, d)) => ParamValue::Color([0.0; 4], d),
Some((ParamKind::Bool, _)) => ParamValue::Bool(false),
Some((ParamKind::Choice, _)) => ParamValue::Choice(0),
Some((ParamKind::Str, _)) => ParamValue::String(cs("")),
Some((ParamKind::StrChoice, _)) => ParamValue::StrChoice(cs("")),
None => match ofx_type {
TYPE_BYTES | TYPE_CUSTOM => ParamValue::Bytes(Vec::new()),
TYPE_PUSHBUTTON => ParamValue::PushButton,
TYPE_GROUP | TYPE_PAGE => ParamValue::Container,
_ => ParamValue::Container, // 未知类型占位(paramDefine 已拒绝)
},
}
}
/// 数值属性表的值(Min/Max/Display 族;HS ofxhParam.cpp:420-449)。
/// `is_max` 选 ±f64::MAX / ±i32::MAX;colour 参数恒 0/1。
fn numeric_value(ofx_type: &str, kind: ParamKind, dim: usize, is_max: bool) -> Vec<Value> {
if ofx_type == TYPE_RGB || ofx_type == TYPE_RGBA {
// colour:display 范围 0..1,min/max 同。
let v = Value::Double(if is_max { 1.0 } else { 0.0 });
return vec![v; dim];
}
match kind {
ParamKind::Double | ParamKind::Double2 | ParamKind::Double3 => {
let v = Value::Double(if is_max { f64::MAX } else { f64::MIN });
vec![v; dim]
}
_ => {
let v = Value::Int(if is_max { i32::MAX } else { i32::MIN });
vec![v; dim]
}
}
}
/// 参数值(与 OFX 参数类型一一对应)。
#[derive(Clone, Debug, PartialEq)]
pub enum ParamValue {
/// kOfxParamTypeInteger / Integer2D / Integer3D。
Int([i32; 3], usize),
/// kOfxParamTypeDouble / Double2D / Double3D。
Double([f64; 3], usize),
/// kOfxParamTypeBoolean。
Bool(bool),
/// kOfxParamTypeChoice(选项索引)。
Choice(i32),
/// kOfxParamTypeString。
String(std::ffi::CString),
/// kOfxParamTypeRGB / RGBA。
Color([f64; 4], usize),
/// kOfxParamTypeStrChoice。
StrChoice(std::ffi::CString),
/// kOfxParamTypeBytes / Custom(不透明字节串)。
Bytes(Vec<u8>),
/// kOfxParamTypePushButton(无值,仅触发)。
PushButton,
/// kOfxParamTypeGroup / Page(容器,无值)。
Container,
}
/// 参数定义(describe 产物,见 [`crate::descriptor::EffectDescriptor`])。
/// `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`])。
#[derive(Clone)]
#[repr(C)]
pub struct ParamDef {
/// 定义属性(label/hint/parent/coordinate-system/secret/
/// display min-max/choice 选项与排序等;describe 期预置
/// HostSupport 同款表格)。
pub props: PropertySet,
/// 参数名(OFX 标识)。
pub name: String,
/// OFX 类型字符串(kOfxParamType*)。
pub ofx_type: String,
/// 默认值(describe 后读取)。
pub default: ParamValue,
}
impl ParamDef {
/// 按 OFX 类型构建定义(镜像 HS ofxhParam.cpp:224-352:
/// universalProps + addStandardParamProps 的分类型属性表)。
pub(crate) fn new(name: &str, ofx_type: &str) -> Self {
let mut props = PropertySet::new();
let uname = name.to_string();
let utype = ofx_type.to_string();
// universalProps(HS ofxhParam.cpp:229-246)。
props.set_one(PROP_TYPE, Value::String(cs(TYPE_PARAMETER)));
props.set_one(P_SECRET, Value::Int(0));
props.set_one(P_HINT, Value::String(cs("")));
props.set_one(P_SCRIPT_NAME, Value::String(cs(&uname)));
props.set_one(P_PARENT, Value::String(cs("")));
props.set_one(P_ENABLED, Value::Int(1));
props.set_one(P_DATA_PTR, Value::Pointer(std::ptr::null_mut()));
props.set_one(P_TYPE, Value::String(cs(&utype)));
props.set_one(PROP_NAME, Value::String(cs(&uname)));
props.set_one(PROP_LABEL, Value::String(cs(&uname)));
props.set_one(PROP_SHORT_LABEL, Value::String(cs(&uname)));
props.set_one(PROP_LONG_LABEL, Value::String(cs(&uname)));
props.define(PROP_ICON, vec![Value::String(cs("")), Value::String(cs(""))]);
// 值类参数(HS addValueParamProps,ofxhParam.cpp:352-380)。
if let Some((kind, dim)) = kind_of_type(ofx_type) {
props.set_one(P_IS_ANIMATING, Value::Int(0));
props.set_one(P_IS_AUTO_KEYING, Value::Int(0));
props.set_one(P_PERSISTANT, Value::Int(1));
props.set_one(P_EVALUATE_ON_CHANGE, Value::Int(1));
props.set_one(P_CAN_UNDO, Value::Int(1));
props.set_one(P_CACHE_INVALIDATION, Value::String(cs(V_INVALIDATE_VALUE_CHANGE)));
// 可动画性(HS ofxhParam.cpp:367-372:custom/string/
// boolean/choice 默认不可动画)。
let animates = ofx_type != TYPE_CUSTOM
&& ofx_type != TYPE_STRING
&& ofx_type != TYPE_BOOLEAN
&& ofx_type != TYPE_CHOICE;
props.set_one(P_ANIMATES, Value::Int(animates as i32));
props.define(P_DEFAULT, default_values(kind, dim));
// 数值类(HS addNumericParamProps,ofxhParam.cpp:391-455)。
if is_numeric_type(ofx_type) {
props.define(P_DISPLAY_MIN, numeric_value(ofx_type, kind, dim, false));
props.define(P_DISPLAY_MAX, numeric_value(ofx_type, kind, dim, true));
props.define(P_MIN, numeric_value(ofx_type, kind, dim, false));
props.define(P_MAX, numeric_value(ofx_type, kind, dim, true));
if matches!(kind, ParamKind::Double | ParamKind::Double2 | ParamKind::Double3) {
props.set_one(P_INCREMENT, Value::Double(1.0));
props.set_one(P_DIGITS, Value::Int(2));
}
if ofx_type == TYPE_DOUBLE || ofx_type == TYPE_DOUBLE2D || ofx_type == TYPE_DOUBLE3D {
props.set_one(P_DOUBLE_TYPE, Value::String(cs(V_DOUBLE_TYPE_PLAIN)));
props.set_one(P_DEFAULT_COORD_SYS, Value::String(cs(V_COORD_CANONICAL)));
if dim == 1 {
props.set_one(P_SHOW_TIME_MARKER, Value::Int(0));
}
}
if dim == 2 || dim == 3 {
let labels: Vec<Value> = ["x", "y", "z"][..dim]
.iter()
.map(|l| Value::String(cs(l)))
.collect();
props.define(P_DIMENSION_LABEL, labels);
}
}
}
// 分类型附加(HS addStandardParamProps,ofxhParam.cpp:248-301)。
match ofx_type {
TYPE_STRING => {
props.set_one(P_STRING_MODE, Value::String(cs(V_STRING_SINGLE_LINE)));
props.set_one(P_STRING_FILE_EXISTS, Value::Int(1));
}
TYPE_CHOICE => {
// 维度 0:选项数由插件 SetN 决定。
props.define(P_CHOICE_OPTION, vec![]);
}
TYPE_CUSTOM => {
props.set_one(P_CUSTOM_INTERP, Value::Pointer(std::ptr::null_mut()));
}
TYPE_PAGE => {
props.define(P_PAGE_CHILD, vec![]);
}
TYPE_GROUP => {
props.set_one(P_GROUP_OPEN, Value::Int(1));
}
_ => {}
}
// 交互属性(HS addInteractParamProps,ofxhParam.cpp:305-317;
// group/page 除外)。
if ofx_type != TYPE_GROUP && ofx_type != TYPE_PAGE {
props.set_one(P_INTERACT_V1, Value::Pointer(std::ptr::null_mut()));
props.define(P_INTERACT_SIZE, vec![Value::Double(0.0), Value::Double(0.0)]);
props.set_one(P_INTERACT_ASPECT, Value::Double(1.0));
props.define(
P_INTERACT_MIN_SIZE,
vec![Value::Double(10.0), Value::Double(10.0)],
);
props.define(
P_INTERACT_PREF_SIZE,
vec![Value::Int(10), Value::Int(10)],
);
}
Self {
props,
name: uname,
ofx_type: utype,
default: type_default(ofx_type),
}
}
/// 值类别(变长参数分发用;无值类返回 None)。
pub(crate) fn kind(&self) -> Option<ParamKind> {
kind_of_type(&self.ofx_type).map(|(k, _)| k)
}
}
/// kOfxParamPropDefault 的属性值(HS ofxhParam.cpp:376-377)。
fn default_values(kind: ParamKind, dim: usize) -> Vec<Value> {
match kind {
ParamKind::Int | ParamKind::Int2 | ParamKind::Int3 | ParamKind::Bool | ParamKind::Choice => {
vec![Value::Int(0); dim.max(1)]
}
ParamKind::Double
| ParamKind::Double2
| ParamKind::Double3
| ParamKind::Rgb
| ParamKind::Rgba => vec![Value::Double(0.0); dim.max(1)],
ParamKind::Str | ParamKind::StrChoice => vec![Value::String(cs(""))],
}
}
/// 参数实例(createInstance 后由 [`ParamDef`] 实例化)。
/// `#[repr(C)]` + props 在偏移 0(句柄约定)。
#[repr(C)]
pub struct ParamInstance {
/// 实例级属性(当前值镜像等)。
pub props: PropertySet,
/// 对应定义。
pub def: ParamDef,
/// 当前值(节点桥与 OFX 侧的同步点)。
value: std::sync::Mutex<ParamValue>,
}
impl ParamInstance {
/// 从定义实例化(createInstance 路径调用)。实例 props 是定义
/// props 的深拷贝(HS: SetInstance::makeParam 的 descriptor 属性
/// 复制语义——插件在实例期读 label/min/max 等)。
pub fn from_def(def: ParamDef) -> Self {
let value = def.default.clone();
let props = def.props.clone();
Self {
props,
def,
value: std::sync::Mutex::new(value),
}
}
/// 读当前值。
pub fn get(&self) -> ParamValue {
self.value.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
/// 写当前值(OFX 语义:paramSetValue action 语义,不打节点桥)。
pub fn set_ofx(&self, value: ParamValue) {
*self.value.lock().unwrap_or_else(|e| e.into_inner()) = value;
}
/// 字符串值的内驻指针(param suite 的 get_string 用;指向实例
/// 存储,下次 set 前有效——克隆的 CString 指针会悬垂,不能给)。
pub(crate) fn string_ptr(&self) -> Option<*const std::ffi::c_char> {
let v = self.value.lock().unwrap_or_else(|e| e.into_inner());
match &*v {
ParamValue::String(s) | ParamValue::StrChoice(s) => Some(s.as_ptr()),
_ => None,
}
}
/// 从 oaknode 输入值写入(节点→插件方向)。类型映射表逐行对照
/// C++ 的 `valueconvert`/`paraminstance.h`(`node_get` 的镜像):
/// INT→Integer、FLOAT→Double、BOOL→Boolean、COMBO→Choice、
/// COLOR→RGB/RGBA、VEC2/VEC3→Double(2D/3D)/Integer(2D/3D)。
/// 维度不齐按 OFX 语义截断/补零(缺失元素按 0)。字符串族
/// (OAKNODE_VALUE_STRING)的 POD 不携带数据——此路径不改值
/// (走 facade 的字符串 API,见 `include/plugin/instance.h`)。
/// 类型不匹配 → 忽略(保持现值;C++ `node_get` 失败时参数不回写)。
pub fn set_from_node(&self, node_value: &crate::bridge::node::Value) {
use crate::bridge::node::node_value_type as T;
let v = node_value;
let mapped: Option<ParamValue> = match self.def.ofx_type.as_str() {
TYPE_DOUBLE => (v.r#type == T::FLOAT).then(|| ParamValue::Double([v.f[0], 0.0, 0.0], 1)),
TYPE_DOUBLE2D => {
(v.r#type == T::VEC2).then(|| ParamValue::Double([v.f[0], v.f[1], 0.0], 2))
}
TYPE_DOUBLE3D => (v.r#type == T::VEC3)
.then(|| ParamValue::Double([v.f[0], v.f[1], v.f[2]], 3)),
TYPE_INTEGER => (v.r#type == T::INT).then(|| ParamValue::Int([v.num as i32, 0, 0], 1)),
TYPE_INTEGER2D | TYPE_INTEGER3D => {
let dim = if self.def.ofx_type == TYPE_INTEGER2D { 2 } else { 3 };
Some(ParamValue::Int(
[v.f[0] as i32, v.f[1] as i32, v.f[2] as i32],
dim,
))
}
TYPE_BOOLEAN => (v.r#type == T::BOOL).then(|| ParamValue::Bool(v.num != 0)),
TYPE_CHOICE => (v.r#type == T::COMBO).then(|| ParamValue::Choice(v.num as i32)),
TYPE_RGB => (v.r#type == T::COLOR)
.then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], 0.0], 3)),
TYPE_RGBA => (v.r#type == T::COLOR)
.then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], v.f[3]], 4)),
_ => None, // 字符串/无值类:POD 无数据,不改值
};
if let Some(pv) = mapped {
self.set_ofx(pv);
}
}
}
/// ParamValue → [`crate::bridge::node::Value`](插件→节点方向;
/// 字符串族经 [`crate::bridge::node::set_input_string_undoable`])。
/// 镜像 C++ `paraminstance.h` 的 `value_int`/`value_double`/
/// `value_vec`/`value_color` 构造:RGB 颜色补 alpha=1(C++
/// `RGBInstance::set` 的 `value_color(r,g,b,1.0)`)。无值类与 Bytes
/// 无节点对应 → None。
pub(crate) fn to_node_value(v: &ParamValue) -> Option<crate::bridge::node::Value> {
use crate::bridge::node::node_value_type as T;
let mut out = crate::bridge::node::Value::default();
match v {
ParamValue::Double(d, 1) => {
out.r#type = T::FLOAT;
out.f = [d[0], 0.0, 0.0, 0.0];
}
ParamValue::Double(d, 2) => {
out.r#type = T::VEC2;
out.f = [d[0], d[1], 0.0, 0.0];
}
ParamValue::Double(d, 3) => {
out.r#type = T::VEC3;
out.f = [d[0], d[1], d[2], 0.0];
}
ParamValue::Double(d, _) => {
out.r#type = T::FLOAT;
out.f = [d[0], 0.0, 0.0, 0.0];
}
ParamValue::Int(a, 1) => {
out.r#type = T::INT;
out.num = a[0] as i64;
}
ParamValue::Int(a, 2) => {
out.r#type = T::VEC2;
out.f = [a[0] as f64, a[1] as f64, 0.0, 0.0];
}
ParamValue::Int(a, 3) => {
out.r#type = T::VEC3;
out.f = [a[0] as f64, a[1] as f64, a[2] as f64, 0.0];
}
ParamValue::Bool(b) => {
out.r#type = T::BOOL;
out.num = *b as i64;
}
ParamValue::Choice(c) => {
out.r#type = T::COMBO;
out.num = *c as i64;
}
ParamValue::Color(c, 3) => {
out.r#type = T::COLOR;
out.f = [c[0], c[1], c[2], 1.0];
}
ParamValue::Color(c, 4) => {
out.r#type = T::COLOR;
out.f = *c;
}
_ => return None,
}
Some(out)
}
/// 参数实例集(实例级)。
pub struct ParamSetInstance {
/// 全部参数(定义顺序稳定;Box 保证句柄地址稳定)。
pub params: Vec<Box<ParamInstance>>,
}
impl ParamSetInstance {
/// 按名查找。
pub fn find(&self, name: &str) -> Option<&ParamInstance> {
self.params.iter().find(|p| p.def.name == name).map(|b| b.as_ref())
}
/// 快照(内省 C ABI 与快照测试用)。
///
/// 声明原为 `Vec<(&str, &ParamValue)>`:值在 Mutex 内,无法返回
/// 活引用——改为拥有型 `(String, ParamValue)`(内省语义不变)。
pub fn snapshot(&self) -> Vec<(String, ParamValue)> {
self.params
.iter()
.map(|p| (p.def.name.clone(), p.get()))
.collect()
}
}
/// 插件 → 节点方向的回写入口(instanceChanged action 触发)。
/// 经 [`crate::bridge::node`] 定位绑定节点(身份注册表),再经
/// [`crate::bridge::undo`] 包成 undoable 修改。未绑定节点时 no-op。
///
/// 语义对照 C++ `paraminstance.h` 的 `set()` 路径(`detail::node_set`/
/// `node_set_at`):
/// - 只回写 [`ChangeReason::PluginEdited`](插件自改)。UserEdited/
/// TimeChanged 是宿主侧变更,值已由 [`ParamInstance::set_from_node`]
/// 同步,不重复写回;
/// - 字符串族经 `oaknode_node_set_input_string_undoable`
/// (POD 不携带字符串数据);
/// - 无值类(PushButton/Group/Page)与 Bytes 无节点对应 → no-op;
/// - 编辑事务内([`crate::instance::Instance::in_edit`])并入 multi
/// 命令,否则单命令立即 redo 生效(C++ `submit_undo_command`)。
pub(crate) fn notify_instance_changed(
instance: &crate::instance::Instance,
param_name: &str,
reason: ChangeReason,
) {
use crate::bridge::{node, undo};
use std::sync::atomic::Ordering;
if !matches!(reason, ChangeReason::PluginEdited) {
return;
}
// 未绑定节点 → no-op(C++:!node_.ctx 时 set 只写本地值)。
let node_id = instance.node_identity.load(Ordering::Relaxed);
if node_id == 0 {
return;
}
// 身份查无(注册表无此项 / 桥符号缺失)→ no-op。
let node_handle = unsafe { node::node_from_identity(node_id) };
if node_handle.is_null() {
return;
}
let Some(param) = instance.params.find(param_name) else {
return;
};
let value = param.get();
let label = format!("Change {param_name}");
let Some(cname) = CString::new(param_name).ok() else {
return;
};
match to_node_value(&value) {
Some(nv) => {
let mut cmd = undo::CommandHandle::null();
let r =
unsafe { node::set_input_undoable(node_handle, cname.as_ptr(), &nv, &mut cmd) };
if r == 0 && !cmd.is_null() {
instance.submit_undo_command(cmd, &label);
}
}
None => match &value {
ParamValue::String(s) | ParamValue::StrChoice(s) => {
let Some(cval) = CString::new(s.to_bytes()).ok() else {
return;
};
let mut cmd = undo::CommandHandle::null();
let r = unsafe {
node::set_input_string_undoable(
node_handle,
cname.as_ptr(),
cval.as_ptr(),
&mut cmd,
)
};
if r == 0 && !cmd.is_null() {
instance.submit_undo_command(cmd, &label);
}
}
_ => {} // 无值类 / Bytes:无节点对应
},
}
}
/// instanceChanged 的原因(OFX kOfxChange*)。
#[derive(Clone, Copy, Debug)]
pub enum ChangeReason {
/// 用户编辑。
UserEdited,
/// 插件自改。
PluginEdited,
/// 时间变化。
TimeChanged,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::node::node_value_type as T;
fn param(ofx_type: &str) -> ParamInstance {
ParamInstance::from_def(ParamDef::new("p", ofx_type))
}
/// ParamValue → oaknode Value 的类型映射表(镜像 C++
/// paraminstance.h 的 value_* 构造)。
#[test]
fn to_node_value_mapping() {
use crate::bridge::node::Value;
let c = |t: i32| Value {
r#type: t,
num: 0,
den: 0,
f: [0.0; 4],
};
// Double 族。
let v = to_node_value(&ParamValue::Double([1.5, 0.0, 0.0], 1)).unwrap();
assert_eq!(v, Value::float(1.5));
let v = to_node_value(&ParamValue::Double([1.0, 2.0, 0.0], 2)).unwrap();
assert_eq!(v, Value::vec(&[1.0, 2.0]));
let v = to_node_value(&ParamValue::Double([1.0, 2.0, 3.0], 3)).unwrap();
assert_eq!(v, Value::vec(&[1.0, 2.0, 3.0]));
// Int 族(2D/3D 升格为 VEC2/VEC3 浮点载荷)。
let v = to_node_value(&ParamValue::Int([7, 0, 0], 1)).unwrap();
assert_eq!(v, Value::int(7));
let v = to_node_value(&ParamValue::Int([1, 2, 0], 2)).unwrap();
assert_eq!(v, Value::vec(&[1.0, 2.0]));
// Bool / Choice。
assert_eq!(to_node_value(&ParamValue::Bool(true)).unwrap(), Value::bool_(true));
assert_eq!(to_node_value(&ParamValue::Choice(3)).unwrap(), Value::combo(3));
// Color:RGB 补 alpha=1(C++ oracle),RGBA 全透传。
let v = to_node_value(&ParamValue::Color([0.1, 0.2, 0.3, 0.0], 3)).unwrap();
assert_eq!(v, Value::color(0.1, 0.2, 0.3, 1.0));
let v = to_node_value(&ParamValue::Color([0.1, 0.2, 0.3, 0.9], 4)).unwrap();
assert_eq!(v, Value::color(0.1, 0.2, 0.3, 0.9));
// 无值类 / Bytes → None(无节点对应)。
assert_eq!(to_node_value(&ParamValue::PushButton), None);
assert_eq!(to_node_value(&ParamValue::Container), None);
assert_eq!(to_node_value(&ParamValue::Bytes(vec![1])), None);
// 字符串族走专用桥(POD 路径 None)。
assert_eq!(to_node_value(&ParamValue::String(cs(""))), None);
let _ = c(T::NONE); // 哨兵:类型常量可达
}
/// type_default 的无值类/字节族基线(paramDefine 拒绝前的兜底)。
#[test]
fn type_default_edge_cases() {
assert!(matches!(
type_default(TYPE_BYTES),
ParamValue::Bytes(_)
));
assert!(matches!(type_default(TYPE_CUSTOM), ParamValue::Bytes(_)));
assert!(matches!(type_default(TYPE_PUSHBUTTON), ParamValue::PushButton));
assert!(matches!(type_default(TYPE_GROUP), ParamValue::Container));
assert!(matches!(type_default(TYPE_PAGE), ParamValue::Container));
// 未知类型占位(paramDefine 已拒绝,此处兜底)。
assert!(matches!(type_default("OfxParamTypeBogus"), ParamValue::Container));
// 数值族的默认 0。
assert!(matches!(type_default(TYPE_INTEGER), ParamValue::Int([0, 0, 0], 1)));
assert!(matches!(
type_default(TYPE_DOUBLE2D),
ParamValue::Double([0.0, 0.0, 0.0], 2)
));
assert!(matches!(
type_default(TYPE_RGBA),
ParamValue::Color([0.0, 0.0, 0.0, 0.0], 4)
));
}
/// set_from_node 的维度截断/补零与字符串静默路径。
#[test]
fn set_from_node_dimension_rules() {
// Integer2D 缺第三维补零;浮点截断。
let p = param(TYPE_INTEGER2D);
p.set_from_node(&crate::bridge::node::Value::vec(&[1.9, 2.9]));
assert_eq!(p.get(), ParamValue::Int([1, 2, 0], 2));
// StrChoice 参数遇 STRING 类型:POD 无数据 → 不改值。
let p = param(TYPE_STRCHOICE);
p.set_from_node(&crate::bridge::node::Value::string());
assert!(matches!(p.get(), ParamValue::StrChoice(_)));
// Bytes/Custom 参数:任意节点值都不改(无映射)。
let p = param(TYPE_CUSTOM);
p.set_from_node(&crate::bridge::node::Value::float(3.0));
assert!(matches!(p.get(), ParamValue::Bytes(_)));
}
}
+86
View File
@@ -0,0 +1,86 @@
// 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/>.
//! 进度上报(Progress suite 的宿主侧)。
//!
//! 对应 C++ 的 `PluginProgressReporter`。进度/取消经 facade 注册的
//! 回调出 crate(M9 的 facade 回调模式,不设全局状态)。
//! 取消是粘滞的:一旦回调答 false,本报告器的 [`is_cancelled`]
//! (image effect suite 的 abort 查询)持续为真。
use std::ffi::c_int;
use std::sync::atomic::{AtomicBool, Ordering};
/// 进度回调:签名与 `include/plugin/instance.h` 的
/// `oakplugin_progress_fn` 逐字一致——`(progress, userdata)`,
/// 返回非 0 表示应中止(骨架声明为 `(userdata, progress) -> bool`,
/// 与头文件不符,以此为准)。
pub type ProgressFn = unsafe extern "C" fn(progress: f64, userdata: *mut std::ffi::c_void) -> c_int;
/// 进度上报器。render 路径持有一份,Progress suite 的
/// progressStart/Update/End 转发到这里。
pub struct ProgressReporter {
callback: Option<ProgressFn>,
userdata: usize,
/// 取消标志(粘滞;update 返回 false 时置位)。
cancelled: AtomicBool,
}
impl ProgressReporter {
/// 无回调(渲染静默进行)。
pub fn silent() -> Self {
Self {
callback: None,
userdata: 0,
cancelled: AtomicBool::new(false),
}
}
/// 带回调。
///
/// # Safety
/// `userdata` 的生命周期由注册方保证。
pub unsafe fn new(callback: ProgressFn, userdata: *mut std::ffi::c_void) -> Self {
Self {
callback: Some(callback),
// usize 存(裸指针破坏 Send 推导;值语义不变)。
userdata: userdata as usize,
cancelled: AtomicBool::new(false),
}
}
/// 报告进度;返回 false 表示应取消(映射
/// kOfxStatReplyNo/action 失败由调用点决定)。
pub fn update(&self, progress: f64) -> bool {
match self.callback {
Some(cb) => {
// 头文件契约:非 0 = 中止。
let abort = unsafe { cb(progress, self.userdata as *mut std::ffi::c_void) } != 0;
if abort {
self.cancelled.store(true, Ordering::Relaxed);
}
!abort
}
None => true,
}
}
/// 本次渲染是否已被取消(image effect suite 的 abort 透传)。
pub(crate) fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Relaxed)
}
}
+172
View File
@@ -0,0 +1,172 @@
// 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/>.
//! OFX 属性集(Property Suite 的宿主侧存储)。
//!
//! 每个 OFX 对象(host/plugin/descriptor/instance/clip/image/param)
//! 都挂一个 [`PropertySet`]。属性是多维数组,按类型存取。
//! 参照:HS: ofxhProperty.cpp(GetSuite 的读写语义:越界返回
//! kOfxStatErrBadIndex,类型不符返回 kOfxStatErrBadHandle)。
use std::sync::Mutex;
use crate::error::{Error, Result};
/// 属性值(单元素)。字符串以 OFX 的 `char*` 语义持有(host 拥有,
/// 插件借用)。
#[derive(Clone, Debug)]
pub enum Value {
/// 32 位整型。
Int(i32),
/// 双精度。
Double(f64),
/// UTF-8 字符串(OFX 侧为 NUL 结尾 char*)。
String(std::ffi::CString),
/// 不透明指针(如 OfxImageEffectHandle 互指)。
Pointer(*mut std::ffi::c_void),
}
// 裸指针默认禁 Send/Sync;但属性集语义把指针当作**不透明令牌**:
// 跨线程只搬运/比较值,解引用永远是 suite 层的 unsafe 责任(OFX
// multithread suite 本就要求插件线程可读写宿主图像缓冲——指针的
// 跨线程传递是规范语义,而非逃逸)。Mutex 包裹后属性集整体可共享。
// 与 Rust 安全模型不冲突:安全代码只能拿到 `&Value`,无法解引用指针。
unsafe impl Send for Value {}
unsafe impl Sync for Value {}
/// 一个属性:名字 + 多维值数组。
#[derive(Clone, Debug)]
pub struct Property {
/// OFX 属性名(kOfxProp*;拥有型——协商期的动态名
/// "OfxImageClipPropComponents_<clip>" 需要,`&'static str`
/// 无法表达)。
pub name: String,
/// 元素数组;维度 = len。
pub values: Vec<Value>,
}
/// 属性集。线程安全(内部 Mutex);OFX 对象的 `*Handle` 即指向它的
/// 包装。
pub struct PropertySet {
props: Mutex<Vec<Property>>,
}
/// 深拷贝(持锁克隆内部数组;createInstance 时描述符属性 → 实例
/// 属性需要)。
impl Clone for PropertySet {
fn clone(&self) -> Self {
let props = lock(&self.props);
Self {
props: Mutex::new(props.clone()),
}
}
}
impl PropertySet {
/// 空集。
pub fn new() -> Self {
Self {
props: Mutex::new(Vec::new()),
}
}
/// 定义(或整体替换)一个属性。已存在同名属性时替换其值数组。
///
/// 对应 C++ `Set::addProperty` 的替换语义(HS: ofxhPropertySuite.cpp:462)
/// 与 `PropertyTemplate::setValueN` 的整体写数组语义;属性不存在时
/// 新建(维度和值都取自 `values`)。
pub fn define(&self, name: &str, values: Vec<Value>) {
let mut props = lock(&self.props);
if let Some(p) = props.iter_mut().find(|p| p.name == name) {
p.values = values;
} else {
props.push(Property {
name: name.to_string(),
values,
});
}
}
/// 单元素便捷定义。
pub fn set_one(&self, name: &str, value: Value) {
self.define(name, vec![value]);
}
/// 读取第 `index` 个元素;属性不存在或越界返回 `None`。
///
/// 越界对应 C++ 的 kOfxStatErrBadIndex
/// (HS: ofxhPropertySuite.cpp:257 `getValueRaw`),此处以 Option 表达。
pub fn get(&self, name: &str, index: usize) -> Option<Value> {
let props = lock(&self.props);
props
.iter()
.find(|p| p.name == name)
.and_then(|p| p.values.get(index).cloned())
}
/// 覆盖第 `index` 个元素的值(不改变维度);失败返回
/// [`crate::error::Error::NotFound`]。
///
/// 维度固定是刻意为之:与 C++ `setValue` 的自动扩容不同,这里
/// 扩容只能经 [`PropertySet::define`] 显式进行(维度语义由定义方
/// 掌控,避免插件意外撑大数组)。
pub fn set_at(&self, name: &str, index: usize, value: Value) -> Result<()> {
let mut props = lock(&self.props);
let p = props
.iter_mut()
.find(|p| p.name == name)
.ok_or(Error::NotFound)?;
let slot = p.values.get_mut(index).ok_or(Error::NotFound)?;
*slot = value;
Ok(())
}
/// 维度(属性不存在为 0)。
pub fn dimension(&self, name: &str) -> usize {
lock(&self.props)
.iter()
.find(|p| p.name == name)
.map_or(0, |p| p.values.len())
}
/// 删除属性;不存在为 no-op。
pub fn remove(&self, name: &str) {
lock(&self.props).retain(|p| p.name != name);
}
/// 遍历快照(dump/快照测试用)。
pub fn snapshot(&self) -> Vec<Property> {
lock(&self.props).clone()
}
/// suite 层专用:持锁访问原始存储。
///
/// 属性 suite 需要"先类型检查再按索引读写、返回内驻字符串指针"
/// 等跨多次读写的一致性语义(逐条对照 HS: ofxhProperty.cpp),
/// 公开 API 无法在不重复加锁的前提下表达;此入口把 `Vec<Property>`
/// 在单一临界区内交给 suite 实现。仅本 crate 可见。
pub(crate) fn with_locked<R>(&self, f: impl FnOnce(&mut Vec<Property>) -> R) -> R {
let mut props = lock(&self.props);
f(&mut props)
}
}
/// 取锁。毒锁(本 crate 代码在持锁时 panic)时接管其内部状态继续
/// 使用——属性集的数据本身总是完好的,宁可继续也不让一次 panic
/// 级联成后续所有 FFI 调用失败。
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(|e| e.into_inner())
}
+435
View File
@@ -0,0 +1,435 @@
// 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 驱动:`src/render/src/plugin/pluginrenderer.cpp`
//! `PluginRenderer::render_plugin`(1857 行 C++ 的一部分)的渲染流程
//! 语义收编(M11 §4)。
//!
//! 目标:oakrender 的 PluginJob 退化为一次 C ABI 调用(
//! [`crate::ffi::oakplugin_instance_render_job`]),本模块承载全部
//! OFX 宿主渲染流程。逐段对照的 C++ 行号已注释。
//!
//! ## 流程(render_frame,对应 render_plugin)
//!
//! 1. 实例锁(pluginrenderer.cpp:1436-1444)+ 取消检查;
//! 2. use_opengl 决策(pluginrenderer.cpp:1446-1457):插件描述符
//! kOfxImageEffectPropOpenGLRenderSupported ∈ {true, needed} 且
//! 渲染器是 OpenGL 且目标纹理有 GL id;
//! 3. 输入纹理收集(pluginrenderer.cpp:1518-1552):effect_input_id
//! 匹配 → job.src;否则按 clip 名的纹理表;SimpleSource 回退;
//! 4. getClipPreferences(pluginrenderer.cpp:1554-1594);
//! 5. RoI/RoD 设定(pluginrenderer.cpp:1596-1607):region_of_interest
//! = 目标尺寸(规范坐标);输出 clip 的 RoD 与输出纹理挂接;
//! 6. 输入 clip 的 RoD 与格式(pluginrenderer.cpp:1627-1665;Phase 2
//! 全链路 F32 → 无转换);
//! 7. getRegionOfInterest(pluginrenderer.cpp:1666-1680):RoI 为
//! 建议值,任何失败按默认整帧继续(C++ 对 BadHandle 即如此;
//! Phase 2 其余状态同样继续——RoI 仅影响上游渲染范围,本管线
//! 输入由 oakrender 整帧提供);
//! 8. 输出 clip 格式(pluginrenderer.cpp:1686-1697);
//! 9. **isIdentity 短路**(ofxRendering "Identity Effects"):
//! isIdentity 命中 → 直接把所引输入 clip 在透传时间的帧拷入
//! 输出(不调 render action);
//! 10. 参数覆盖(pluginrenderer.cpp:132-290 apply_param_overrides);
//! 11. render action:CPU 路径经 [`crate::instance::Instance::render`]
//! (输出装配:图像 → 目标纹理帧,行跨度感知);GL 路径经
//! [`crate::instance::Instance::render_gl`](插件直接画进已附着
//! 的输出纹理,无 CPU 回读)。
//!
//! ## begin/end 序列括号
//!
//! ofxRendering 文档:"All calls to the render action are bracketed by
//! a pair of begin/end sequence render actions"。oakrender 对同一实例
//! 的一批帧先 [`begin_sequence`] 后 [`end_sequence`],中间逐帧
//! [`render_frame`]。
use crate::bridge::render::{self, FrameHandle, RendererHandle, TextureHandle};
use crate::image::Image;
use crate::instance::{Instance, OfxRangeD, OfxRectD, RenderScale};
use crate::property::Value;
/// 一帧渲染任务的输入(oakrender PluginJob 的 C ABI 载体;
/// [`crate::ffi::OakPluginJob`] 的 Rust 侧视图)。
pub struct RenderJob {
/// 帧时间(秒)。
pub time: f64,
/// 目标纹理(输出;oakrender 侧创建并经句柄传入)。
pub dst: TextureHandle,
/// 主输入纹理(effect_input_id / SimpleSource;可空)。
pub src: TextureHandle,
/// effect 输入 clip 名(job.src 的落点;C++ `node->get_effect_input_id()`)。
pub effect_input_id: Option<String>,
/// 其余输入 clip 的纹理表(clip 名 → 纹理)。
pub inputs: Vec<(String, TextureHandle)>,
/// 参数覆盖(参数名 → oaknode_value POD;对应 NodeValueRow)。
pub values: Vec<(String, crate::ffi::OakNodeValue)>,
/// GL 渲染器(None → CPU 路径;Some → 视 use_opengl 决策)。
pub renderer: Option<RendererHandle>,
/// 渲染前是否清空目标(信息性——C++ render_plugin 亦不处理,
/// 由上层渲染器负责;见插件渲染器注释)。
pub clear_destination: bool,
/// 交互式渲染标记(信息性;Phase 2 的 render in args 恒 0,见
/// [`crate::instance::Instance::render`])。
pub interactive: bool,
}
impl Default for RenderJob {
fn default() -> Self {
Self {
time: 0.0,
dst: TextureHandle::null(),
src: TextureHandle::null(),
effect_input_id: None,
inputs: Vec::new(),
values: Vec::new(),
renderer: None,
clear_destination: false,
interactive: false,
}
}
}
/// beginSequenceRender 括号(ofxRendering:"bracketed by a pair of
/// begin/end sequence render actions";ofxGPURender.h 的 GL 模式下该
/// action 的 in args 也带 kOfxImageEffectPropOpenGLEnabled)。
/// `gl` 非空时按 GL 模式调用。
pub fn begin_sequence(
inst: &Instance,
range: OfxRangeD,
gl: Option<RendererHandle>,
) -> crate::error::Result<()> {
if gl.is_some() {
inst.begin_sequence_render_gl(range)
} else {
inst.begin_sequence_render(range)
}
}
/// endSequenceRender 括号(与 [`begin_sequence`] 配对)。
pub fn end_sequence(
inst: &Instance,
range: OfxRangeD,
gl: Option<RendererHandle>,
) -> crate::error::Result<()> {
if gl.is_some() {
inst.end_sequence_render_gl(range)
} else {
inst.end_sequence_render(range)
}
}
/// 渲染一帧(`render_plugin` 的 Rust 移植;逐段行号对照见模块文档)。
///
/// 返回各输入 clip 的 RoI(clip 名 → 矩形;Phase 2 供测试断言,
/// 宿主不据此裁剪输入——输入由 oakrender 整帧提供,与 C++ 渲染器
/// 行为一致)。
pub fn render_frame(
inst: &Instance,
job: &RenderJob,
) -> crate::error::Result<Vec<(String, OfxRectD)>> {
use crate::error::Error;
// 1. 实例锁(pluginrenderer.cpp:1436-1444:OlivePluginInstance 非
// 线程安全,并发 setInputTexture/renderAction 会毁内部状态)。
let _lock = inst.render_lock.lock().unwrap_or_else(|e| e.into_inner());
// 取消检查必须在任何插件调用之前(shutdown 后入口已卸载)。
if inst.cancel.load(std::sync::atomic::Ordering::Relaxed) {
return Err(Error::Failed("已取消".into()));
}
// 2. use_opengl(pluginrenderer.cpp:1446-1457):插件声明 GL 支持
// 且渲染器是 OpenGL 且目标纹理有 GL id 且像素深度协商可行
// (管线 F32 满足插件 kOfxOpenGLPropPixelDepth 声明)。
let use_opengl = match job.renderer {
Some(r) if unsafe { render::renderer_is_open_gl(r) } == 1 => {
let plugin_gl = plugin_supports_opengl(inst);
let depth_ok =
crate::suites::gl_render::pick_gl_pixel_depth(&inst.plugin.descriptor.props).is_some();
let dst_id = unsafe { render::texture_id(job.dst) };
plugin_gl && depth_ok && dst_id != 0
}
_ => false,
};
// 目标帧与参数(F32 校验;输出装配的依据)。
let (dst_frame, dst_params, w, h) = read_dst(job.dst)?;
let mut dst_frame = dst_frame;
let par = pixel_aspect(&dst_params);
// 规范坐标的 RoI/RoD(pluginrenderer.cpp:1595-1603:x2 = 宽 × PAR)。
let region_of_interest = OfxRectD {
x1: 0.0,
y1: 0.0,
x2: w * par,
y2: h,
};
// 3. 输入纹理收集(pluginrenderer.cpp:1518-1552)。
for clip in &inst.clips {
if clip.name == "Output" {
continue;
}
let tex = pick_input(&clip.name, job);
if usable(tex) {
clip.set_input_texture(tex, job.time);
}
}
// 4. getClipPreferences(pluginrenderer.cpp:1554-1594)。
let prefs = inst.get_clip_preferences()?;
let components = match prefs.output_components.as_str() {
"OfxImageComponentRGBA" => crate::image::Components::Rgba,
"OfxImageComponentRGB" => crate::image::Components::Rgb,
"OfxImageComponentAlpha" => crate::image::Components::Alpha,
_ => return Err(Error::Failed("协商分量未知".into())),
};
if prefs.output_bit_depth != "OfxBitDepthFloat" {
return Err(Error::Failed("Phase 2 仅支持 F32 输出".into()));
}
// 5. 输出 clip:RoD + 输出纹理挂接(pluginrenderer.cpp:1603-1606)。
let output_clip = inst
.clips
.iter()
.find(|c| c.name == "Output")
.ok_or_else(|| Error::Failed("实例无 Output clip".into()))?;
output_clip.set_region_of_definition(region_of_interest, job.time);
output_clip.set_output_texture(job.dst, job.time);
// 6. 输入 clip:RoD 与格式(pluginrenderer.cpp:1627-1665;Phase 2
// 全链路 F32 → 格式选择恒等,无转换路径)。
for clip in &inst.clips {
if clip.name == "Output" {
continue;
}
if usable(pick_input(&clip.name, job)) {
clip.set_region_of_definition(region_of_interest, job.time);
clip.set_video_params(render::PIXEL_FORMAT_F32, 4);
}
}
// 7. getRegionOfInterest(pluginrenderer.cpp:1666-1680)。RoI 为
// 建议值:失败按默认整帧继续(C++ 对 BadHandle 如此;本管线
// 输入整帧提供,RoI 只作记录)。
let rois = inst
.get_regions_of_interest(job.time, RenderScale { x: 1.0, y: 1.0 }, region_of_interest)
.unwrap_or_else(|_| {
inst.clips
.iter()
.map(|_| region_of_interest)
.collect()
});
// 8. 输出 clip 格式(pluginrenderer.cpp:1686-1697)。
output_clip.set_video_params(render::PIXEL_FORMAT_F32, components.channel_count() as i32);
// 渲染窗口(像素坐标;pluginrenderer.cpp:1699-1704)。
let render_window = OfxRectD { x1: 0.0, y1: 0.0, x2: w, y2: h };
// 9. isIdentity 短路(ofxRendering "Identity Effects"):插件声明
// 本帧等价于某输入 clip → 直接透传该 clip 在透传时间的帧。
if let Some((t, clip_name)) = inst.is_identity(job.time)? {
passthrough(inst, &clip_name, t, job.dst)?;
unsafe { render::frame_free(&mut (dst_frame)) };
return Ok(zip_rois(inst, &rois));
}
// 10. 参数覆盖(pluginrenderer.cpp:1729-1731 + 132-290)。
apply_param_overrides(inst, &job.values);
// 11. render action。
if !use_opengl {
let output = std::sync::Arc::new(Image::allocate(
crate::image::BitDepth::Float,
components,
OfxRectD { x1: 0.0, y1: 0.0, x2: w, y2: h },
));
inst.render(job.time, RenderScale { x: 1.0, y: 1.0 }, render_window, output.clone())?;
// 输出装配(pluginrenderer.cpp:1762-1834 的 CPU 路径)。
write_output_frame(job.dst, &output)?;
} else {
// GL 路径:插件直接画进已附着的输出纹理(
// pluginrenderer.cpp:1784-1834 的 GL 分支);无 CPU 回读。
inst.render_gl(job.time, RenderScale { x: 1.0, y: 1.0 }, render_window, job.renderer.unwrap(), job.dst)?;
}
unsafe { render::frame_free(&mut (dst_frame)) };
Ok(zip_rois(inst, &rois))
}
/// 把输入 clip 名与 RoI 列表配对(与 `clips` 顺序一致)。
fn zip_rois(inst: &Instance, rois: &[OfxRectD]) -> Vec<(String, OfxRectD)> {
inst.clips
.iter()
.enumerate()
.filter(|(_, c)| c.name != "Output")
.map(|(i, c)| (c.name.clone(), rois.get(i).copied().unwrap_or_default()))
.collect()
}
/// 读目标纹理的帧与参数(F32 校验)。返回 (帧句柄, 参数, 宽, 高)。
fn read_dst(dst: TextureHandle) -> crate::error::Result<(FrameHandle, render::VideoParams, f64, f64)> {
use crate::error::Error;
let mut frame = FrameHandle::null();
if unsafe { render::texture_get_frame(dst, &mut frame) } != 0 || frame.is_null() {
return Err(Error::Failed("输出纹理无 CPU 帧".into()));
}
let mut params = render::VideoParams::default();
if unsafe { render::frame_get_params(frame, &mut params) } != 0 {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Failed("输出帧无参数".into()));
}
if params.format != render::PIXEL_FORMAT_F32 {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Failed(format!(
"输出帧格式 {} 非 F32(Phase 2 约束)",
params.format
)));
}
let (w, h) = (params.width as f64, params.height as f64);
if w <= 0.0 || h <= 0.0 {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Invalid);
}
Ok((frame, params, w, h))
}
/// 目标参数的像素比(缺失 1.0)。
fn pixel_aspect(params: &render::VideoParams) -> f64 {
if params.pixel_aspect_den != 0 {
params.pixel_aspect_num as f64 / params.pixel_aspect_den as f64
} else {
1.0
}
}
/// 插件描述符是否声明 GL 渲染支持(ofxGPURender.h:397-408:
/// "true"/"needed")。
fn plugin_supports_opengl(inst: &Instance) -> bool {
inst.plugin
.descriptor
.props
.get(crate::host::PROP_GL_RENDER_SUPPORTED, 0)
.map(|v| match v {
Value::String(s) => {
let s = s.to_string_lossy();
s == "true" || s == "needed"
}
_ => false,
})
.unwrap_or(false)
}
/// 输入纹理是否可用(非空且非占位;pluginrenderer.cpp:1504-1513 的
/// is_usable_input——Phase 2 只看非 dummy,帧/Renderer 由 oakrender
/// 保证)。
fn usable(tex: TextureHandle) -> bool {
!tex.is_null() && unsafe { render::texture_is_dummy(tex) } == 0
}
/// 按 C++ pluginrenderer.cpp:1527-1543 的规则选输入纹理。
fn pick_input(clip_name: &str, job: &RenderJob) -> TextureHandle {
if job.effect_input_id.as_deref() == Some(clip_name) && !job.src.is_null() {
return job.src;
}
for (name, tex) in &job.inputs {
if name == clip_name {
return *tex;
}
}
// SimpleSource 回退(pluginrenderer.cpp:1534-1543:
// kOfxImageEffectSimpleSourceClipName 取 k_texture_input,再回退
// job.src)。
if clip_name == "Source" && !job.src.is_null() {
return job.src;
}
TextureHandle::null()
}
/// isIdentity 透传:把所引输入 clip 在 `t` 的帧拷入输出(CPU 拷贝;
/// 目标帧 F32 校验)。
fn passthrough(
inst: &Instance,
clip_name: &str,
t: f64,
dst: TextureHandle,
) -> crate::error::Result<()> {
use crate::error::Error;
let clip = inst
.clips
.iter()
.find(|c| c.name == clip_name)
.ok_or_else(|| Error::Failed(format!("isIdentity 引用未知 clip {clip_name}")))?;
let image = clip.fetch_image(t, RenderScale { x: 1.0, y: 1.0 }, None)?;
write_output_frame(dst, &image)
}
/// 参数覆盖(pluginrenderer.cpp:132-290 `apply_param_overrides` 的
/// Rust 移植):把每帧的节点值注入实例参数。字符串族(String/
/// StrChoice)经专用 C ABI(set_param_string),不在此表的 oaknode
/// POD 表达范围内 → 跳过(与 C++ 的 k_file/k_text/k_font/k_str_combo
/// 走专用桥一致)。
fn apply_param_overrides(inst: &Instance, values: &[(String, crate::ffi::OakNodeValue)]) {
for (key, v) in values {
let Some(p) = inst.params.find(key) else { continue };
let Some(pv) = crate::ffi::node_value_to_param(v, &p.def.ofx_type) else {
continue;
};
p.set_ofx(pv);
}
}
/// 把 CPU 图像写入目标纹理的帧(行优先、行跨度感知;F32 校验)。
/// Phase 2 输出装配的公共落点(CPU render 路径与 isIdentity 透传
/// 共用)。
pub(crate) fn write_output_frame(dst: TextureHandle, image: &Image) -> crate::error::Result<()> {
use crate::error::Error;
let mut frame = FrameHandle::null();
if unsafe { render::texture_get_frame(dst, &mut frame) } != 0 || frame.is_null() {
return Err(Error::Failed("输出纹理无 CPU 帧".into()));
}
let mut params = render::VideoParams::default();
if unsafe { render::frame_get_params(frame, &mut params) } != 0 {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Failed("输出帧无参数".into()));
}
if params.format != render::PIXEL_FORMAT_F32 {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Failed("输出帧格式非 F32(Phase 2 约束)".into()));
}
let (w, h) = (params.width as usize, params.height as usize);
let tight = w * image.components().channel_count() * 4;
if tight != image.row_bytes() || tight * h != image.pixels().len() {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Failed("图像尺寸与输出帧不一致".into()));
}
let dst_ptr = unsafe { render::frame_data(frame) };
if dst_ptr.is_null() {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Failed("输出帧无数据".into()));
}
let row = unsafe { render::frame_linesize_bytes(frame) } as usize;
let row = if row > 0 { row } else { tight };
let dst_bytes =
unsafe { std::slice::from_raw_parts_mut(dst_ptr as *mut u8, row * h) };
let pixels = image.pixels();
for y in 0..h {
let d = y * row;
let s = y * tight;
dst_bytes[d..d + tight].copy_from_slice(&pixels[s..s + tight]);
}
unsafe { render::frame_free(&mut frame) };
Ok(())
}
+503
View File
@@ -0,0 +1,503 @@
// 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/>.
//! OfxImageEffectOpenGLRenderSuiteV1(M11 第 2 期):clipLoadTexture /
//! clipFreeTexture / flushResources。
//!
//! 语义对照 ofxGPURender.h(vendored,OpenGL Render Suite 一节)与
//! HostSupport 插件侧实现(HS: ofxhImageEffect.cpp:2296-2367):
//!
//! - `clipLoadTexture(clip, time, format, region, *texture)`:把 clip
//! 在 `time` 的图像加载为 GL 纹理。纹理句柄是**属性集**(标签 0),
//! 含 ofxGPURender.h 规定的 12 个属性(OpenGLTextureIndex/
//! OpenGLTextureTarget/PixelDepth/Components/PreMultiplication/
//! RenderScale/PixelAspectRatio/Bounds/RegionOfDefinition/RowBytes/
//! Field/UniqueIdentifier);宿主侧存强引用([`LIVE_TEXTURES`]),
//! clipFreeTexture 摘除即释放(对应 HS 的 get/release 配对)。
//! - Output clip:`format` 忽略,宿主返回已附着的输出纹理句柄——
//! 绑定渲染目标的动作(ofxGPURender.h "the host must bind the
//! resulting texture as the current color buffer")由调用方约定:
//! GL render 驱动的 C ABI 契约要求 oakrender 在进入 render_job 前
//! 已把输出纹理附着为渲染器输出目标(等价 C++ 的
//! `PluginRenderer::attach_output_texture`)。clipFreeTexture 对
//! Output 只释放句柄、不删纹理(宿主还要读它)。
//! - 输入纹理经 [`crate::bridge::render`] 在渲染器上创建(CPU 帧 →
//! GL 上传)。纹理格式:全链路 F32 约束下,像素深度按 clip 协商
//! 结果(恒 F32);`format` 参数(kOfxImageEffectGLFormat*)若
//! 请求的分量与协商分量不符,Phase 2 不做转换 → Failed(规范要求
//! "host ensures it gives the requested format"——宁可显式失败也
//! 不静默给错格式)。ofxGPURender.h 注明"宿主无需按 Clip
//! Preferences 把图像重映射到插件请求的位深",插件以纹理句柄的
//! PixelDepth/Components 为准。
//! - `flushResources`:宿主在 render 之间不缓存 GPU 资源(纹理随
//! clipFreeTexture 立即释放)→ 无可释放 → kOfxStatReplyDefault
//! (规范:"nothing the host could do")。
//! - GL 上下文规则(ofxGPURender.h "OpenGL Current Context"):宿主
//! 只在 Render/BeginSequenceRender/EndSequenceRender/Attach/Detach
//! 期间要求上下文 current;本实现的约定是调用方(oakrender
//! PluginJob 路径)在调用前置好上下文,本 suite 经
//! [`crate::suites::gl_ctx`] TLS 取渲染器句柄。
use std::collections::HashMap;
use std::ffi::{c_char, c_double, c_int, c_void, CStr, CString};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Mutex;
use crate::clip::ClipInstance;
use crate::image::Image;
use crate::property::{PropertySet, Value};
use crate::suites::{status, tag};
// ---- GL 常量(ofxGPURender.h;GLFormat 字符串为 ofxOpenGLRender.h
// OpenFX 1.4 的规范名,vendored 头文件是 stub 未收录,按规范定义)----
/// kOfxImageEffectPropOpenGLTextureIndex(ofxGPURender.h:135)。
pub(crate) const GL_TEXTURE_INDEX: &str = "OfxImageEffectPropOpenGLTextureIndex";
/// kOfxImageEffectPropOpenGLTextureTarget(ofxGPURender.h:152)。
pub(crate) const GL_TEXTURE_TARGET: &str = "OfxImageEffectPropOpenGLTextureTarget";
/// kOfxImageEffectGLFormatRGBA(ofxOpenGLRender.h 1.4)。
pub(crate) const GL_FORMAT_RGBA: &str = "OfxImageEffectGLFormatRGBA";
/// kOfxImageEffectGLFormatRGB。
pub(crate) const GL_FORMAT_RGB: &str = "OfxImageEffectGLFormatRGB";
/// kOfxImageEffectGLFormatAlpha。
pub(crate) const GL_FORMAT_ALPHA: &str = "OfxImageEffectGLFormatAlpha";
/// kOfxImageEffectGLFormatLuminance。
pub(crate) const GL_FORMAT_LUMINANCE: &str = "OfxImageEffectGLFormatLuminance";
/// kOfxImageEffectGLFormatLuminanceAlpha。
pub(crate) const GL_FORMAT_LUMINANCE_ALPHA: &str = "OfxImageEffectGLFormatLuminanceAlpha";
/// GL_TEXTURE_2D 的 GLenum 值(0x0DE1;纹理句柄的 OpenGLTextureTarget
/// 属性。GL 规范值,非 OFX 宏)。
pub(crate) const GL_TEXTURE_2D: i32 = 0x0DE1;
/// kOfxImageEffectPropPreMultiplication 的非预乘默认值。
pub(crate) const PREMULT_NONE: &str = "OfxImagePreMultipliedNone";
/// GL 像素深度协商(ofxGPURender.h:65-89 kOfxOpenGLPropPixelDepth):
/// 插件描述符声明的 GL 渲染支持位深列表(可选)。
///
/// 返回 Some(实际位深) 表示 GL 模式可行,None 表示管线无法满足
/// 插件声明 → 宿主应回退 CPU 渲染(ofxGPURender.h "the host will
/// try to provide buffers/textures in one of the supported formats";
/// Phase 2 全链路 F32,无法提供其他位深):
/// - 列表缺失/为空 → Some(Float)(宿主自选,规范默认);
/// - 列表含 Float → Some(Float);
/// - 列表存在且不含 Float → None(管线约束;GL 模式不可行)。
pub(crate) fn pick_gl_pixel_depth(plugin_props: &crate::property::PropertySet) -> Option<&'static str> {
use crate::property::Value;
let dim = plugin_props.dimension(crate::host::PROP_GL_PIXEL_DEPTH);
if dim == 0 {
return Some("OfxBitDepthFloat");
}
for i in 0..dim {
if let Some(Value::String(s)) = plugin_props.get(crate::host::PROP_GL_PIXEL_DEPTH, i) {
if s.to_string_lossy() == "OfxBitDepthFloat" {
return Some("OfxBitDepthFloat");
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::property::{PropertySet, Value};
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
/// GL 像素深度协商矩阵(ofxGPURender.h kOfxOpenGLPropPixelDepth):
/// 未声明/含 Float → 管线 F32 可行;声明且不含 Float → None
/// (GL 模式不可行,回退 CPU)。
#[test]
fn pick_gl_pixel_depth_matrix() {
let props = PropertySet::new();
assert_eq!(pick_gl_pixel_depth(&props), Some("OfxBitDepthFloat"));
let props2 = PropertySet::new();
props2.define(
crate::host::PROP_GL_PIXEL_DEPTH,
vec![
Value::String(cs("OfxBitDepthHalf")),
Value::String(cs("OfxBitDepthByte")),
],
);
assert_eq!(pick_gl_pixel_depth(&props2), None);
let props3 = PropertySet::new();
props3.define(
crate::host::PROP_GL_PIXEL_DEPTH,
vec![
Value::String(cs("OfxBitDepthByte")),
Value::String(cs("OfxBitDepthFloat")),
],
);
assert_eq!(pick_gl_pixel_depth(&props3), Some("OfxBitDepthFloat"));
let props4 = PropertySet::new();
props4.define(
crate::host::PROP_GL_PIXEL_DEPTH,
vec![Value::String(cs("OfxBitDepthFloat"))],
);
assert_eq!(pick_gl_pixel_depth(&props4), Some("OfxBitDepthFloat"));
}
}
// ---- 存活纹理表 -----------------------------------------------------------
/// 存活 GL 纹理表:clipLoadTexture 产出(props 地址 → 属性集 +
/// 纹理强引用 + 是否输出 clip);clipFreeTexture 摘除即释放——对应
/// HS 的 get/release 配对(HS: ofxhImageEffect.cpp:2336-2351)。
///
/// 属性集必须**装箱**(Box 稳定堆地址):纹理句柄指向它,函数返回后
/// 必须仍存活;栈上临时变量会悬垂(phase-2 实现初版的 bug)。
static LIVE_TEXTURES: std::sync::LazyLock<
Mutex<HashMap<usize, (Box<PropertySet>, crate::bridge::render::TextureHandle, bool)>>,
> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
/// 登记纹理(clipLoadTexture 内部;`props` 装箱后取地址为句柄基址)。
fn register(props: Box<PropertySet>, texture: crate::bridge::render::TextureHandle, is_output: bool) -> usize {
let addr = &*props as *const PropertySet as usize;
LIVE_TEXTURES
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(addr, (props, texture, is_output));
addr
}
/// 释放全部残留输入纹理(GL render action 返回后的安全网:规范要求
/// 插件在 action 返回前 clipFreeTexture 全部句柄;遗漏的输入纹理在
/// 此释放,输出纹理保留——宿主还要读它)。
pub(crate) fn purge_leftovers() {
let leftovers: Vec<(crate::bridge::render::TextureHandle, bool)> = {
let mut live = LIVE_TEXTURES.lock().unwrap_or_else(|e| e.into_inner());
live.drain().map(|(_k, (_p, t, o))| (t, o)).collect()
};
for (texture, is_output) in leftovers {
if !is_output && !texture.is_null() {
let mut t = texture;
unsafe { crate::bridge::render::texture_free(&mut t) };
}
}
}
/// 公共入口模板:panic 兜底。
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
catch_unwind(AssertUnwindSafe(f))
.map_or_else(|_| status::FAILED, |r| r.map_or_else(|c| c, |()| status::OK))
}
/// clip 句柄解析(实例期)。
fn resolve_clip(handle: *mut c_void) -> Result<&'static ClipInstance, c_int> {
if handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe {
match tag::kind(handle) {
tag::CLIP => Ok(&*(tag::strip(handle) as *const ClipInstance)),
_ => Err(status::ERR_BAD_HANDLE),
}
}
}
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
/// 把矩形写成 Int×4 属性(Bounds/ROD 用;OFX 图像属性是像素坐标)。
fn rect_props(props: &PropertySet, name: &str, rect: crate::instance::OfxRectD) {
let b = |v: f64| Value::Int(v.round() as i32);
props.define(
name,
vec![b(rect.x1), b(rect.y1), b(rect.x2), b(rect.y2)],
);
}
/// 从 clip 属性读字符串(缺失返回默认)。
fn clip_string(clip: &ClipInstance, name: &str, default: &str) -> String {
clip.props
.get(name, 0)
.map(|v| match v {
Value::String(s) => s.to_string_lossy().into_owned(),
_ => default.to_string(),
})
.unwrap_or_else(|| default.to_string())
}
/// 构造纹理属性集(ofxGPURender.h 规定的属性;输入与输出共用,
/// 只是数据来源不同)。
#[allow(clippy::too_many_arguments)]
fn make_texture_props(
texture: crate::bridge::render::TextureHandle,
width: f64,
height: f64,
components: crate::image::Components,
depth: &str,
premult: &str,
par: f64,
scale: crate::instance::RenderScale,
row_bytes: i32,
) -> PropertySet {
let props = PropertySet::new();
props.set_one(
GL_TEXTURE_INDEX,
Value::Int(unsafe { crate::bridge::render::texture_id(texture) }),
);
props.set_one(GL_TEXTURE_TARGET, Value::Int(GL_TEXTURE_2D));
props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
Value::String(cs(depth)),
);
props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS,
Value::String(cs(components.to_ofx())),
);
props.set_one("OfxImageEffectPropPreMultiplication", Value::String(cs(premult)));
props.define(
crate::host::PROP_RENDER_SCALE,
vec![Value::Double(scale.x), Value::Double(scale.y)],
);
props.set_one("OfxImagePropPixelAspectRatio", Value::Double(par));
let bounds = crate::instance::OfxRectD { x1: 0.0, y1: 0.0, x2: width, y2: height };
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(
crate::image::K_IMAGE_PROP_UNIQUE_ID,
Value::String(crate::image::unique_identifier()),
);
props
}
/// 纹理请求格式 → 期望分量(NULL/未知 → None = 宿主自选)。
fn format_components(format: Option<&str>) -> Option<crate::image::Components> {
match format {
Some(GL_FORMAT_RGBA) | None => Some(crate::image::Components::Rgba),
Some(GL_FORMAT_RGB) => Some(crate::image::Components::Rgb),
Some(GL_FORMAT_ALPHA) => Some(crate::image::Components::Alpha),
// Luminance 族:Phase 2 不建模(无对应 Components),宿主
// 按 RGBA 上传并如实上报——格式核对走 None 分支。
Some(GL_FORMAT_LUMINANCE) | Some(GL_FORMAT_LUMINANCE_ALPHA) => None,
Some(_) => None,
}
}
/// clipLoadTexture:把 clip 在 `time` 的图像加载为 GL 纹理。
///
/// `format` 为请求的纹理格式(kOfxImageEffectGLFormat*;NULL = 宿主
/// 按插件的 kOfxOpenGLPropPixelDepth 决定——Phase 2 全链路 F32)。
/// `region`(规范坐标,可空)会裁剪到 clip 的 RoD——Phase 2 仅支持
/// 整帧(NULL);子区域请求返回 Failed(插件按规范继续,视作黑底)。
unsafe extern "C" fn clip_load_texture(
clip: *mut c_void,
time: c_double,
format: *const c_char,
region: *const c_void,
out: *mut *mut c_void,
) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe { *out = std::ptr::null_mut() };
let c = resolve_clip(clip)?;
// GL 上下文(TLS):仅 GL 渲染期存在(ofxGPURender.h 的
// "OpenGL Current Context" 规则)。
let gl = crate::suites::gl_ctx().ok_or(status::ERR_MISSING_HOST_FEATURE)?;
let scale = crate::suites::render_ctx()
.map(|ctx| ctx.scale)
.unwrap_or(crate::instance::RenderScale { x: 1.0, y: 1.0 });
if c.name == "Output" {
// Output:返回已附着的输出纹理(format 忽略;渲染目标
// 绑定由调用方契约保证——等价 C++ attach_output_texture)。
let tex = gl.output_texture;
if tex.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let (w, h) = texture_size(&tex);
if w <= 0.0 || h <= 0.0 {
return Err(status::ERR_BAD_HANDLE);
}
let props = make_texture_props(
tex,
w,
h,
crate::image::Components::Rgba,
gl.gl_pixel_depth,
&clip_string(c, "OfxImageEffectPropPreMultiplication", PREMULT_NONE),
clip_par(c),
scale,
(w as i32) * 4 * 4,
);
let addr = register(Box::new(props), tex, true);
unsafe { *out = tag::make(addr as *const PropertySet, tag::PROPERTY_SET) };
return Ok(());
}
// 输入 clip:Phase 2 仅支持整帧(fetch_image 的 phase-1 约束)。
if !region.is_null() {
return Err(status::FAILED);
}
let format = if format.is_null() {
None
} else {
unsafe { CStr::from_ptr(format) }.to_str().ok()
};
let image = c
.fetch_image(time, scale, None)
.map_err(|_| status::FAILED)?;
let components = image.components();
// 明确请求了不同分量 → Phase 2 不转换 → Failed(规范要求
// 满足请求格式;静默给错格式比显式失败更糟)。
if let Some(expected) = format_components(format) {
if expected != components {
return Err(status::FAILED);
}
}
let premult = clip_string(c, "OfxImageEffectPropPreMultiplication", PREMULT_NONE);
let (w, h) = (image_width(&image), image_height(&image));
if w <= 0.0 || h <= 0.0 {
return Err(status::FAILED);
}
let params = crate::bridge::render::VideoParams {
width: w as i32,
height: h as i32,
format: crate::bridge::render::PIXEL_FORMAT_F32,
..Default::default()
};
let tex = unsafe {
crate::bridge::render::texture_create(
gl.renderer,
&params,
image.pixels().as_ptr() as *const c_void,
image.row_bytes() as i32,
)
};
if tex.is_null() {
return Err(status::ERR_MEMORY);
}
let props = make_texture_props(
tex,
w,
h,
components,
gl.gl_pixel_depth,
&premult,
clip_par(c),
scale,
image.row_bytes() as i32,
);
let addr = register(Box::new(props), tex, false);
unsafe { *out = tag::make(addr as *const PropertySet, tag::PROPERTY_SET) };
Ok(())
})
}
/// 纹理尺寸(经 texture_get_params;失败回退 0,0)。
fn texture_size(tex: &crate::bridge::render::TextureHandle) -> (f64, f64) {
let mut p = crate::bridge::render::VideoParams::default();
if unsafe { crate::bridge::render::texture_get_params(*tex, &mut p) } == 0 && p.width > 0 {
(p.width as f64, p.height as f64)
} else {
(0.0, 0.0)
}
}
fn image_width(image: &Image) -> f64 {
(image.bounds().x2 - image.bounds().x1).round()
}
fn image_height(image: &Image) -> f64 {
(image.bounds().y2 - image.bounds().y1).round()
}
/// clip 的协商像素比。
fn clip_par(c: &ClipInstance) -> f64 {
c.props
.get("OfxImagePropPixelAspectRatio", 0)
.and_then(|v| match v {
Value::Double(d) => Some(d),
_ => None,
})
.unwrap_or(1.0)
}
/// clipFreeTexture:释放纹理句柄(输入 clip 删除 GL 纹理;Output 只
/// 释放句柄不删纹理——宿主还要读它)。
unsafe extern "C" fn clip_free_texture(texture_handle: *mut c_void) -> c_int {
caught(|| {
if texture_handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let addr = tag::strip(texture_handle) as usize;
let entry = {
let mut live = LIVE_TEXTURES.lock().unwrap_or_else(|e| e.into_inner());
live.remove(&addr)
};
match entry {
Some((_props, texture, is_output)) => {
if !is_output && !texture.is_null() {
let mut t = texture;
unsafe { crate::bridge::render::texture_free(&mut t) };
}
Ok(())
}
None => Err(status::ERR_BAD_HANDLE),
}
})
}
/// flushResources:宿主不缓存 GPU 资源 → REPLY_DEFAULT(规范语义
/// "nothing the host could do")。
unsafe extern "C" fn flush_resources() -> c_int {
status::REPLY_DEFAULT
}
/// 函数表布局(与 SDK `OfxImageEffectOpenGLRenderSuiteV1` 逐字段
/// 一致;ofxGPURender.h:181-310)。
#[repr(C)]
pub struct GlRenderSuiteV1 {
/// clipLoadTexture。
pub clip_load_texture: unsafe extern "C" fn(
*mut c_void,
c_double,
*const c_char,
*const c_void,
*mut *mut c_void,
) -> c_int,
/// clipFreeTexture。
pub clip_free_texture: unsafe extern "C" fn(*mut c_void) -> c_int,
/// flushResources。
pub flush_resources: unsafe extern "C" fn() -> c_int,
}
/// 函数表实例。
pub fn suite_v1() -> &'static GlRenderSuiteV1 {
static SUITE: std::sync::OnceLock<GlRenderSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| GlRenderSuiteV1 {
clip_load_texture: clip_load_texture,
clip_free_texture: clip_free_texture,
flush_resources: flush_resources,
})
}
+580
View File
@@ -0,0 +1,580 @@
// 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/>.
//! OfxImageEffectSuite v1:clip/image 操作。
//!
//! 语义对照 HS: ofxhImageEffect.cpp:
//! - clipGetImage 返回图像 **属性集** handle(HS:2038 `getPropHandle`),
//! 图像存活由宿主表托管(get → 插入强引用,clipReleaseImage → 摘除
//! 释放;对应 HS 的引用计数配对,HS:2044);
//! - clipGetRegionOfDefinition:读 clip 实例属性里的
//! kOfxImageEffectPropRegionOfDefinition(协商时宿主写入);非法
//! RoD → kOfxStatFailed(HS:2143-2147);
//! - abort:实例期 → 当前渲染的进度取消状态(HS:2154-2170;
//! HS 默认返回 0,本实现按规范返回 REPLY_YES/REPLY_NO);
//! - imageMemory*:账本同 memory suite(HS 的 lock 是"锁住防重分配"
//! 语义,第 1 期账本不需要 → OK no-op,见 memory.rs 文档)。
//!
//! `// TODO(clip)`:clipGetImage 依赖 [`crate::clip::ClipInstance::fetch_image`]
//! (bridge::render 帧访问 C ABI 未冻结),代码已齐、运行时待 clip.rs。
use std::collections::HashMap;
use std::ffi::{c_char, c_double, c_int, c_void, CStr};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Mutex;
use crate::clip::ClipInstance;
use crate::descriptor::{ClipDescriptor, EffectDescriptor};
use crate::image::Image;
use crate::instance::{Instance, OfxRectD, RenderScale};
use crate::property::PropertySet;
use crate::suites::{status, tag};
/// 函数表布局(与 SDK `OfxImageEffectSuiteV1` 逐字段一致;常用子集
/// 注释,完整字段以 SDK 为准)。
#[repr(C)]
pub struct ImageEffectSuiteV1 {
/// getPropertySet:取 effect/clip/image 的属性集。
pub get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
/// getParamSet:取实例参数集。
pub get_param_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
/// clipDefine(describe 期间):定义 clip。
pub clip_define: unsafe extern "C" fn(*mut c_void, *const c_char, *mut *mut c_void) -> c_int,
/// clipGetHandle:按名取 clip。
pub clip_get_handle: unsafe extern "C" fn(*mut c_void, *const c_char, *mut *mut c_void, *mut *mut c_void) -> c_int,
/// clipGetPropertySet
pub clip_get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
/// clipGetImage:取图像(host 侧增加引用,必须与
/// clipReleaseImage 配对)。
pub clip_get_image: unsafe extern "C" fn(*mut c_void, c_double, *const c_void, *mut *mut c_void) -> c_int,
/// clipReleaseImage
pub clip_release_image: unsafe extern "C" fn(*mut c_void) -> c_int,
/// clipGetRegionOfDefinition
pub clip_get_region_of_definition: unsafe extern "C" fn(*mut c_void, c_double, *mut c_void) -> c_int,
/// abort:查询是否应中止(进度取消透传)。
pub abort: unsafe extern "C" fn(*mut c_void) -> c_int,
/// imageMemoryAlloc / imageMemoryFree / imageMemoryLock /
/// imageMemoryUnlock:图像内存管理(账本同 memory suite)。
pub image_memory_alloc: unsafe extern "C" fn(*mut c_void, c_int, *mut *mut c_void) -> c_int,
/// imageMemoryFree
pub image_memory_free: unsafe extern "C" fn(*mut c_void) -> c_int,
/// imageMemoryLock
pub image_memory_lock: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
/// imageMemoryUnlock
pub image_memory_unlock: unsafe extern "C" fn(*mut c_void) -> c_int,
}
/// 存活图像表:clipGetImage 产出(props 地址 → 强引用;唯一持有者,
/// clipReleaseImage 摘除即释放——对应 HS 的 get/release 配对)。
static LIVE_IMAGES: std::sync::LazyLock<Mutex<HashMap<usize, std::sync::Arc<Image>>>> =
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
/// 公共入口模板:panic 兜底。
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
catch_unwind(AssertUnwindSafe(f))
.map_or_else(|_| status::FAILED, |r| r.map_or_else(|c| c, |()| status::OK))
}
/// 属性名(空指针/非 UTF-8 → ErrValue)。
unsafe fn c_name<'a>(name: *const c_char) -> Result<&'a str, c_int> {
if name.is_null() {
return Err(status::ERR_VALUE);
}
unsafe { CStr::from_ptr(name) }
.to_str()
.map_err(|_| status::ERR_VALUE)
}
/// effect 句柄解析(describe 期 → 描述符;实例期 → 实例)。
enum EffectRef<'a> {
Descriptor(&'a mut EffectDescriptor),
Instance(&'a Instance),
}
fn resolve_effect(handle: *mut c_void) -> Result<EffectRef<'static>, c_int> {
if handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
// 两者 props 均在偏移 0(句柄约定)。
unsafe {
match tag::kind(handle) {
tag::DESCRIPTOR => Ok(EffectRef::Descriptor(&mut *(tag::strip(handle) as *mut EffectDescriptor))),
tag::INSTANCE => Ok(EffectRef::Instance(&*(tag::strip(handle) as *const Instance))),
_ => Err(status::ERR_BAD_HANDLE),
}
}
}
/// clip 句柄解析(实例期;describe 期 ClipDescriptor 只在属性
/// suite 里用,不走这里)。
fn resolve_clip(handle: *mut c_void) -> Result<&'static ClipInstance, c_int> {
if handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe {
match tag::kind(handle) {
tag::CLIP => Ok(&*(tag::strip(handle) as *const ClipInstance)),
_ => Err(status::ERR_BAD_HANDLE),
}
}
}
/// getPropertySet:effect 属性集 = handle 本体(props 在偏移 0;
/// HS:1886-1898 返回 `getProps().getHandle()`,同址)。
unsafe extern "C" fn get_property_set(effect: *mut c_void, out: *mut *mut c_void) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let _ = resolve_effect(effect)?;
unsafe { *out = effect };
Ok(())
})
}
/// getParamSet:param-set 即 effect 本体(HS:1901-1931 描述符/实例
/// 各自返回其 param set;本设计两者合一)。
unsafe extern "C" fn get_param_set(effect: *mut c_void, out: *mut *mut c_void) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let _ = resolve_effect(effect)?;
unsafe { *out = effect };
Ok(())
})
}
/// clipDefine(describe 期):定义 clip 并返回其属性集 handle。
/// 重复名 → 整体替换(HS: `defineClip` 的 map 覆盖语义,
/// ofxhImageEffect.cpp:265-271;旧句柄随之失效,与 HS 一致)。
unsafe extern "C" fn clip_define(effect: *mut c_void, name: *const c_char, out: *mut *mut c_void) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe { *out = std::ptr::null_mut() };
let n = unsafe { c_name(name)? };
let desc = match resolve_effect(effect)? {
EffectRef::Descriptor(d) => d,
EffectRef::Instance(_) => return Err(status::ERR_BAD_HANDLE),
};
let clip = ClipDescriptor::new(n);
// 重复定义:替换原 Box(HS map 覆盖语义)。
if let Some(existing) = desc.clips.iter_mut().find(|c| c.name == n) {
*existing = Box::new(clip);
} else {
desc.clips.push(Box::new(clip));
}
// 地址取自已入盒的对象(栈上临时变量在移动后失效)。
let clip = desc.clips.iter().find(|c| c.name == n).expect("just stored");
let addr = &clip.props as *const _ as usize;
unsafe { *out = tag::make(addr as *const PropertySet, tag::CLIP) };
Ok(())
})
}
/// clipGetHandle(实例期):按名取 clip 及其属性集。未找到 →
/// BadHandle(HS:2067-2070)。
unsafe extern "C" fn clip_get_handle(
effect: *mut c_void,
name: *const c_char,
clip: *mut *mut c_void,
property_set: *mut *mut c_void,
) -> c_int {
caught(|| {
if clip.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe { *clip = std::ptr::null_mut() };
let n = unsafe { c_name(name)? };
let handle = match resolve_effect(effect)? {
EffectRef::Instance(i) => {
let c = i.clips.iter().find(|c| c.name == n).ok_or(status::ERR_BAD_HANDLE)?;
let addr = &c.props as *const _ as usize;
tag::make(addr as *const PropertySet, tag::CLIP)
}
EffectRef::Descriptor(_) => return Err(status::ERR_BAD_HANDLE),
};
unsafe { *clip = handle };
if !property_set.is_null() {
// props 在偏移 0:clip handle 与 props handle 同值。
unsafe { *property_set = handle };
}
Ok(())
})
}
/// clipGetPropertySet:clip 属性集 = clip handle 本体。
unsafe extern "C" fn clip_get_property_set(clip: *mut c_void, out: *mut *mut c_void) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let _ = resolve_clip(clip)?;
unsafe { *out = clip };
Ok(())
})
}
/// clipGetImage:抓取输入图像并登记到存活表,返回图像属性集 handle
/// (HS:2003-2049;`getImage` 失败 → Failed)。
///
/// `// TODO(clip)`:fetch_image 待 bridge::render 帧访问 C ABI。
unsafe extern "C" fn clip_get_image(
clip: *mut c_void,
time: c_double,
region: *const c_void,
out: *mut *mut c_void,
) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe { *out = std::ptr::null_mut() };
let c = resolve_clip(clip)?;
// Output clip:返回当前渲染的输出图像(render 驱动经 TLS
// 设置;对应 HS 渲染期输出图像挂在 Output clip 上)。
if c.name == "Output" {
if let Some(image) = crate::suites::current_output() {
let addr = &image.props as *const _ as usize;
LIVE_IMAGES
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(addr, image);
unsafe { *out = tag::make(addr as *const PropertySet, tag::IMAGE) };
return Ok(());
}
return Err(status::FAILED);
}
// 区域(可空):像素坐标的可选 bounds。
let region = if region.is_null() {
None
} else {
Some(unsafe { *(region as *const OfxRectD) })
};
// 渲染比例取自当前渲染上下文(HS 的 clip 在 render 期从
// in_args 拿到 scale;本设计经 TLS,见 suites::RenderCtx)。
let scale = crate::suites::render_ctx()
.map(|ctx| ctx.scale)
.unwrap_or(RenderScale { x: 1.0, y: 1.0 });
let image = c.fetch_image(time, scale, region).map_err(|_| status::FAILED)?;
let image = std::sync::Arc::new(image);
let addr = &image.props as *const _ as usize;
LIVE_IMAGES
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(addr, image);
unsafe { *out = tag::make(addr as *const PropertySet, tag::IMAGE) };
Ok(())
})
}
/// clipReleaseImage:摘除存活表强引用(图像随之释放;HS:2053-2068
/// 的 releaseReference 配对)。
unsafe extern "C" fn clip_release_image(image: *mut c_void) -> c_int {
caught(|| {
if image.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
if tag::kind(image) != tag::IMAGE {
return Err(status::ERR_BAD_HANDLE);
}
let addr = tag::strip(image) as usize;
let mut live = LIVE_IMAGES.lock().unwrap_or_else(|e| e.into_inner());
match live.remove(&addr) {
Some(_) => Ok(()),
None => Err(status::ERR_BAD_HANDLE),
}
})
}
/// clipGetRegionOfDefinition:读 clip 实例属性的协商 RoD;缺失/非法
/// → Failed(HS:2111-2150;非法判断 x2<x1 || y2<y1 → Failed)。
unsafe extern "C" fn clip_get_region_of_definition(
clip: *mut c_void,
_time: c_double,
bounds: *mut c_void,
) -> c_int {
caught(|| {
if bounds.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let c = resolve_clip(clip)?;
let rod = read_rod(&c.props).ok_or(status::FAILED)?;
if rod.x2 < rod.x1 || rod.y2 < rod.y1 {
return Err(status::FAILED);
}
unsafe { *(bounds as *mut OfxRectD) = rod };
Ok(())
})
}
/// 从 clip 属性读协商 RoD(kOfxImageEffectPropRegionOfDefinition,
/// Double×4;协商时宿主写入)。
fn read_rod(props: &PropertySet) -> Option<OfxRectD> {
let v = props.get("OfxImageEffectPropRegionOfDefinition", 0)?;
let Value::Double(x1) = v else { return None };
let Value::Double(y1) = props.get("OfxImageEffectPropRegionOfDefinition", 1)? else {
return None;
};
let Value::Double(x2) = props.get("OfxImageEffectPropRegionOfDefinition", 2)? else {
return None;
};
let Value::Double(y2) = props.get("OfxImageEffectPropRegionOfDefinition", 3)? else {
return None;
};
Some(OfxRectD { x1, y1, x2, y2 })
}
/// abort:实例期 → 当前渲染进度是否已取消(HS:2154-2170 的
/// `instance->abort()`;HS 默认 0,本实现按规范返回 REPLY_YES/NO)。
/// 取消状态是成功应答而非错误码,不能走 caught 的 Err 通道。
unsafe extern "C" fn abort(effect: *mut c_void) -> c_int {
catch_unwind(AssertUnwindSafe(|| {
match resolve_effect(effect)? {
EffectRef::Instance(_) => {}
EffectRef::Descriptor(_) => return Err(status::ERR_BAD_HANDLE),
}
Ok(())
}))
.map_or_else(
|_| status::FAILED,
|r| match r {
Ok(()) => {
if crate::suites::progress::is_cancelled() {
status::REPLY_YES
} else {
status::REPLY_NO
}
}
Err(c) => c,
},
)
}
/// imageMemoryAlloc:账本同 memory suite(HS:2174-2212;失败 →
/// ErrMemory)。`handle` 忽略。
unsafe extern "C" fn image_memory_alloc(
_handle: *mut c_void,
byte_size: c_int,
out: *mut *mut c_void,
) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_VALUE);
}
match crate::suites::memory::alloc(byte_size as usize) {
Some(ptr) => {
unsafe { *out = ptr as *mut c_void };
Ok(())
}
None => Err(status::ERR_MEMORY),
}
})
}
/// imageMemoryFree:账本销账;未知指针 → BadHandle(SDK 契约)。
unsafe extern "C" fn image_memory_free(memory: *mut c_void) -> c_int {
caught(|| {
if crate::suites::memory::free(memory as *mut u8) {
Ok(())
} else {
Err(status::ERR_BAD_HANDLE)
}
})
}
/// imageMemoryLock/Unlock:HS 语义是"锁住防重分配"(ofxhMemory.cpp
/// lock/unlock 计数),第 1 期账本不建模 → OK no-op(文档见
/// memory.rs)。
unsafe extern "C" fn image_memory_lock(memory: *mut c_void, out: *mut *mut c_void) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_VALUE);
}
// 账本里的地址即数据指针(分配即就绪,无 HS 的延迟分配)。
unsafe { *out = memory };
Ok(())
})
}
unsafe extern "C" fn image_memory_unlock(_memory: *mut c_void) -> c_int {
caught(|| Ok(()))
}
use crate::property::Value;
/// 静态函数表实例。
pub fn suite_v1() -> &'static ImageEffectSuiteV1 {
static SUITE: std::sync::OnceLock<ImageEffectSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| ImageEffectSuiteV1 {
get_property_set: get_property_set,
get_param_set: get_param_set,
clip_define: clip_define,
clip_get_handle: clip_get_handle,
clip_get_property_set: clip_get_property_set,
clip_get_image: clip_get_image,
clip_release_image: clip_release_image,
clip_get_region_of_definition: clip_get_region_of_definition,
abort: abort,
image_memory_alloc: image_memory_alloc,
image_memory_free: image_memory_free,
image_memory_lock: image_memory_lock,
image_memory_unlock: image_memory_unlock,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use crate::descriptor::EffectDescriptor;
use crate::property::PropertySet;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
fn descriptor_handle(d: &EffectDescriptor) -> *mut c_void {
tag::make(&d.props as *const PropertySet, tag::DESCRIPTOR)
}
/// describe 期:clipDefine 建 clip、属性 suite 读写其属性、
/// 重复定义整体替换(HS map 覆盖)。
#[test]
fn describe_clip_define_and_props() {
let mut desc = EffectDescriptor::new();
let s = suite_v1();
let h = descriptor_handle(&desc);
let mut clip: *mut c_void = std::ptr::null_mut();
let name = cs("Source");
unsafe {
assert_eq!((s.clip_define)(h, name.as_ptr(), &mut clip), 0);
}
assert_eq!(tag::kind(clip), tag::CLIP);
// 属性 suite 直接读写 clip handle(props 在偏移 0)。
let ps = crate::suites::property::suite_v1();
let label_prop = cs(crate::param::PROP_LABEL);
let optional_prop = cs(crate::descriptor::CLIP_OPTIONAL);
let label = cs("SourceLabel");
unsafe {
assert_eq!((ps.set_string)(clip, label_prop.as_ptr(), 0, label.as_ptr()), 0);
}
let mut out: *mut c_char = std::ptr::null_mut();
unsafe {
assert_eq!((ps.get_string)(clip, label_prop.as_ptr(), 0, &mut out), 0);
assert_eq!(CStr::from_ptr(out).to_bytes(), b"SourceLabel");
// 可选性标记:插件写 Optional=1。
assert_eq!((ps.set_int)(clip, optional_prop.as_ptr(), 0, 1), 0);
}
// getPropertySet / getParamSet:返回 effect 本体 handle。
let mut props: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.get_property_set)(h, &mut props), 0);
assert_eq!(props, h);
assert_eq!((s.get_param_set)(h, &mut props), 0);
assert_eq!(props, h);
}
// 重复 clipDefine:替换(旧句柄失效,HS 一致)。
let mut clip2: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.clip_define)(h, name.as_ptr(), &mut clip2), 0);
}
assert_ne!(clip, clip2);
assert_eq!(desc.clips.len(), 1);
// 空 out / 空 handle → BadHandle。
unsafe {
assert_eq!((s.clip_define)(h, name.as_ptr(), std::ptr::null_mut()), status::ERR_BAD_HANDLE);
assert_eq!((s.clip_define)(std::ptr::null_mut(), name.as_ptr(), &mut clip2), status::ERR_BAD_HANDLE);
}
}
/// abort:describe 期 → BadHandle;实例期 → REPLY_NO(未取消)。
#[test]
fn abort_requires_instance() {
let desc = EffectDescriptor::new();
let s = suite_v1();
let dh = descriptor_handle(&desc);
unsafe {
assert_eq!((s.abort)(dh), status::ERR_BAD_HANDLE);
assert_eq!((s.abort)(std::ptr::null_mut()), status::ERR_BAD_HANDLE);
}
// 实例期:未取消 → REPLY_NO。
let inst = std::sync::Arc::new(crate::instance::Instance {
props: PropertySet::new(),
plugin: std::sync::Arc::new(crate::host::Plugin {
identifier: "t".into(),
version: (1, 0),
bundle_path: std::path::PathBuf::new(),
contexts: vec![],
descriptor: EffectDescriptor::new(),
lib: std::ptr::null_mut(),
entry: dummy_entry,
ofx_plugin: std::ptr::null_mut(),
}),
context: "OfxImageEffectContextFilter".into(),
params: crate::param::ParamSetInstance { params: vec![] },
clips: vec![],
node_identity: std::sync::atomic::AtomicUsize::new(0),
destroyed: std::sync::atomic::AtomicBool::new(false),
sequence_range: std::sync::Mutex::new(None),
progress_cb: std::sync::Mutex::new(None),
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
});
let ih = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
unsafe {
assert_eq!((s.abort)(ih), status::REPLY_NO);
}
}
unsafe extern "C" fn dummy_entry(
_: *const c_char,
_: *const c_void,
_: *mut c_void,
_: *mut c_void,
) -> c_int {
status::OK
}
/// imageMemoryAlloc/Free 走 memory 账本。
#[test]
fn image_memory_ledger() {
let s = suite_v1();
let mut mem: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.image_memory_alloc)(std::ptr::null_mut(), 1024, &mut mem), 0);
assert!(!mem.is_null());
// lock 返回数据指针(第 1 期 = 句柄本身)。
let mut ptr: *mut c_void = std::ptr::null_mut();
assert_eq!((s.image_memory_lock)(mem, &mut ptr), 0);
assert_eq!(ptr, mem);
assert_eq!((s.image_memory_unlock)(mem), 0);
// 未知指针 free → BadHandle。
assert_eq!((s.image_memory_free)(0xdeadbeef as *mut c_void), status::ERR_BAD_HANDLE);
assert_eq!((s.image_memory_free)(mem), 0);
}
}
}
+152
View File
@@ -0,0 +1,152 @@
// 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/>.
//! OfxMemorySuite v1:宿主代分配内存。
//!
//! 语义参照 HS: ofxhHost.cpp:26-42(裸 malloc/free 语义:失败 →
//! kOfxStatErrMemory,free 恒 OK)——但本 crate 按骨架声明增加
//! **宿主级账本**:分配记录进账本,destroyInstance 时兜底回收
//! (泄漏防线;插件正常应自行 free)。未知指针 free →
//! kOfxStatErrBadHandle(SDK ofxMemory.h:51 的契约)。
//! image_effect suite 的 imageMemory* 与此共用账本。
use std::alloc::Layout;
use std::sync::Mutex;
use crate::suites::status;
/// 对齐:malloc 保证 max_align_t(本平台 16 字节);插件会把缓冲当
/// float*/SIMD 用,16 字节是安全下限。
const ALIGN: usize = 16;
/// 一个已分配块:地址 + 布局(回收时按记录布局 dealloc)。
/// 地址以 `usize` 存(不透明令牌,避免裸指针破坏 `static` 的
/// Send/Sync 推导;账本只做 alloc/dealloc 配对,从不解引用)。
struct Block {
ptr: usize,
layout: Layout,
}
/// 宿主级账本(进程单例;memory suite 无宿主指针参数,全局即可)。
static LEDGER: Mutex<Vec<Block>> = Mutex::new(Vec::new());
/// 取锁(毒锁接管:一次持锁 panic 不级联)。
fn lock() -> std::sync::MutexGuard<'static, Vec<Block>> {
LEDGER.lock().unwrap_or_else(|e| e.into_inner())
}
/// 分配并记账。返回裸指针;失败(OOM/布局非法)返回 `None`。
pub(crate) fn alloc(size: usize) -> Option<*mut u8> {
// 零尺寸分配:Layout 要求非零,取 1 字节兜底(free 按同布局回收)。
let layout = Layout::from_size_align(size.max(1), ALIGN).ok()?;
let ptr = unsafe { std::alloc::alloc(layout) };
if ptr.is_null() {
return None;
}
lock().push(Block {
ptr: ptr as usize,
layout,
});
Some(ptr)
}
/// 按指针释放并销账。未知指针返回 `false`(suite 层映射
/// kOfxStatErrBadHandle);`NULL` 按 C 语义 no-op 返回 `true`。
pub(crate) fn free(ptr: *mut u8) -> bool {
if ptr.is_null() {
return true;
}
let mut ledger = lock();
let pos = ledger.iter().position(|b| b.ptr == ptr as usize);
match pos {
Some(i) => {
let b = ledger.remove(i);
unsafe { std::alloc::dealloc(b.ptr as *mut u8, b.layout) };
true
}
None => false,
}
}
/// 兜底回收全部在账块(destroyInstance 的泄漏防线)。返回回收块数
/// (泄漏断言用)。
pub(crate) fn sweep_leaked() -> usize {
let mut ledger = lock();
let n = ledger.len();
for b in ledger.drain(..) {
unsafe { std::alloc::dealloc(b.ptr as *mut u8, b.layout) };
}
n
}
/// 函数表布局(与 SDK `OfxMemorySuiteV1` 一致;`size_t` 在本平台
/// 与 `usize` 同宽,stable Rust 用 `usize` 表达——骨架的 `c_size_t`
/// 是不稳定特性,弃用)。
#[repr(C)]
pub struct MemorySuiteV1 {
/// memoryAlloc:分配 `size` 字节,写 `*out`。
pub alloc: unsafe extern "C" fn(*mut c_void, usize, *mut *mut c_void) -> c_int,
/// memoryFree:释放;NULL 是 no-op。
pub free: unsafe extern "C" fn(*mut c_void) -> c_int,
}
use std::ffi::{c_int, c_void};
/// memoryAlloc 实现:`handle` 忽略(HS 同,ofxhHost.cpp:27)。
///
/// `# Safety`:`out` 必须是可写指针(插件契约)。
unsafe extern "C" fn memory_alloc(
_handle: *mut c_void,
byte_size: usize,
out: *mut *mut c_void,
) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if out.is_null() {
return status::ERR_VALUE;
}
match alloc(byte_size) {
Some(ptr) => {
unsafe { *out = ptr as *mut c_void };
status::OK
}
// 与 HS 一致:分配失败 → kOfxStatErrMemory(ofxhHost.cpp:35)。
None => status::ERR_MEMORY,
}
}))
.unwrap_or(status::FAILED)
}
/// memoryFree 实现:未知指针 → kOfxStatErrBadHandle(SDK 契约);
/// NULL 按 C 语义 no-op → OK(HS 的 free(NULL) 行为,ofxhHost.cpp:40)。
unsafe extern "C" fn memory_free(data: *mut c_void) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if free(data as *mut u8) {
status::OK
} else {
status::ERR_BAD_HANDLE
}
}))
.unwrap_or(status::FAILED)
}
/// 静态函数表实例。
pub fn suite_v1() -> &'static MemorySuiteV1 {
static SUITE: std::sync::OnceLock<MemorySuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| MemorySuiteV1 {
alloc: memory_alloc,
free: memory_free,
})
}
+295
View File
@@ -0,0 +1,295 @@
// 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/>.
//! OfxMessageSuite v1/v2:插件消息 → 宿主日志。
//!
//! stable Rust 无法定义 C-variadic 函数(c_variadic 仍不稳定),
//! v1 的 `...` 与 v2 的 va_list 入口由 C shim 承载
//! (cbits/ofx_message_shim.c,build.rs 经 `cc` 编译,符号随
//! staticlib 进入 liboakplugin):C 侧 vsnprintf 成定长缓冲后转发
//! 到本模块的 `oak_ofx_message_impl`。
//!
//! 消息出口 = facade 注册的 `oakplugin_message_fn`
//! (include/plugin/host.h)。注意:骨架 ffi.rs 声明的 `MessageFn`
//! 与头文件不符(头文件是 `(type, message, userdata)`,骨架是
//! `(userdata, level, message)`)——做 ffi.rs 时以头文件为准修正。
//! 本模块按头文件契约建模。
use std::ffi::{c_char, c_int, c_void, CStr};
use crate::suites::status;
/// facade 消息回调(include/plugin/host.h `oakplugin_message_fn`):
/// `(type, message, userdata) -> OAKPLUGIN_MESSAGE_ANSWER_NO/YES`。
pub(crate) type MessageHandler =
unsafe extern "C" fn(*const c_char, *const c_char, *mut c_void) -> c_int;
/// 消息出口注册表(ffi 层经 `oakplugin_host_set_message_handler`
/// 写入,suite 读;userdata 以 usize 存,避免裸指针破坏 static 的
/// Send/Sync 推导)。
static HANDLER: std::sync::Mutex<(Option<MessageHandler>, usize)> =
std::sync::Mutex::new((None, 0));
/// 注册/注销消息出口(ffi 层 `oakplugin_host_set_message_handler`
/// 调用;公开:测试直接注入捕获器)。
pub fn set_handler(f: Option<MessageHandler>, userdata: *mut c_void) {
let mut h = HANDLER.lock().unwrap_or_else(|e| e.into_inner());
*h = (f, userdata as usize);
}
/// 读取当前出口。
fn handler() -> (Option<MessageHandler>, *mut c_void) {
let h = HANDLER.lock().unwrap_or_else(|e| e.into_inner());
(h.0, h.1 as *mut c_void)
}
/// kOfxMessageQuestion(ofxMessage.h:61):无出口时答"否"。
const K_MESSAGE_QUESTION: &[u8] = b"OfxMessageQuestion";
/// 消息类型是否为 question。
fn is_question(type_: *const c_char) -> bool {
// 空指针已在调用点拦截;此处仍防御(CStr::from_ptr 的调用方义务)。
if type_.is_null() {
return false;
}
unsafe { CStr::from_ptr(type_) }.to_bytes() == K_MESSAGE_QUESTION
}
/// Rust 侧实现(C shim 转发至此;`#[no_mangle]` 保证符号名与 C 侧
/// 引用一致)。语义逐条对照 C++:
/// - type/message 为空 → kOfxStatFailed(olivehost.cpp:267,vmessage
/// 对 !type || !format 的处理;format 为空的 case 由 shim 以
/// NULL message 转发);
/// - 有出口 → 转发 `(type, message, userdata)`,返回按头文件契约
/// 映射为 REPLY_YES/REPLY_NO(olivehost.cpp:277-279 的 handler
/// 路径;非 question 消息插件忽略返回值);
/// - 无出口 → stderr 日志(镜像 olivehost.cpp:282 的 fprintf 默认),
/// question 答"否"(olivehost.cpp:283-284)。
///
/// `# Safety`:`type`/`message` 必须是有效 C 字符串(插件契约,
/// shim 已保证 message 非悬垂——指向其栈缓冲,调用期间有效)。
#[no_mangle]
pub unsafe extern "C" fn oak_ofx_message_impl(
_handle: *mut c_void,
type_: *const c_char,
_id: *const c_char,
message: *const c_char,
) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if type_.is_null() || message.is_null() {
return status::FAILED;
}
let (handler, userdata) = handler();
if let Some(h) = handler {
// 头文件契约:返回值 0/1 即答复 NO/YES。
let answer = unsafe { h(type_, message, userdata) };
return if answer != 0 {
status::REPLY_YES
} else {
status::REPLY_NO
};
}
// Headless 默认:stderr(C++ fprintf 的 Rust 等价物)。
eprintln!("OFX message: {}", unsafe { CStr::from_ptr(message) }.to_string_lossy());
if is_question(type_) {
status::REPLY_NO
} else {
status::OK
}
}))
.unwrap_or(status::FAILED)
}
/// 函数表布局(OfxMessageSuiteV1)。
#[repr(C)]
pub struct MessageSuiteV1 {
/// message(变长参数版;入口在 C shim)
pub message: unsafe extern "C" fn(
*mut c_void,
*const c_char,
*const c_char,
*const c_char,
...
) -> c_int,
}
/// 函数表布局(OfxMessageSuiteV2:第 5 参为平台相关 `va_list`)。
///
/// 真实 ABI 是 C 的 va_list(Apple arm64 上是结构体按值传递);
/// 声明为不透明指针仅作占位——本表只被插件→C shim 调用,Rust 侧
/// 永不直接调用,指针类型不参与任何实际调用。
#[repr(C)]
pub struct MessageSuiteV2 {
/// message(va_list 版;入口在 C shim)
pub message: unsafe extern "C" fn(
*mut c_void,
*const c_char,
*const c_char,
*const c_char,
*mut c_void,
) -> c_int,
}
extern "C" {
/// C shim 的 v1 入口(cbits/ofx_message_shim.c)。
fn ofx_message_shim_v1(
handle: *mut c_void,
type_: *const c_char,
id: *const c_char,
format: *const c_char,
...
) -> c_int;
/// C shim 的 v2 入口(va_list 已由 C 侧消费,Rust 只见不透明值)。
fn ofx_message_shim_v2(
handle: *mut c_void,
type_: *const c_char,
id: *const c_char,
format: *const c_char,
args: *mut c_void,
) -> c_int;
}
/// 静态函数表实例(v1)。
pub fn suite_v1() -> &'static MessageSuiteV1 {
static SUITE: std::sync::OnceLock<MessageSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| MessageSuiteV1 {
message: ofx_message_shim_v1,
})
}
/// 静态函数表实例(v2)。
pub fn suite_v2() -> &'static MessageSuiteV2 {
static SUITE: std::sync::OnceLock<MessageSuiteV2> = std::sync::OnceLock::new();
SUITE.get_or_init(|| MessageSuiteV2 {
message: ofx_message_shim_v2,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
/// 测试共享全局 HANDLER,cargo 默认并行执行——串行化契约用例。
static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// 捕获型出口:把 (type, message) 记进 userdata 指向的 Vec。
unsafe extern "C" fn capture(
type_: *const c_char,
message: *const c_char,
userdata: *mut c_void,
) -> c_int {
let v = unsafe { &mut *(userdata as *mut Vec<(String, String)>) };
v.push((
unsafe { CStr::from_ptr(type_) }.to_string_lossy().into_owned(),
unsafe { CStr::from_ptr(message) }.to_string_lossy().into_owned(),
));
1
}
/// v1 入口:`...` 在 C shim 侧 vsnprintf 后转发,格式化正确。
#[test]
fn shim_v1_formats_and_forwards() {
let _g = TEST_LOCK.lock().unwrap();
let mut captured: Vec<(String, String)> = Vec::new();
set_handler(Some(capture), &mut captured as *mut _ as *mut c_void);
let s = suite_v1();
let type_ = CString::new("OfxMessageError").unwrap();
let id = CString::new("test-id").unwrap();
let fmt = CString::new("value=%d name=%s").unwrap();
let name = CString::new("oak").unwrap();
let r = unsafe {
(s.message)(
std::ptr::null_mut(),
type_.as_ptr(),
id.as_ptr(),
fmt.as_ptr(),
42,
name.as_ptr(),
)
};
// 出口答 YES → REPLY_YES。
assert_eq!(r, status::REPLY_YES);
assert_eq!(captured.len(), 1);
assert_eq!(captured[0].0, "OfxMessageError");
assert_eq!(captured[0].1, "value=42 name=oak");
set_handler(None, std::ptr::null_mut());
}
/// v1 入口:NULL format → kOfxStatFailed(C++ olivehost.cpp:267)。
#[test]
fn shim_v1_null_format_fails() {
let _g = TEST_LOCK.lock().unwrap();
let s = suite_v1();
let type_ = CString::new("OfxMessageError").unwrap();
let id = CString::new("id").unwrap();
let r = unsafe {
(s.message)(
std::ptr::null_mut(),
type_.as_ptr(),
id.as_ptr(),
std::ptr::null(),
)
};
assert_eq!(r, status::FAILED);
}
/// v2 入口:表字段非空且指向 C shim 符号(va_list 路径无法从
/// Rust 构造 va_list 调用,链的其余部分与 v1 共用 forward)。
#[test]
fn shim_v2_table_entry_resolves() {
let _g = TEST_LOCK.lock().unwrap();
let s = suite_v2();
assert!(!std::ptr::eq(s.message as *const (), ofx_message_shim_v1 as *const ()));
}
/// 无出口(headless 默认):非 question → OK;question → REPLY_NO。
#[test]
fn no_handler_headless_defaults() {
let _g = TEST_LOCK.lock().unwrap();
set_handler(None, std::ptr::null_mut());
let s = suite_v1();
let type_ = CString::new("OfxMessageError").unwrap();
let id = CString::new("id").unwrap();
let fmt = CString::new("hello %s").unwrap();
let arg = CString::new("x").unwrap();
let r = unsafe {
(s.message)(
std::ptr::null_mut(),
type_.as_ptr(),
id.as_ptr(),
fmt.as_ptr(),
arg.as_ptr(),
)
};
assert_eq!(r, status::OK);
let q = CString::new("OfxMessageQuestion").unwrap();
let r = unsafe {
(s.message)(
std::ptr::null_mut(),
q.as_ptr(),
id.as_ptr(),
fmt.as_ptr(),
arg.as_ptr(),
)
};
assert_eq!(r, status::REPLY_NO);
}
}
+325
View File
@@ -0,0 +1,325 @@
// 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/>.
//! suite 层:插件调进来的 C 函数表(unsafe trampoline 集中区)。
//!
//! 每张 suite 是一个 `#[repr(C)]` 的函数表静态实例;插件经
//! [`fetch_suite`] 取得。所有入口的安全模式相同:
//!
//! 1. 解 handle(`RefBox`/`PropertySet` 指针),空指针 →
//! kOfxStatErrBadHandle;
//! 2. 调 safe Rust 实现;
//! 3. `catch_unwind` 兜底 → kOfxStatFailed。
//!
//! 参照:HS: ofxhImageEffect.cpp:2776(fetchSuite 分发表与版本协商)。
pub mod gl_render;
pub mod image_effect;
pub mod memory;
pub mod message;
pub mod multithread;
pub mod param;
pub mod progress;
pub mod property;
pub mod timeline;
/// OFX 状态码(全表与 SDK ofxCore.h:895-953 逐字一致;插件对状态码
/// 做精确比较,任何偏差都是静默兼容性破坏)。
pub mod status {
/// kOfxStatOK:成功。
pub const OK: i32 = 0;
/// kOfxStatFailed:通用失败。
pub const FAILED: i32 = 1;
/// kOfxStatErrFatal:致命错误。
pub const ERR_FATAL: i32 = 2;
/// kOfxStatErrUnknown:未知对象/属性。
pub const ERR_UNKNOWN: i32 = 3;
/// kOfxStatErrMissingHostFeature:宿主缺功能。
pub const ERR_MISSING_HOST_FEATURE: i32 = 4;
/// kOfxStatErrUnsupported:不支持的操作。
pub const ERR_UNSUPPORTED: i32 = 5;
/// kOfxStatErrExists:对象已存在。
pub const ERR_EXISTS: i32 = 6;
/// kOfxStatErrFormat:格式错误。
pub const ERR_FORMAT: i32 = 7;
/// kOfxStatErrMemory:内存不足。
pub const ERR_MEMORY: i32 = 8;
/// kOfxStatErrBadHandle:非法 handle(含 NULL)。
pub const ERR_BAD_HANDLE: i32 = 9;
/// kOfxStatErrBadIndex:索引越界。
pub const ERR_BAD_INDEX: i32 = 10;
/// kOfxStatErrValue:非法值。
pub const ERR_VALUE: i32 = 11;
/// kOfxStatReplyYes。
pub const REPLY_YES: i32 = 12;
/// kOfxStatReplyNo。
pub const REPLY_NO: i32 = 13;
/// kOfxStatReplyDefault。
pub const REPLY_DEFAULT: i32 = 14;
}
/// 句柄约定:发给插件的每个 OFX 句柄 = 对象 `props` 字段地址 | 低
/// 3 位标签。
///
/// 前置条件(本 crate 内部纪律,宿主创建对象时遵守):
/// - 对象 `#[repr(C)]` 且 `props: PropertySet` 在**偏移 0**——句柄的
/// 地址就是属性集地址,与 C++ "基类在偏移 0"(HS 里
/// OfxPropertySetHandle 即 Property::Set*)同构,属性 suite 无需
/// 反查即可直接解引用;
/// - 对象堆上稳定:`Arc<RefBox<T>>` / 结构体内的 `Box<T>` 元素
/// (Vec 重分配只移动 Box 指针,不移动负载)。
///
/// 标签位编码:低 3 位(含 Mutex/Arc 的对象对齐 ≥8,低 3 位恒 0)。
pub mod tag {
/// 标签掩码(低 3 位)。
pub const MASK: usize = 0b111;
/// 裸属性集(宿主内部直用 / 测试直传 `&props`)。
pub const PROPERTY_SET: usize = 0;
/// [`crate::descriptor::EffectDescriptor`](describe 期 effect/
/// param-set handle)。
pub const DESCRIPTOR: usize = 1;
/// [`crate::instance::Instance`](实例期 effect/param-set handle)。
pub const INSTANCE: usize = 2;
/// [`crate::param::ParamDef`](describe 期 param handle)。
pub const PARAM_DEF: usize = 3;
/// [`crate::param::ParamInstance`](实例期 param handle)。
pub const PARAM_INSTANCE: usize = 4;
/// [`crate::clip::ClipInstance`](实例期 clip handle;describe 期
/// [`crate::descriptor::ClipDescriptor`] 共用此标签——两类都只在
/// 各自的阶段被使用,属性 suite 不区分)。
pub const CLIP: usize = 5;
/// [`crate::image::Image`](clipGetImage 的产物)。
pub const IMAGE: usize = 6;
/// 打标签(宿主创建对象句柄用;公开供宿主/测试构造句柄)。
pub fn make(
props: *const crate::property::PropertySet,
t: usize,
) -> *mut std::ffi::c_void {
(props as usize | t) as *mut std::ffi::c_void
}
/// 剥标签,取 props 指针(标签 0 的裸指针原样返回)。
pub fn strip(handle: *mut std::ffi::c_void) -> *const crate::property::PropertySet {
((handle as usize) & !MASK) as *const crate::property::PropertySet
}
/// 读标签。
pub fn kind(handle: *mut std::ffi::c_void) -> usize {
(handle as usize) & MASK
}
}
/// 渲染上下文(TLS):render 驱动在调用插件 action 前设置,suite
/// 回调(timeline、clipGetImage 的 scale、进度)从中读取;对应 HS
/// 中实例在渲染期的状态(ofxhImageEffect.cpp Render in_args 的子集)。
#[derive(Clone, Copy)]
pub struct RenderCtx {
/// 当前渲染时间(秒)。
pub time: f64,
/// 渲染比例。
pub scale: crate::instance::RenderScale,
/// 项目时间域(timeline getTimeBounds)。
pub range: crate::instance::OfxRangeD,
}
thread_local! {
static RENDER_CTX: std::cell::RefCell<Option<RenderCtx>> = const { std::cell::RefCell::new(None) };
}
/// 设置/清除渲染上下文(render 驱动调用;action 返回前必须清除;
/// 公开:render 驱动在 host 层,测试也直接注入)。
pub fn set_render_ctx(ctx: Option<RenderCtx>) {
RENDER_CTX.with(|c| *c.borrow_mut() = ctx);
}
/// 读取渲染上下文(无上下文返回 None;无 TLS 时插件时间查询得到
/// 0 的 headless 默认)。
pub(crate) fn render_ctx() -> Option<RenderCtx> {
RENDER_CTX.with(|c| *c.borrow())
}
thread_local! {
/// 当前渲染的输出图像(render 驱动设置;image effect suite 的
/// clipGetImage 对 Output clip 返回它)。
static CURRENT_OUTPUT: std::cell::RefCell<Option<std::sync::Arc<crate::image::Image>>> =
const { std::cell::RefCell::new(None) };
}
/// 设置/清除当前输出图像(render 驱动调用;action 返回前必须清除)。
pub(crate) fn set_current_output(image: Option<std::sync::Arc<crate::image::Image>>) {
CURRENT_OUTPUT.with(|c| *c.borrow_mut() = image);
}
/// 读取当前输出图像。
pub(crate) fn current_output() -> Option<std::sync::Arc<crate::image::Image>> {
CURRENT_OUTPUT.with(|c| c.borrow().clone())
}
/// GL 渲染上下文(TLS):GL render 驱动在调用插件 action 前设置,
/// OpenGLRender suite 的 clipLoadTexture 从中取当前渲染器与输出
/// 纹理(对应 C++ 里实例渲染期的 GL 状态;ofxGPURender.h
/// "OpenGL Current Context" 一节要求宿主在 Render 期间持有 GL
/// 上下文——本设计的约定是调用方(oakrender 的 PluginJob 路径)在
/// 进入 render_job 前已把渲染器上下文置为 current,本表只传递句柄)。
#[derive(Clone, Copy)]
pub struct GlCtx {
/// 当前渲染器(oakrender 句柄)。
pub renderer: crate::bridge::render::RendererHandle,
/// 已附着的输出纹理(渲染目标;GL 模式下插件把结果画进它)。
pub output_texture: crate::bridge::render::TextureHandle,
/// 当前 GL 纹理的实际像素深度(kOfxBitDepth* 静态串;Phase 2
/// 全链路 F32,由 render_gl 按插件 kOfxOpenGLPropPixelDepth 协商
/// 后填入——纹理句柄的 kOfxImageEffectPropPixelDepth 以它为准)。
pub gl_pixel_depth: &'static str,
}
thread_local! {
static GL_CTX: std::cell::RefCell<Option<GlCtx>> = const { std::cell::RefCell::new(None) };
}
/// 设置/清除 GL 渲染上下文(GL render 驱动调用;action 返回前必须
/// 清除;公开:测试直接注入)。
pub fn set_gl_ctx(ctx: Option<GlCtx>) {
GL_CTX.with(|c| *c.borrow_mut() = ctx);
}
/// 读取 GL 渲染上下文(无上下文返回 None——clipLoadTexture 在非
/// GL 渲染期调用时按规范返回 kOfxStatErrMissingHostFeature)。
pub(crate) fn gl_ctx() -> Option<GlCtx> {
GL_CTX.with(|c| *c.borrow())
}
/// 宿主进程身份(fetchSuite 的 version 检查用;= OFX API 1.5)。
pub(crate) const OFX_API_VERSION: i32 = 105;
/// fetchSuite 宿主入口:按名字与版本返回函数表指针;不认识或版本
/// 不符返回 `None`(FFI 层转 NULL)。
///
/// 第 1 期注册:OfxPropertySuite v1、OfxMemorySuite v1、
/// OfxImageEffectSuite v1、OfxParameterSuite v1、OfxMessageSuite
/// v1/v2、OfxProgressSuite v1/v2、OfxTimeLineSuite v1、
/// OfxMultiThreadSuite v1。
/// 第 2 期追加:OfxImageEffectOpenGLRenderSuite v1(GL 路径);
/// ofxColour 无 suite 表(纯属性 + GetOutputColourspace action)。
pub fn fetch_suite(name: &str, version: i32) -> Option<*const std::ffi::c_void> {
let suite: *const std::ffi::c_void = match (name, version) {
("OfxPropertySuite", 1) => ptr(property::suite_v1()),
("OfxMemorySuite", 1) => ptr(memory::suite_v1()),
("OfxImageEffectSuite", 1) => ptr(image_effect::suite_v1()),
("OfxParameterSuite", 1) => ptr(param::suite_v1()),
("OfxMessageSuite", 1) => ptr(message::suite_v1()),
("OfxMessageSuite", 2) => ptr(message::suite_v2()),
("OfxProgressSuite", 1) => ptr(progress::suite_v1()),
("OfxProgressSuite", 2) => ptr(progress::suite_v2()),
("OfxTimeLineSuite", 1) => ptr(timeline::suite_v1()),
("OfxMultiThreadSuite", 1) => ptr(multithread::suite_v1()),
("OfxImageEffectOpenGLRenderSuite", 1) => ptr(gl_render::suite_v1()),
_ => return None,
};
Some(suite)
}
/// 表引用 → 不透明指针(fetch_suite 的统一出口)。
fn ptr<T>(p: &'static T) -> *const std::ffi::c_void {
p as *const T as *const std::ffi::c_void
}
#[cfg(test)]
mod tests {
use super::*;
/// 八张 suite 的分发表:版本精确匹配、未知版本/名字 → None。
#[test]
fn fetch_suite_dispatch() {
assert!(fetch_suite("OfxPropertySuite", 1).is_some());
assert!(fetch_suite("OfxMemorySuite", 1).is_some());
assert!(fetch_suite("OfxImageEffectSuite", 1).is_some());
assert!(fetch_suite("OfxParameterSuite", 1).is_some());
assert!(fetch_suite("OfxMessageSuite", 1).is_some());
assert!(fetch_suite("OfxMessageSuite", 2).is_some());
assert!(fetch_suite("OfxProgressSuite", 1).is_some());
assert!(fetch_suite("OfxProgressSuite", 2).is_some());
assert!(fetch_suite("OfxTimeLineSuite", 1).is_some());
assert!(fetch_suite("OfxMultiThreadSuite", 1).is_some());
assert!(fetch_suite("OfxImageEffectOpenGLRenderSuite", 1).is_some());
assert!(fetch_suite("OfxPropertySuite", 2).is_none());
assert!(fetch_suite("OfxMessageSuite", 3).is_none());
assert!(fetch_suite("OfxImageEffectOpenGLRenderSuite", 2).is_none());
assert!(fetch_suite("OfxBogusSuite", 1).is_none());
assert!(fetch_suite("", 1).is_none());
}
/// 句柄标签:打标/剥标/读 kind 往返;对齐地址低位为 0。
#[test]
fn handle_tag_roundtrip() {
let set = crate::property::PropertySet::new();
let props = &set as *const crate::property::PropertySet;
assert_eq!(props as usize & tag::MASK, 0, "PropertySet 必须 ≥8 对齐");
for t in [
tag::PROPERTY_SET,
tag::DESCRIPTOR,
tag::INSTANCE,
tag::PARAM_DEF,
tag::PARAM_INSTANCE,
tag::CLIP,
tag::IMAGE,
] {
let h = tag::make(props, t);
assert_eq!(tag::kind(h), t);
assert_eq!(tag::strip(h) as usize, props as usize);
}
}
/// 渲染上下文 TLS:设置/读取/清除。
#[test]
fn render_ctx_tls() {
assert!(render_ctx().is_none());
let ctx = RenderCtx {
time: 1.5,
scale: crate::instance::RenderScale { x: 2.0, y: 2.0 },
range: crate::instance::OfxRangeD { min: 0.0, max: 100.0 },
};
set_render_ctx(Some(ctx));
let got = render_ctx().unwrap();
assert_eq!(got.time, 1.5);
assert_eq!(got.scale.x, 2.0);
assert_eq!(got.range.max, 100.0);
set_render_ctx(None);
assert!(render_ctx().is_none());
}
/// GL 上下文 TLS:设置/读取/清除(clipLoadTexture 的渲染期取值)。
#[test]
fn gl_ctx_tls() {
assert!(gl_ctx().is_none());
let renderer = crate::handle::CHandle::null();
let tex = crate::handle::CHandle::null();
set_gl_ctx(Some(GlCtx {
renderer,
output_texture: tex,
gl_pixel_depth: "OfxBitDepthFloat",
}));
let got = gl_ctx().unwrap();
assert!(got.renderer.is_null());
assert!(got.output_texture.is_null());
assert_eq!(got.gl_pixel_depth, "OfxBitDepthFloat");
set_gl_ctx(None);
assert!(gl_ctx().is_none());
}
}
+217
View File
@@ -0,0 +1,217 @@
// 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/>.
//! OfxMultiThreadSuite v1:插件自起线程的登记与索引分配。
//!
//! 纪律:插件线程可在本 suite 存活期回调任意 suite——所有共享状态
//! 必须 Mutex(README §3)。线程经 `std::thread::scope` 启动并同步
//! join(OFX 语义:multiThread 返回时全部线程已完成;
//! HS: ofxhImageEffect.cpp gMultiThreadSuite),故无需常驻线程表。
//! 线程索引经 TLS 分配:宿主线程 [`index`] 为 -1、
//! [`is_spawned`] 为 0。
//!
//! 参照:HS: ofxhImageEffect.cpp gMultiThreadSuite。
use std::ffi::{c_int, c_uint, c_void};
use crate::suites::status;
thread_local! {
/// 本线程的 OFX 线程索引(宿主线程 = None)。
static THREAD_INDEX: std::cell::Cell<Option<c_uint>> = const { std::cell::Cell::new(None) };
}
/// 函数表布局(OfxMultiThreadSuiteV1)。
#[repr(C)]
pub struct MultiThreadSuiteV1 {
/// multiThread:启动 `n_threads` 个线程跑 `func`,线程索引
/// 0..n_threads-1 经 `thread_arg` 传入。
pub multi_thread: unsafe extern "C" fn(
func: unsafe extern "C" fn(c_uint, c_uint, *mut c_void),
n_threads: c_uint,
thread_arg: *mut c_void,
) -> c_int,
/// multiThreadNumCPUs
pub num_cpus: unsafe extern "C" fn(*mut c_int) -> c_int,
/// multiThreadIndex:当前线程的 OFX 线程索引(宿主线程为 -1)。
pub index: unsafe extern "C" fn(*mut c_int) -> c_int,
/// multiThreadIsSpawnedThread:当前线程是否插件线程。
pub is_spawned: unsafe extern "C" fn(*mut c_int) -> c_int,
}
/// 公共入口模板:panic 兜底。
fn caught(f: impl FnOnce() -> c_int) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or(status::FAILED)
}
/// multiThread 实现:同步起 n 个线程(0..n-1),join 后返回。
/// 线程体只做两件事:登记 TLS 索引、调 `func`(C 代码,不 panic)。
unsafe extern "C" fn multi_thread(
func: unsafe extern "C" fn(c_uint, c_uint, *mut c_void),
n_threads: c_uint,
thread_arg: *mut c_void,
) -> c_int {
caught(|| {
// 裸指针不可 Send:以 usize 搬运,线程内还原(C 侧本即
// 整数传递语义)。
let arg = thread_arg as usize;
let _ = std::thread::scope(|scope| {
for i in 0..n_threads {
scope.spawn(move || {
THREAD_INDEX.with(|t| t.set(Some(i)));
// func 是插件代码:按 OFX 契约不 panic;Rust 侧
// 只做 TLS 登记,无 panic 源。
unsafe { (func)(i, n_threads, arg as *mut c_void) };
THREAD_INDEX.with(|t| t.set(None));
});
}
});
status::OK
})
}
unsafe extern "C" fn multi_thread_num_cpus(out: *mut c_int) -> c_int {
caught(|| {
if out.is_null() {
return status::ERR_VALUE;
}
// 物理核数(HS 同:系统 CPU 数)。
let n = std::thread::available_parallelism()
.map(|n| n.get() as c_int)
.unwrap_or(1);
unsafe { *out = n };
status::OK
})
}
unsafe extern "C" fn multi_thread_index(out: *mut c_int) -> c_int {
caught(|| {
if out.is_null() {
return status::ERR_VALUE;
}
// 宿主线程 → -1(HS 约定)。
unsafe { *out = THREAD_INDEX.with(|t| t.get()).map_or(-1, |i| i as c_int) };
status::OK
})
}
unsafe extern "C" fn multi_thread_is_spawned(out: *mut c_int) -> c_int {
caught(|| {
if out.is_null() {
return status::ERR_VALUE;
}
unsafe { *out = THREAD_INDEX.with(|t| t.get().is_some()) as c_int };
status::OK
})
}
/// 静态函数表实例。
pub fn suite_v1() -> &'static MultiThreadSuiteV1 {
static SUITE: std::sync::OnceLock<MultiThreadSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| MultiThreadSuiteV1 {
multi_thread: multi_thread,
num_cpus: multi_thread_num_cpus,
index: multi_thread_index,
is_spawned: multi_thread_is_spawned,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
/// 每个插件线程把 (index, count) 累计进 userdata。
unsafe extern "C" fn worker(index: c_uint, count: c_uint, arg: *mut c_void) {
let state = unsafe { &mut *(arg as *mut (AtomicUsize, AtomicUsize)) };
state.0.fetch_add(index as usize, Ordering::Relaxed);
state.1.fetch_add(count as usize, Ordering::Relaxed);
}
#[test]
fn multi_thread_spawns_and_joins() {
let s = suite_v1();
let state = (AtomicUsize::new(0), AtomicUsize::new(0));
unsafe {
assert_eq!(
(s.multi_thread)(worker, 8, &state as *const _ as *mut c_void),
status::OK
);
}
// 索引 0..7 各一次 → 28;count 各 8 → 64。
assert_eq!(state.0.load(Ordering::Relaxed), 28);
assert_eq!(state.1.load(Ordering::Relaxed), 64);
// 0 线程:no-op OK。
unsafe {
assert_eq!(
(s.multi_thread)(worker, 0, &state as *const _ as *mut c_void),
status::OK
);
}
}
/// 插件线程内查询 index/isSpawned 并把结果写回 arg 指向的槽。
/// (闭包捕获 suite 无法转 fn 指针,用具名函数 + 静态槽。)
static SPAWNED_RESULT: std::sync::LazyLock<std::sync::Mutex<Option<(c_int, c_int)>>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(None));
unsafe extern "C" fn query_worker(_index: c_uint, _count: c_uint, _arg: *mut c_void) {
let s = suite_v1();
let mut i = 0;
let mut sp = 1;
unsafe {
let _ = (s.index)(&mut i);
let _ = (s.is_spawned)(&mut sp);
}
*SPAWNED_RESULT.lock().unwrap_or_else(|e| e.into_inner()) = Some((i, sp));
}
#[test]
fn index_and_spawned() {
let s = suite_v1();
let mut i = 0;
let mut spawned = 1;
unsafe {
// 宿主线程:index = -1,isSpawned = 0。
assert_eq!((s.index)(&mut i), status::OK);
assert_eq!(i, -1);
assert_eq!((s.is_spawned)(&mut spawned), status::OK);
assert_eq!(spawned, 0);
// 插件线程内:index 正确、isSpawned = 1。
assert_eq!(
(s.multi_thread)(query_worker, 1, std::ptr::null_mut()),
status::OK
);
assert_eq!(
*SPAWNED_RESULT.lock().unwrap_or_else(|e| e.into_inner()),
Some((0, 1))
);
}
}
#[test]
fn num_cpus_positive() {
let s = suite_v1();
let mut n = 0;
unsafe {
assert_eq!((s.num_cpus)(&mut n), status::OK);
}
assert!(n >= 1);
}
}
+983
View File
@@ -0,0 +1,983 @@
// 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/>.
//! OfxParameterSuite v1:参数定义与读写。
//!
//! 语义对照 HS: ofxhParam.cpp:
//! - paramDefine:describe 期把参数定义挂到效果描述符(未定义类型 →
//! kOfxStatErrUnsupported,HS:1665-1710;parametric 第 1 期不支持);
//! - paramGetHandle:按名查(未找到 → kOfxStatErrUnknown,HS:1758);
//! - paramGetValue/paramSetValue 等变长入口在 C shim
//! (cbits/ofx_param_shim.c):按 [`crate::param::ParamKind`] 解析
//! va_args 后转发到本模块的 `ofx_param_*_impl`;
//! - paramSetValue 成功后触发 instanceChanged(kOfxChangePluginEdited,
//! HS:1982-1994 的 `paramChangedByPlugin`),经
//! [`crate::param::notify_instance_changed`] 回写绑定节点;
//! - 第 1 期无动画:AtTime == 当前值;keys 恒 0;derivative/integral
//! → kOfxStatErrMissingHostFeature;editBegin/End 为编辑事务括号
//! (undo 分组:事务内回写并入一条 multi 命令,见
//! [`crate::instance::Instance::edit_begin`])。
use std::collections::HashMap;
use std::ffi::{c_char, c_double, c_int, c_void, CStr, CString};
use std::sync::Mutex;
use crate::descriptor::EffectDescriptor;
use crate::instance::Instance;
use crate::param::{ChangeReason, ParamDef, ParamInstance, ParamKind, ParamValue};
use crate::suites::status;
use crate::suites::tag;
/// 函数表布局(与 SDK `OfxParameterSuiteV1` 一致;字段序以
/// SDK 为准)。
#[repr(C)]
pub struct ParameterSuiteV1 {
/// paramDefine(describe 期间):定义参数。
pub param_define: unsafe extern "C" fn(*mut c_void, *const c_char, *const c_char, *mut *mut c_void) -> c_int,
/// paramGetHandle
pub param_get_handle: unsafe extern "C" fn(*mut c_void, *const c_char, *mut *mut c_void, *mut *mut c_void) -> c_int,
/// paramSetGetPropertySet
pub param_set_get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
/// paramGetPropertySet
pub param_get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
/// paramGetValue(变长出口参数按类型解释;入口在 C shim)
pub param_get_value: unsafe extern "C" fn(*mut c_void, ...) -> c_int,
/// paramGetValueAtTime
pub param_get_value_at_time: unsafe extern "C" fn(*mut c_void, c_double, ...) -> c_int,
/// paramGetDerivative
pub param_get_derivative: unsafe extern "C" fn(*mut c_void, c_double, ...) -> c_int,
/// paramGetIntegral
pub param_get_integral: unsafe extern "C" fn(*mut c_void, c_double, c_double, ...) -> c_int,
/// paramSetValue(触发 instanceChanged)
pub param_set_value: unsafe extern "C" fn(*mut c_void, ...) -> c_int,
/// paramSetValueAtTime
pub param_set_value_at_time: unsafe extern "C" fn(*mut c_void, c_double, ...) -> c_int,
/// paramGetNumKeys
pub param_get_num_keys: unsafe extern "C" fn(*mut c_void, *mut c_int) -> c_int,
/// paramGetKeyTime
pub param_get_key_time: unsafe extern "C" fn(*mut c_void, c_int, *mut c_double) -> c_int,
/// paramGetKeyIndex
pub param_get_key_index: unsafe extern "C" fn(*mut c_void, c_double, c_int, *mut c_int) -> c_int,
/// paramDeleteKey
pub param_delete_key: unsafe extern "C" fn(*mut c_void, c_double) -> c_int,
/// paramDeleteAllKeys
pub param_delete_all_keys: unsafe extern "C" fn(*mut c_void) -> c_int,
/// paramCopy
pub param_copy: unsafe extern "C" fn(*mut c_void, *mut c_void, c_double, c_double, *const c_void) -> c_int,
/// paramEditBegin / paramEditEnd(编辑事务括号)
pub param_edit_begin: unsafe extern "C" fn(*mut c_void) -> c_int,
/// paramEditEnd
pub param_edit_end: unsafe extern "C" fn(*mut c_void) -> c_int,
}
// ---- 句柄解析 -----------------------------------------------------------
/// param-set handle:describe 期是效果描述符,实例期是实例
/// (getParamSet 返回与 effect 相同的 handle,见 image_effect suite)。
enum ParamSetRef<'a> {
Descriptor(&'a mut EffectDescriptor),
Instance(&'a Instance),
}
/// param handle:describe 期是定义(值=默认值),实例期是实例。
enum ParamRef<'a> {
Def(&'a ParamDef),
Instance(&'a ParamInstance),
}
/// 解析 param-set(effect)句柄。空指针/标签不符 → BadHandle。
fn resolve_param_set(handle: *mut c_void) -> Result<ParamSetRef<'static>, c_int> {
if handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
// 两种对象 props 均在偏移 0(句柄约定)。
unsafe {
match tag::kind(handle) {
tag::DESCRIPTOR => {
Ok(ParamSetRef::Descriptor(&mut *(tag::strip(handle) as *mut EffectDescriptor)))
}
tag::INSTANCE => Ok(ParamSetRef::Instance(&*(tag::strip(handle) as *const Instance))),
_ => Err(status::ERR_BAD_HANDLE),
}
}
}
/// 解析 param 句柄。
fn resolve_param(handle: *mut c_void) -> Result<ParamRef<'static>, c_int> {
if handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe {
match tag::kind(handle) {
tag::PARAM_DEF => Ok(ParamRef::Def(&*(tag::strip(handle) as *const ParamDef))),
tag::PARAM_INSTANCE => {
Ok(ParamRef::Instance(&*(tag::strip(handle) as *const ParamInstance)))
}
_ => Err(status::ERR_BAD_HANDLE),
}
}
}
/// 属性名(空指针/非 UTF-8 → ErrValue;防御性)。
unsafe fn c_name<'a>(name: *const c_char) -> Result<&'a str, c_int> {
if name.is_null() {
return Err(status::ERR_VALUE);
}
unsafe { CStr::from_ptr(name) }
.to_str()
.map_err(|_| status::ERR_VALUE)
}
/// 公共入口模板:panic 兜底。
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f))
.map_or_else(|_| status::FAILED, |r| r.map_or_else(|c| c, |()| status::OK))
}
// ---- 变长参数实现(C shim 转发)----------------------------------------
impl ParamKind {
fn from_i32(v: c_int) -> Option<ParamKind> {
Some(match v {
1 => ParamKind::Int,
2 => ParamKind::Int2,
3 => ParamKind::Int3,
4 => ParamKind::Double,
5 => ParamKind::Double2,
6 => ParamKind::Double3,
7 => ParamKind::Bool,
8 => ParamKind::Choice,
9 => ParamKind::Rgb,
10 => ParamKind::Rgba,
11 => ParamKind::Str,
12 => ParamKind::StrChoice,
_ => return None,
})
}
}
/// 按 kind 把 ParamValue 写进类型化 out(数值类)。
fn write_value(v: &ParamValue, kind: ParamKind, out: *mut c_void) -> Result<(), c_int> {
let err = || Err(status::FAILED);
unsafe {
match kind {
ParamKind::Int => match v {
ParamValue::Int(a, _) => {
*(out as *mut c_int) = a[0];
Ok(())
}
_ => err(),
},
ParamKind::Int2 | ParamKind::Int3 => match v {
ParamValue::Int(a, dim) => {
let n = if kind == ParamKind::Int2 { 2 } else { 3 };
let dst = std::slice::from_raw_parts_mut(out as *mut c_int, n);
for (d, s) in dst.iter_mut().zip(a.iter()).take(n) {
*d = *s;
}
let _ = dim;
Ok(())
}
_ => err(),
},
ParamKind::Double => match v {
ParamValue::Double(d, _) => {
*(out as *mut f64) = d[0];
Ok(())
}
_ => err(),
},
ParamKind::Double2 | ParamKind::Double3 => match v {
ParamValue::Double(d, _) => {
let n = if kind == ParamKind::Double2 { 2 } else { 3 };
let dst = std::slice::from_raw_parts_mut(out as *mut f64, n);
for (dd, s) in dst.iter_mut().zip(d.iter()).take(n) {
*dd = *s;
}
Ok(())
}
_ => err(),
},
ParamKind::Bool => match v {
ParamValue::Bool(b) => {
*(out as *mut c_int) = *b as c_int;
Ok(())
}
_ => err(),
},
ParamKind::Choice => match v {
ParamValue::Choice(c) => {
*(out as *mut c_int) = *c;
Ok(())
}
_ => err(),
},
ParamKind::Rgb | ParamKind::Rgba => match v {
ParamValue::Color(c, _) => {
let n = if kind == ParamKind::Rgb { 3 } else { 4 };
let dst = std::slice::from_raw_parts_mut(out as *mut f64, n);
for (dd, s) in dst.iter_mut().zip(c.iter()).take(n) {
*dd = *s;
}
Ok(())
}
_ => err(),
},
ParamKind::Str | ParamKind::StrChoice => {
// 字符串走 ofx_param_get_string_impl(内驻指针)。
err()
}
}
}
}
/// 按 kind 从类型化 input 构造 ParamValue(数值类)。
fn read_value(kind: ParamKind, input: *const c_void) -> Option<ParamValue> {
unsafe {
match kind {
ParamKind::Int => Some(ParamValue::Int([*(input as *const c_int), 0, 0], 1)),
ParamKind::Int2 => {
let a = *(input as *const [c_int; 2]);
Some(ParamValue::Int([a[0], a[1], 0], 2))
}
ParamKind::Int3 => {
let a = *(input as *const [c_int; 3]);
Some(ParamValue::Int([a[0], a[1], a[2]], 3))
}
ParamKind::Double => Some(ParamValue::Double([*(input as *const f64), 0.0, 0.0], 1)),
ParamKind::Double2 => {
let a = *(input as *const [f64; 2]);
Some(ParamValue::Double([a[0], a[1], 0.0], 2))
}
ParamKind::Double3 => {
let a = *(input as *const [f64; 3]);
Some(ParamValue::Double([a[0], a[1], a[2]], 3))
}
ParamKind::Bool => Some(ParamValue::Bool(*(input as *const c_int) != 0)),
ParamKind::Choice => Some(ParamValue::Choice(*(input as *const c_int))),
ParamKind::Rgb => {
let a = *(input as *const [f64; 3]);
Some(ParamValue::Color([a[0], a[1], a[2], 0.0], 3))
}
ParamKind::Rgba => {
let a = *(input as *const [f64; 4]);
Some(ParamValue::Color([a[0], a[1], a[2], a[3]], 4))
}
ParamKind::Str | ParamKind::StrChoice => None,
}
}
}
/// 取"当前值"(实例期=当前值,describe 期=默认值)。
fn value_of(r: &ParamRef) -> ParamValue {
match r {
ParamRef::Instance(p) => p.get(),
ParamRef::Def(d) => d.default.clone(),
}
}
/// param → 所属 instance 的回写定位表(instanceChanged 用)。
/// `// TODO(instance)`:createInstance 后由宿主登记,destroy 时摘除。
static PARAM_OWNER: std::sync::LazyLock<Mutex<HashMap<usize, usize>>> =
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
/// 登记参数 → 实例(实例化路径调用)。
pub(crate) fn register_param_owner(param_props: usize, instance_props: usize) {
PARAM_OWNER
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(param_props, instance_props);
}
/// 摘除某实例的全部参数登记(destroy 路径调用)。
pub(crate) fn unregister_params_of(instance_props: usize) {
PARAM_OWNER
.lock()
.unwrap_or_else(|e| e.into_inner())
.retain(|_, owner| *owner != instance_props);
}
/// paramSetValue 成功后的 instanceChanged 触发(HS:
/// ofxhParam.cpp:1991-1994 `paramChangedByPlugin`)。未登记实例时
/// no-op(未绑定节点的场景本就 no-op)。
fn notify_changed(param_props: usize, name: &str) {
let owner = PARAM_OWNER
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&param_props)
.copied();
if let Some(inst_addr) = owner {
// instance 的 props 在偏移 0(句柄约定)。
let inst = unsafe { &*(inst_addr as *const Instance) };
crate::param::notify_instance_changed(inst, name, ChangeReason::PluginEdited);
}
}
/// 参数类型查询(C shim 调用):未知/非法句柄 → 0(dispatch 失败)。
#[no_mangle]
pub unsafe extern "C" fn ofx_param_kind_of(param: *mut c_void) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
match resolve_param(param) {
Ok(r) => match &r {
ParamRef::Instance(p) => p.def.kind().map_or(0, |k| k as c_int),
ParamRef::Def(d) => d.kind().map_or(0, |k| k as c_int),
},
Err(_) => 0,
}
}))
.unwrap_or(0)
}
/// 数值类 get(C shim 转发):`out` 为按 kind 的类型化数组。
#[no_mangle]
pub unsafe extern "C" fn ofx_param_get_impl(
param: *mut c_void,
kind: c_int,
out: *mut c_void,
) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_VALUE);
}
let k = ParamKind::from_i32(kind).ok_or(status::ERR_UNKNOWN)?;
let r = resolve_param(param)?;
write_value(&value_of(&r), k, out)?;
Ok(())
})
}
/// 字符串 get(内驻指针:指向实例存储 / 定义默认值;下次 set 前
/// 有效——与属性 suite 的字符串指针契约一致)。
#[no_mangle]
pub unsafe extern "C" fn ofx_param_get_string_impl(
param: *mut c_void,
out: *mut *mut c_char,
) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_VALUE);
}
let ptr: *const c_char = match resolve_param(param)? {
ParamRef::Instance(p) => p.string_ptr().ok_or(status::FAILED)?,
ParamRef::Def(d) => match &d.default {
ParamValue::String(s) | ParamValue::StrChoice(s) => s.as_ptr(),
_ => return Err(status::FAILED),
},
};
unsafe { *out = ptr as *mut c_char };
Ok(())
})
}
/// 数值类 set(C shim 转发):`input` 为按 kind 的类型化数组。
/// describe 期(ParamDef)→ BadHandle(HS: paramSetValue 的
/// verifyMagic 对 descriptor 失败)。
#[no_mangle]
pub unsafe extern "C" fn ofx_param_set_impl(
param: *mut c_void,
kind: c_int,
input: *const c_void,
) -> c_int {
caught(|| {
if input.is_null() {
return Err(status::ERR_VALUE);
}
let k = ParamKind::from_i32(kind).ok_or(status::ERR_UNKNOWN)?;
let value = read_value(k, input).ok_or(status::FAILED)?;
let (name, props_addr) = match resolve_param(param)? {
ParamRef::Instance(p) => {
p.set_ofx(value);
(p.def.name.clone(), &p.props as *const _ as usize)
}
ParamRef::Def(_) => return Err(status::ERR_BAD_HANDLE),
};
notify_changed(props_addr, &name);
Ok(())
})
}
/// 字符串 set。
#[no_mangle]
pub unsafe extern "C" fn ofx_param_set_string_impl(
param: *mut c_void,
input: *const c_char,
) -> c_int {
caught(|| {
if input.is_null() {
return Err(status::ERR_VALUE);
}
let s = CString::new(unsafe { CStr::from_ptr(input) }.to_bytes()).unwrap();
let (name, props_addr) = match resolve_param(param)? {
ParamRef::Instance(p) => {
p.set_ofx(ParamValue::String(s));
(p.def.name.clone(), &p.props as *const _ as usize)
}
ParamRef::Def(_) => return Err(status::ERR_BAD_HANDLE),
};
notify_changed(props_addr, &name);
Ok(())
})
}
/// derivative/integral:第 1 期无动画 → MissingHostFeature
/// (C shim 不消费 va_args 直接转发)。
#[no_mangle]
pub unsafe extern "C" fn ofx_param_missing_feature_impl(param: *mut c_void) -> c_int {
caught(|| {
// handle 合法性仍检查(空/坏句柄 → BadHandle)。
let _ = resolve_param(param)?;
Err(status::ERR_MISSING_HOST_FEATURE)
})
}
// ---- 非变长入口 ----------------------------------------------------------
/// paramDefine:describe 期定义参数(未定义类型/parametric →
/// Unsupported,HS:1704;重复名 → HS 允许,原样再建一条——宿主以
/// 首个为准,与 HS 的 map 覆盖语义一致)。
unsafe extern "C" fn param_define(
param_set: *mut c_void,
param_type: *const c_char,
name: *const c_char,
property_set: *mut *mut c_void,
) -> c_int {
caught(|| {
if property_set.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe { *property_set = std::ptr::null_mut() };
let t = unsafe { c_name(param_type)? };
let n = unsafe { c_name(name)? };
// 仅 describe 期(HS: SetDescriptor 专属,实例期 dynamic_cast
// 失败 → BadHandle)。
let desc = match resolve_param_set(param_set)? {
ParamSetRef::Descriptor(d) => d,
ParamSetRef::Instance(_) => return Err(status::ERR_BAD_HANDLE),
};
// 类型合法性(kind_of_type 之外还有无值类与 parametric)。
let valid = crate::param::kind_of_type(t).is_some()
|| matches!(
t,
crate::param::TYPE_BYTES
| crate::param::TYPE_CUSTOM
| crate::param::TYPE_PUSHBUTTON
| crate::param::TYPE_GROUP
| crate::param::TYPE_PAGE
);
if !valid || t == crate::param::TYPE_PARAMETRIC {
return Err(status::ERR_UNSUPPORTED);
}
let def = ParamDef::new(n, t);
desc.params.push(Box::new(def));
// 地址必须取自已入盒的对象(栈上临时变量在 push 后失效,
// 句柄会指向死内存——Box 负载地址在 Vec 重分配时稳定)。
let def = desc.params.last().expect("just pushed");
let addr = &def.props as *const _ as usize;
unsafe { *property_set = tag::make(addr as *const crate::property::PropertySet, tag::PARAM_DEF) };
Ok(())
})
}
/// paramGetHandle:按名查(未找到 → ErrUnknown,HS:1758)。
unsafe extern "C" fn param_get_handle(
param_set: *mut c_void,
name: *const c_char,
param: *mut *mut c_void,
property_set: *mut *mut c_void,
) -> c_int {
caught(|| {
if param.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe { *param = std::ptr::null_mut() };
let n = unsafe { c_name(name)? };
let handle = match resolve_param_set(param_set)? {
ParamSetRef::Descriptor(d) => {
let def = d.param(n).ok_or(status::ERR_UNKNOWN)?;
let addr = &def.props as *const _ as usize;
tag::make(addr as *const crate::property::PropertySet, tag::PARAM_DEF)
}
ParamSetRef::Instance(i) => {
let p = i.params.find(n).ok_or(status::ERR_UNKNOWN)?;
let addr = &p.props as *const _ as usize;
tag::make(addr as *const crate::property::PropertySet, tag::PARAM_INSTANCE)
}
};
unsafe { *param = handle };
if !property_set.is_null() {
// props 在偏移 0:param handle 与 props handle 同值。
unsafe { *property_set = handle };
}
Ok(())
})
}
/// paramSetGetPropertySet:param-set 的属性集(= effect handle 本体)。
unsafe extern "C" fn param_set_get_property_set(
param_set: *mut c_void,
property_set: *mut *mut c_void,
) -> c_int {
caught(|| {
if property_set.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let _ = resolve_param_set(param_set)?;
unsafe { *property_set = param_set };
Ok(())
})
}
/// paramGetPropertySet:param 的属性集(= param handle 本体)。
unsafe extern "C" fn param_get_property_set(
param: *mut c_void,
property_set: *mut *mut c_void,
) -> c_int {
caught(|| {
if property_set.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let _ = resolve_param(param)?;
unsafe { *property_set = param };
Ok(())
})
}
/// paramGetNumKeys:第 1 期无动画 → 恒 0。
unsafe extern "C" fn param_get_num_keys(param: *mut c_void, count: *mut c_int) -> c_int {
caught(|| {
let _ = resolve_param(param)?;
if count.is_null() {
return Err(status::ERR_VALUE);
}
unsafe { *count = 0 };
Ok(())
})
}
/// paramGetKeyTime:无关键帧 → BadIndex。
unsafe extern "C" fn param_get_key_time(param: *mut c_void, _index: c_int, _time: *mut c_double) -> c_int {
caught(|| {
let _ = resolve_param(param)?;
Err(status::ERR_BAD_INDEX)
})
}
/// paramGetKeyIndex:无关键帧 → BadIndex。
unsafe extern "C" fn param_get_key_index(
param: *mut c_void,
_time: c_double,
_direction: c_int,
_index: *mut c_int,
) -> c_int {
caught(|| {
let _ = resolve_param(param)?;
Err(status::ERR_BAD_INDEX)
})
}
/// paramDeleteKey / paramDeleteAllKeys:无关键帧 → OK no-op。
unsafe extern "C" fn param_delete_key(param: *mut c_void, _time: c_double) -> c_int {
caught(|| {
let _ = resolve_param(param)?;
Ok(())
})
}
unsafe extern "C" fn param_delete_all_keys(param: *mut c_void) -> c_int {
caught(|| {
let _ = resolve_param(param)?;
Ok(())
})
}
/// paramCopy:第 1 期无关键帧可拷 → OK no-op。
unsafe extern "C" fn param_copy(
dst: *mut c_void,
src: *mut c_void,
_dst_time: c_double,
_src_time: c_double,
_key_range: *const c_void,
) -> c_int {
caught(|| {
let _ = resolve_param(dst)?;
let _ = resolve_param(src)?;
Ok(())
})
}
/// paramEditBegin:进入编辑事务(undo 分组)。解析 param 句柄后经
/// PARAM_OWNER 定位所属实例并递增其事务深度;未登记实例时 no-op
/// (手工构造的实例没有宿主注册,edit 括号退化为无分组)。
unsafe extern "C" fn param_edit_begin(param: *mut c_void) -> c_int {
caught(|| {
let addr = match resolve_param(param)? {
ParamRef::Instance(p) => &p.props as *const _ as usize,
ParamRef::Def(_) => return Err(status::ERR_BAD_HANDLE),
};
if let Some(owner) = PARAM_OWNER
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&addr)
.copied()
{
let inst = unsafe { &*(owner as *const Instance) };
inst.edit_begin();
}
Ok(())
})
}
/// paramEditEnd:退出编辑事务;最外层结束时把累积的 multi 命令
/// redo 生效并释放(见 [`crate::instance::Instance::edit_end`])。
unsafe extern "C" fn param_edit_end(param: *mut c_void) -> c_int {
caught(|| {
let addr = match resolve_param(param)? {
ParamRef::Instance(p) => &p.props as *const _ as usize,
ParamRef::Def(_) => return Err(status::ERR_BAD_HANDLE),
};
if let Some(owner) = PARAM_OWNER
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&addr)
.copied()
{
let inst = unsafe { &*(owner as *const Instance) };
inst.edit_end();
}
Ok(())
})
}
extern "C" {
fn ofx_param_get_value_shim(param: *mut c_void, ...) -> c_int;
fn ofx_param_get_value_at_time_shim(param: *mut c_void, time: c_double, ...) -> c_int;
fn ofx_param_set_value_shim(param: *mut c_void, ...) -> c_int;
fn ofx_param_set_value_at_time_shim(param: *mut c_void, time: c_double, ...) -> c_int;
fn ofx_param_get_derivative_shim(param: *mut c_void, time: c_double, ...) -> c_int;
fn ofx_param_get_integral_shim(
param: *mut c_void,
time1: c_double,
time2: c_double,
...
) -> c_int;
}
/// 静态函数表实例。
pub fn suite_v1() -> &'static ParameterSuiteV1 {
static SUITE: std::sync::OnceLock<ParameterSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| ParameterSuiteV1 {
param_define: param_define,
param_get_handle: param_get_handle,
param_set_get_property_set: param_set_get_property_set,
param_get_property_set: param_get_property_set,
param_get_value: ofx_param_get_value_shim,
param_get_value_at_time: ofx_param_get_value_at_time_shim,
param_get_derivative: ofx_param_get_derivative_shim,
param_get_integral: ofx_param_get_integral_shim,
param_set_value: ofx_param_set_value_shim,
param_set_value_at_time: ofx_param_set_value_at_time_shim,
param_get_num_keys: param_get_num_keys,
param_get_key_time: param_get_key_time,
param_get_key_index: param_get_key_index,
param_delete_key: param_delete_key,
param_delete_all_keys: param_delete_all_keys,
param_copy: param_copy,
param_edit_begin: param_edit_begin,
param_edit_end: param_edit_end,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use std::sync::Arc;
use crate::host::Plugin;
use crate::param::{ParamInstance, ParamSetInstance};
use crate::property::PropertySet;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
fn descriptor_handle(d: &EffectDescriptor) -> *mut c_void {
tag::make(&d.props as *const crate::property::PropertySet, tag::DESCRIPTOR)
}
/// 假插件(host::Plugin 的构造只为拿 Arc 喂给 Instance;describe
/// 之外的字段不被本测试触碰)。
fn dummy_plugin(descriptor: EffectDescriptor) -> Arc<Plugin> {
unsafe extern "C" fn dummy_entry(
_: *const c_char,
_: *const c_void,
_: *mut c_void,
_: *mut c_void,
) -> c_int {
status::OK
}
Arc::new(Plugin {
identifier: "test.plugin".into(),
version: (1, 0),
bundle_path: std::path::PathBuf::new(),
contexts: vec![],
descriptor,
lib: std::ptr::null_mut(),
entry: dummy_entry,
ofx_plugin: std::ptr::null_mut(),
})
}
fn instance_handle(i: &Instance) -> *mut c_void {
tag::make(&i.props as *const crate::property::PropertySet, tag::INSTANCE)
}
/// 用 paramDefine 建一个实例(describe 产物 → createInstance)。
fn make_instance() -> (Arc<Instance>, *mut c_void) {
let mut desc = EffectDescriptor::new();
let s = suite_v1();
let dhandle = descriptor_handle(&desc);
unsafe {
for (t, n) in [
("OfxParamTypeInteger", "gain"),
("OfxParamTypeDouble", "opacity"),
("OfxParamTypeDouble2D", "pos"),
("OfxParamTypeRGB", "color"),
("OfxParamTypeBoolean", "enabled"),
("OfxParamTypeString", "label"),
] {
let t = cs(t);
let n = cs(n);
let mut ph: *mut c_void = std::ptr::null_mut();
assert_eq!((s.param_define)(dhandle, t.as_ptr(), n.as_ptr(), &mut ph), 0);
}
}
let params = ParamSetInstance {
params: desc
.params
.iter()
.map(|d| Box::new(ParamInstance::from_def((**d).clone())))
.collect(),
};
let plugin = dummy_plugin(desc);
let inst = Arc::new(Instance {
props: PropertySet::new(),
plugin,
context: "OfxImageEffectContextFilter".into(),
params,
clips: vec![],
node_identity: std::sync::atomic::AtomicUsize::new(0),
destroyed: std::sync::atomic::AtomicBool::new(false),
sequence_range: std::sync::Mutex::new(None),
progress_cb: std::sync::Mutex::new(None),
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
});
(inst.clone(), instance_handle(&inst))
}
#[test]
fn describe_define_and_get() {
let mut desc = EffectDescriptor::new();
let s = suite_v1();
let handle = descriptor_handle(&desc);
let t = cs("OfxParamTypeDouble");
let n = cs("opacity");
let mut ph: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.param_define)(handle, t.as_ptr(), n.as_ptr(), &mut ph), 0);
}
assert!(!ph.is_null());
assert_eq!(tag::kind(ph), tag::PARAM_DEF);
// describe 期 paramGetValue:默认值 0.0。
let mut v = 99.0;
unsafe {
assert_eq!((s.param_get_value)(ph, &mut v), 0);
}
assert_eq!(v, 0.0);
// describe 期 set → BadHandle(HS: verifyMagic 对 descriptor 失败)。
unsafe {
assert_eq!((s.param_set_value)(ph, 42.0), status::ERR_BAD_HANDLE);
}
// paramGetHandle 按名取(props handle 同值)。
let mut ph2: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!(
(s.param_get_handle)(handle, n.as_ptr(), &mut ph2, std::ptr::null_mut()),
0
);
assert_eq!(ph, ph2);
}
// 未找到 → ErrUnknown(HS:1758)。
let nope = cs("nope");
unsafe {
assert_eq!(
(s.param_get_handle)(handle, nope.as_ptr(), &mut ph2, std::ptr::null_mut()),
status::ERR_UNKNOWN
);
}
// 未定义类型 / parametric → ErrUnsupported(HS:1704)。
let bad = cs("OfxParamTypeBogus");
let par = cs("OfxParamTypeParametric");
unsafe {
assert_eq!(
(s.param_define)(handle, bad.as_ptr(), n.as_ptr(), &mut ph2),
status::ERR_UNSUPPORTED
);
assert_eq!(
(s.param_define)(handle, par.as_ptr(), n.as_ptr(), &mut ph2),
status::ERR_UNSUPPORTED
);
}
// 实例期 paramDefine → BadHandle。
let (_inst, ih) = make_instance();
unsafe {
assert_eq!(
(s.param_define)(ih, t.as_ptr(), n.as_ptr(), &mut ph2),
status::ERR_BAD_HANDLE
);
}
}
#[test]
fn instance_get_set_variadic_roundtrip() {
let (_inst, ih) = make_instance();
let s = suite_v1();
// 取各参数 handle。
let mut gain: *mut c_void = std::ptr::null_mut();
let mut opacity: *mut c_void = std::ptr::null_mut();
let mut pos: *mut c_void = std::ptr::null_mut();
let mut color: *mut c_void = std::ptr::null_mut();
let mut enabled: *mut c_void = std::ptr::null_mut();
let mut label: *mut c_void = std::ptr::null_mut();
unsafe {
for (n, out) in [
("gain", &mut gain),
("opacity", &mut opacity),
("pos", &mut pos),
("color", &mut color),
("enabled", &mut enabled),
("label", &mut label),
] {
let n = cs(n);
let r = (s.param_get_handle)(ih, n.as_ptr(), out, std::ptr::null_mut());
assert_eq!(r, 0, "gethandle {} failed: {}", n.to_str().unwrap(), r);
assert_eq!(tag::kind(*out), tag::PARAM_INSTANCE);
}
}
// Integer set/get。
unsafe {
assert_eq!((s.param_set_value)(gain, 7), 0);
}
let mut v = 0;
unsafe {
assert_eq!((s.param_get_value)(gain, &mut v), 0);
}
assert_eq!(v, 7);
// Double2D。
unsafe {
assert_eq!((s.param_set_value)(pos, 1.5, -2.5), 0);
}
let (mut x, mut y) = (0.0, 0.0);
unsafe {
assert_eq!((s.param_get_value)(pos, &mut x, &mut y), 0);
}
assert_eq!((x, y), (1.5, -2.5));
// RGB。
unsafe {
assert_eq!((s.param_set_value)(color, 0.1, 0.2, 0.3), 0);
}
let (mut r, mut g, mut b) = (0.0, 0.0, 0.0);
unsafe {
assert_eq!((s.param_get_value)(color, &mut r, &mut g, &mut b), 0);
}
assert_eq!((r, g, b), (0.1, 0.2, 0.3));
// Boolean(int 0/1)。
unsafe {
assert_eq!((s.param_set_value)(enabled, 1), 0);
}
let mut e = 0;
unsafe {
assert_eq!((s.param_get_value)(enabled, &mut e), 0);
}
assert_eq!(e, 1);
// String:set 后 get 返回内驻指针。
let hello = cs("hello");
unsafe {
assert_eq!((s.param_set_value)(label, hello.as_ptr()), 0);
}
let mut p: *mut c_char = std::ptr::null_mut();
unsafe {
assert_eq!((s.param_get_value)(label, &mut p), 0);
assert_eq!(CStr::from_ptr(p).to_bytes(), b"hello");
}
// AtTime == 当前值(无动画)。
let mut v2 = 0.0;
unsafe {
assert_eq!((s.param_get_value_at_time)(opacity, 12.0, &mut v2), 0);
assert_eq!(v2, 0.0);
assert_eq!((s.param_set_value_at_time)(opacity, 12.0, 0.75), 0);
}
let mut v3 = 0.0;
unsafe {
assert_eq!((s.param_get_value)(opacity, &mut v3), 0);
}
assert_eq!(v3, 0.75);
// keys:恒 0 / BadIndex。
let mut nkeys = -1;
unsafe {
assert_eq!((s.param_get_num_keys)(opacity, &mut nkeys), 0);
assert_eq!(nkeys, 0);
let mut kt = 0.0;
assert_eq!((s.param_get_key_time)(opacity, 0, &mut kt), status::ERR_BAD_INDEX);
}
// derivative/integral → MissingHostFeature。
let mut dv = 0.0;
unsafe {
assert_eq!(
(s.param_get_derivative)(opacity, 1.0, &mut dv),
status::ERR_MISSING_HOST_FEATURE
);
}
// editBegin/End、deleteKey(s)、copy:no-op OK。
unsafe {
assert_eq!((s.param_edit_begin)(opacity), 0);
assert_eq!((s.param_edit_end)(opacity), 0);
assert_eq!((s.param_delete_key)(opacity, 1.0), 0);
assert_eq!((s.param_delete_all_keys)(opacity), 0);
assert_eq!((s.param_copy)(opacity, opacity, 1.0, 1.0, std::ptr::null()), 0);
}
}
}
+168
View File
@@ -0,0 +1,168 @@
// 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/>.
//! OfxProgressSuite v1/v2:进度 → 当前渲染的
//! [`crate::progress::ProgressReporter`]。
//!
//! 定位方式:TLS(render 驱动在调用插件 action 前
//! [`set_current`],action 返回后清除)——进度只在渲染期内有意义,
//! 不设全局状态(progress.rs 文档)。
//! 语义:update 返回 false(取消)→ kOfxStatReplyNo
//! (HS: ofxhImageEffect.cpp gProgressSuite);无报告器(渲染外
//! 调用)→ 静默 OK。start/end 为括号,OK。
use std::ffi::{c_char, c_double, c_int, c_void};
use crate::progress::ProgressReporter;
use crate::suites::status;
thread_local! {
static CURRENT: std::cell::RefCell<Option<ProgressReporter>> = const { std::cell::RefCell::new(None) };
}
/// 设置/清除当前报告器(render 驱动调用;公开:render 驱动在
/// host 层,测试也直接注入)。
pub fn set_current(reporter: Option<ProgressReporter>) {
CURRENT.with(|c| *c.borrow_mut() = reporter);
}
/// 读取当前报告器的取消状态(image effect suite 的 abort 用)。
pub(crate) fn is_cancelled() -> bool {
CURRENT.with(|c| c.borrow().as_ref().is_some_and(|r| r.is_cancelled()))
}
/// 进度更新:取消 → kOfxStatReplyNo;无报告器/未取消 → OK。
fn progress_update(progress: f64) -> c_int {
let cancelled = CURRENT.with(|c| match c.borrow().as_ref() {
Some(r) => !r.update(progress),
None => false,
});
if cancelled {
status::REPLY_NO
} else {
status::OK
}
}
/// 公共入口模板:panic 兜底。
fn caught(f: impl FnOnce() -> c_int) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or(status::FAILED)
}
/// 函数表布局(OfxProgressSuiteV1)。
#[repr(C)]
pub struct ProgressSuiteV1 {
/// progressStart
pub start: unsafe extern "C" fn(*mut c_void, *const c_char) -> c_int,
/// progressUpdate(返回非 kOfxStatOK 即取消)
pub update: unsafe extern "C" fn(*mut c_void, c_double) -> c_int,
/// progressEnd
pub end: unsafe extern "C" fn(*mut c_void) -> c_int,
}
/// 函数表布局(OfxProgressSuiteV2:start 带 label + message)。
#[repr(C)]
pub struct ProgressSuiteV2 {
/// progressStart
pub start: unsafe extern "C" fn(*mut c_void, *const c_char, *const c_char) -> c_int,
/// progressUpdate
pub update: unsafe extern "C" fn(*mut c_void, c_double) -> c_int,
/// progressEnd
pub end: unsafe extern "C" fn(*mut c_void) -> c_int,
}
/// progressStart:括号起点,OK(label/message 留作未来 UI 展示,
/// 第 1 期不建模)。
unsafe extern "C" fn progress_start_v1(_handle: *mut c_void, _label: *const c_char) -> c_int {
caught(|| status::OK)
}
unsafe extern "C" fn progress_update_v1(_handle: *mut c_void, progress: c_double) -> c_int {
caught(|| progress_update(progress))
}
unsafe extern "C" fn progress_end_v1(_handle: *mut c_void) -> c_int {
caught(|| status::OK)
}
unsafe extern "C" fn progress_start_v2(
_handle: *mut c_void,
_label: *const c_char,
_message: *const c_char,
) -> c_int {
caught(|| status::OK)
}
unsafe extern "C" fn progress_update_v2(_handle: *mut c_void, progress: c_double) -> c_int {
caught(|| progress_update(progress))
}
unsafe extern "C" fn progress_end_v2(_handle: *mut c_void) -> c_int {
caught(|| status::OK)
}
/// 静态函数表实例(v1)。
pub fn suite_v1() -> &'static ProgressSuiteV1 {
static SUITE: std::sync::OnceLock<ProgressSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| ProgressSuiteV1 {
start: progress_start_v1,
update: progress_update_v1,
end: progress_end_v1,
})
}
/// 静态函数表实例(v2)。
pub fn suite_v2() -> &'static ProgressSuiteV2 {
static SUITE: std::sync::OnceLock<ProgressSuiteV2> = std::sync::OnceLock::new();
SUITE.get_or_init(|| ProgressSuiteV2 {
start: progress_start_v2,
update: progress_update_v2,
end: progress_end_v2,
})
}
#[cfg(test)]
mod tests {
use super::*;
/// 取消型出口(头文件契约:非 0 = 中止)。
unsafe extern "C" fn cancel_cb(_p: f64, _userdata: *mut c_void) -> c_int {
1
}
/// 静默:update 恒 OK;带取消回调:update → REPLY_NO 且 abort
/// 查询为真(粘滞)。
#[test]
fn update_cancel_and_abort() {
// 无报告器(渲染外):静默 OK。
set_current(None);
let s = suite_v1();
unsafe {
assert_eq!((s.update)(std::ptr::null_mut(), 0.5), status::OK);
assert_eq!((s.start)(std::ptr::null_mut(), std::ptr::null()), status::OK);
assert_eq!((s.end)(std::ptr::null_mut()), status::OK);
}
// 带取消回调:update → REPLY_NO;is_cancelled 为真。
set_current(Some(unsafe { ProgressReporter::new(cancel_cb, std::ptr::null_mut()) }));
unsafe {
assert_eq!((s.update)(std::ptr::null_mut(), 0.5), status::REPLY_NO);
}
assert!(is_cancelled());
set_current(None);
assert!(!is_cancelled());
}
}
+619
View File
@@ -0,0 +1,619 @@
// 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/>.
//! OfxPropertySuite v1:属性读写。
//!
//! 语义逐条对照 HS: ofxhPropertySuite.cpp:
//! - 读未定义属性 / 类型不符 → kOfxStatErrUnknown(HS: propGet 的
//! `fetchTypedProperty` 失败路径,ofxhPropertySuite.cpp:787-794);
//! - 越界读写 → kOfxStatErrBadIndex(HS: `getValueRaw`/
//! `setValue`,ofxhPropertySuite.cpp:257/284);
//! - propGetN 拷贝 min(count, dimension) 个元素,不报错
//! (HS: `getValueNRaw`,ofxhPropertySuite.cpp:271-280);
//! - propSetN 维度变化时整体替换:本 crate 的属性维度始终跟随数组
//! 长度(HS 对固定维度属性 count > dimension 会返回
//! kOfxStatErrBadIndex,ofxhPropertySuite.cpp:299——本 crate 不区分
//! 固定/可变维度,如实更宽容;三个系统 bundle 均以正确 count 调用,
//! 无行为分歧);
//! - 只读属性:SDK 无 kOfxStatErrReadOnly,HostSupport 也不在 suite
//! 层拦截写(`_pluginReadOnly` 仅供宿主内部逻辑查询)——本实现
//! 如实不拦截;若日后需要,在 PropertySet 增加只读标记再评。
//! - propReset:HostSupport 恢复到 define 时默认值
//! (ofxhPropertySuite.cpp:330);本 crate 不保存默认值快照,
//! 第 1 期明确返回 kOfxStatErrUnsupported(真话比静默 no-op 安全)。
use std::ffi::{c_char, c_double, c_int, c_void, CStr, CString};
use crate::property::{Property, PropertySet, Value};
use crate::suites::status;
/// 函数表布局(与 SDK `OfxPropertySuiteV1` 逐字段一致)。
#[repr(C)]
pub struct PropertySuiteV1 {
/// propSetPointer
pub set_pointer: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_void) -> c_int,
/// propSetString
pub set_string: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const c_char) -> c_int,
/// propSetDouble
pub set_double: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, c_double) -> c_int,
/// propSetInt
pub set_int: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, c_int) -> c_int,
/// propSetPointerN
pub set_pointer_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const *mut c_void) -> c_int,
/// propSetStringN
pub set_string_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const *const c_char) -> c_int,
/// propSetDoubleN
pub set_double_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const c_double) -> c_int,
/// propSetIntN
pub set_int_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const c_int) -> c_int,
/// propGetPointer
pub get_pointer: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut *mut c_void) -> c_int,
/// propGetString
pub get_string: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut *mut c_char) -> c_int,
/// propGetDouble
pub get_double: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_double) -> c_int,
/// propGetInt
pub get_int: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_int) -> c_int,
/// propGetPointerN
pub get_pointer_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut *mut c_void) -> c_int,
/// propGetStringN
pub get_string_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut *mut c_char) -> c_int,
/// propGetDoubleN
pub get_double_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_double) -> c_int,
/// propGetIntN
pub get_int_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_int) -> c_int,
/// propReset
pub reset: unsafe extern "C" fn(*mut c_void, *const c_char) -> c_int,
/// propGetDimension
pub get_dimension: unsafe extern "C" fn(*mut c_void, *const c_char, *mut c_int) -> c_int,
}
/// 属性值类别(suite 层的类型检查;对应 HS 的 `Property::TypeEnum`)。
#[derive(Clone, Copy, PartialEq, Eq)]
enum Kind {
Int,
Double,
Str,
Pointer,
}
impl Kind {
fn of(v: &Value) -> Kind {
match v {
Value::Int(_) => Kind::Int,
Value::Double(_) => Kind::Double,
Value::String(_) => Kind::Str,
Value::Pointer(_) => Kind::Pointer,
}
}
}
/// 属性 suite 公共入口模板:解句柄 + panic 兜底。
///
/// 句柄按 [`crate::suites::tag`] 约定:低 3 位是对象种类标签,地址
/// 即对象 props 字段(偏移 0);剥标签后可直接当 PropertySet 用。
/// 裸 PropertySet 指针(标签 0,宿主内部/测试直传)原样通过。
///
/// `# Safety`:`handle` 必须指向活的 `PropertySet` 或已注册对象
/// (suite 生命周期契约:宿主对象先于 suite 调用创建,后于全部调用
/// 销毁)。
unsafe fn caught(handle: *mut c_void, f: impl FnOnce(&PropertySet) -> Result<(), c_int>) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if handle.is_null() {
return status::ERR_BAD_HANDLE;
}
let set = unsafe { &*crate::suites::tag::strip(handle) };
f(set).map_or_else(|code| code, |()| status::OK)
}))
.unwrap_or(status::FAILED)
}
/// 属性名:空指针 / 非 UTF-8 → kOfxStatErrValue(防御性;HostSupport
/// 直接解引用会崩)。
///
/// `# Safety`:`name` 必须是有效的 NUL 结尾 C 字符串(插件契约)。
unsafe fn c_name<'a>(name: *const c_char) -> Result<&'a str, c_int> {
if name.is_null() {
return Err(status::ERR_VALUE);
}
unsafe { CStr::from_ptr(name) }
.to_str()
.map_err(|_| status::ERR_VALUE)
}
/// 越界防护:`index` 转 usize(负数 → 越界错误)。
fn idx(index: c_int) -> Result<usize, c_int> {
usize::try_from(index).map_err(|_| status::ERR_BAD_INDEX)
}
/// 先类型后索引的取值(HS 顺序:`fetchTypedProperty` 在
/// `getValueRaw` 之前,ofxhPropertySuite.cpp:787/794/257)。
fn get_value<'a>(props: &'a [Property], name: &str, index: c_int, kind: Kind) -> Result<&'a Value, c_int> {
let p = props.iter().find(|p| p.name == name).ok_or(status::ERR_UNKNOWN)?;
// 首元素代理整条属性的类型(宿主只定义同构数组;HS 是定义期
// 固定类型,等价)。
let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?;
if Kind::of(probe) != kind {
return Err(status::ERR_UNKNOWN);
}
p.values.get(idx(index)?).ok_or(status::ERR_BAD_INDEX)
}
// ---- propSet(单元素)-----------------------------------------------------
/// propSet 通用实现:类型不符/未定义 → Unknown;越界 → BadIndex。
fn set_value(set: &PropertySet, name: &str, index: c_int, value: Value) -> Result<(), c_int> {
set.with_locked(|props| {
let p = props.iter_mut().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::of(&value) {
return Err(status::ERR_UNKNOWN);
}
// HS `setValue` 允许 index == size 时追加(ofxhPropertySuite.cpp:284);
// 本 crate 维度固定语义(扩容只能经 define)——越界一律 BadIndex。
let slot = p.values.get_mut(idx(index)?).ok_or(status::ERR_BAD_INDEX)?;
*slot = value;
Ok(())
})
}
unsafe extern "C" fn prop_set_pointer(
handle: *mut c_void,
name: *const c_char,
index: c_int,
value: *mut c_void,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
set_value(set, name, index, Value::Pointer(value))
})
}
}
unsafe extern "C" fn prop_set_string(
handle: *mut c_void,
name: *const c_char,
index: c_int,
value: *const c_char,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if value.is_null() {
return Err(status::ERR_VALUE);
}
// C 语义:截断到首个 NUL;CString::new 不会失败
//(from_ptr 已保证 NUL 结尾)。
let s = CString::new(CStr::from_ptr(value).to_bytes()).unwrap();
set_value(set, name, index, Value::String(s))
})
}
}
unsafe extern "C" fn prop_set_double(
handle: *mut c_void,
name: *const c_char,
index: c_int,
value: c_double,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
set_value(set, name, index, Value::Double(value))
})
}
}
unsafe extern "C" fn prop_set_int(
handle: *mut c_void,
name: *const c_char,
index: c_int,
value: c_int,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
set_value(set, name, index, Value::Int(value))
})
}
}
// ---- propSetN(批量)------------------------------------------------------
/// 从 C 数组读出 `count` 个元素并整体写入;count != 现有维度时
/// 整体替换(HS `setValueN` 的 resize 语义,ofxhPropertySuite.cpp:299-311)。
fn set_values(set: &PropertySet, name: &str, count: c_int, values: Vec<Value>) -> Result<(), c_int> {
set.with_locked(|props| {
let p = props.iter_mut().find(|p| p.name == name).ok_or(status::ERR_UNKNOWN)?;
// count == 0 时无类型可探(HS 仍会做 fetchTypedProperty)。
if let Some(first) = p.values.first() {
if let Some(v) = values.first() {
if Kind::of(first) != Kind::of(v) {
return Err(status::ERR_UNKNOWN);
}
}
}
let count = idx(count)?;
if count != p.values.len() {
// 维度变化:整体替换(HS 是 resize + 逐位写,等价)。
p.values = values;
} else {
for (i, v) in values.into_iter().enumerate() {
p.values[i] = v;
}
}
Ok(())
})
}
unsafe extern "C" fn prop_set_pointer_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
values: *const *mut c_void,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && values.is_null() {
return Err(status::ERR_VALUE);
}
let vals = (0..idx(count)?)
.map(|i| Value::Pointer(*values.add(i)))
.collect();
set_values(set, name, count, vals)
})
}
}
unsafe extern "C" fn prop_set_string_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
values: *const *const c_char,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && values.is_null() {
return Err(status::ERR_VALUE);
}
let mut vals = Vec::with_capacity(count.max(0) as usize);
for i in 0..idx(count)? {
let v = *values.add(i);
if v.is_null() {
return Err(status::ERR_VALUE);
}
vals.push(Value::String(
CString::new(CStr::from_ptr(v).to_bytes()).unwrap(),
));
}
set_values(set, name, count, vals)
})
}
}
unsafe extern "C" fn prop_set_double_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
values: *const c_double,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && values.is_null() {
return Err(status::ERR_VALUE);
}
let vals = (0..idx(count)?)
.map(|i| Value::Double(*values.add(i)))
.collect();
set_values(set, name, count, vals)
})
}
}
unsafe extern "C" fn prop_set_int_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
values: *const c_int,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && values.is_null() {
return Err(status::ERR_VALUE);
}
let vals = (0..idx(count)?)
.map(|i| Value::Int(*values.add(i)))
.collect();
set_values(set, name, count, vals)
})
}
}
// ---- propGet(单元素)-----------------------------------------------------
/// 非字符串标量读取(克隆值写出;字符串走内驻指针路径)。
fn get_scalar(set: &PropertySet, name: &str, index: c_int, kind: Kind) -> Result<Value, c_int> {
set.with_locked(|props| get_value(props, name, index, kind).cloned())
}
unsafe extern "C" fn prop_get_pointer(
handle: *mut c_void,
name: *const c_char,
index: c_int,
out: *mut *mut c_void,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if out.is_null() {
return Err(status::ERR_VALUE);
}
let v = get_scalar(set, name, index, Kind::Pointer)?;
match v {
Value::Pointer(p) => {
*out = p;
Ok(())
}
_ => Err(status::FAILED),
}
})
}
}
/// propGetString:写**内驻** C 串指针(OFX 契约:指向宿主内部存储,
/// 属性被下次修改前有效;克隆的 CString 指针会悬垂,绝不能给插件)。
fn get_string(set: &PropertySet, name: &str, index: c_int) -> Result<*mut c_char, c_int> {
set.with_locked(|props| {
match get_value(props, name, index, Kind::Str)? {
Value::String(s) => Ok(s.as_ptr() as *mut c_char),
_ => Err(status::FAILED),
}
})
}
unsafe extern "C" fn prop_get_string(
handle: *mut c_void,
name: *const c_char,
index: c_int,
out: *mut *mut c_char,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if out.is_null() {
return Err(status::ERR_VALUE);
}
*out = get_string(set, name, index)?;
Ok(())
})
}
}
unsafe extern "C" fn prop_get_double(
handle: *mut c_void,
name: *const c_char,
index: c_int,
out: *mut c_double,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if out.is_null() {
return Err(status::ERR_VALUE);
}
let v = get_scalar(set, name, index, Kind::Double)?;
match v {
Value::Double(d) => {
*out = d;
Ok(())
}
_ => Err(status::FAILED),
}
})
}
}
unsafe extern "C" fn prop_get_int(
handle: *mut c_void,
name: *const c_char,
index: c_int,
out: *mut c_int,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if out.is_null() {
return Err(status::ERR_VALUE);
}
let v = get_scalar(set, name, index, Kind::Int)?;
match v {
Value::Int(i) => {
*out = i;
Ok(())
}
_ => Err(status::FAILED),
}
})
}
}
// ---- propGetN(批量)------------------------------------------------------
/// 批量读取:拷贝 min(count, dimension) 个(HS `getValueNRaw`,
/// ofxhPropertySuite.cpp:271-280,不报错)。
fn get_n(set: &PropertySet, name: &str, count: c_int, kind: Kind, write: impl Fn(&Value, usize)) -> Result<(), c_int> {
set.with_locked(|props| {
let p = props.iter().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 {
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);
}
Ok(())
})
}
unsafe extern "C" fn prop_get_pointer_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
out: *mut *mut c_void,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && out.is_null() {
return Err(status::ERR_VALUE);
}
get_n(set, name, count, Kind::Pointer, |v, i| match v {
Value::Pointer(p) => *out.add(i) = *p,
_ => {}
})
})
}
}
unsafe extern "C" fn prop_get_string_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
out: *mut *mut c_char,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && out.is_null() {
return Err(status::ERR_VALUE);
}
get_n(set, name, count, Kind::Str, |v, i| match v {
Value::String(s) => *out.add(i) = s.as_ptr() as *mut c_char,
_ => {}
})
})
}
}
unsafe extern "C" fn prop_get_double_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
out: *mut c_double,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && out.is_null() {
return Err(status::ERR_VALUE);
}
get_n(set, name, count, Kind::Double, |v, i| match v {
Value::Double(d) => *out.add(i) = *d,
_ => {}
})
})
}
}
unsafe extern "C" fn prop_get_int_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
out: *mut c_int,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && out.is_null() {
return Err(status::ERR_VALUE);
}
get_n(set, name, count, Kind::Int, |v, i| match v {
Value::Int(d) => *out.add(i) = *d,
_ => {}
})
})
}
}
// ---- propReset / propGetDimension -----------------------------------------
unsafe extern "C" fn prop_reset(handle: *mut c_void, name: *const c_char) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
// HS `reset()` 恢复到 define 时的默认值
// (ofxhPropertySuite.cpp:330);本 crate 不保存默认值快照,
// 第 1 期明确不支持(见模块文档)。
let _ = (set, name);
Err(status::ERR_UNSUPPORTED)
})
}
}
unsafe extern "C" fn prop_get_dimension(
handle: *mut c_void,
name: *const c_char,
out: *mut c_int,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if out.is_null() {
return Err(status::ERR_VALUE);
}
// HS: fetchProperty 失败(未定义)→ Unknown(ofxhPropertySuite.cpp:992)。
let dim = set.dimension(name);
if dim == 0 {
return Err(status::ERR_UNKNOWN);
}
*out = dim as c_int;
Ok(())
})
}
}
/// 静态函数表实例(fetch_suite 返回其地址)。
pub fn suite_v1() -> &'static PropertySuiteV1 {
static SUITE: std::sync::OnceLock<PropertySuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| PropertySuiteV1 {
set_pointer: prop_set_pointer,
set_string: prop_set_string,
set_double: prop_set_double,
set_int: prop_set_int,
set_pointer_n: prop_set_pointer_n,
set_string_n: prop_set_string_n,
set_double_n: prop_set_double_n,
set_int_n: prop_set_int_n,
get_pointer: prop_get_pointer,
get_string: prop_get_string,
get_double: prop_get_double,
get_int: prop_get_int,
get_pointer_n: prop_get_pointer_n,
get_string_n: prop_get_string_n,
get_double_n: prop_get_double_n,
get_int_n: prop_get_int_n,
reset: prop_reset,
get_dimension: prop_get_dimension,
})
}
+141
View File
@@ -0,0 +1,141 @@
// 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/>.
//! OfxTimeLineSuite v1:时间查询转发到当前渲染上下文
//! ([`crate::suites::RenderCtx`],frame range 来自 clip 桥)。
//! 参照 HS: ofxhImageEffect.cpp gTimelineSuite。
//!
//! 无渲染上下文(渲染外调用)→ 时间 0 / 时间域 (0,0) 的 headless
//! 默认;gotoTime 第 1 期无时间线驱动 → OK no-op(渲染时间由驱动
//! 固定,见 [`crate::suites::set_render_ctx`])。
use std::ffi::{c_double, c_int, c_void};
use crate::suites::{render_ctx, status};
/// 函数表布局(OfxTimeLineSuiteV1)。
#[repr(C)]
pub struct TimeLineSuiteV1 {
/// getTime
pub get_time: unsafe extern "C" fn(*mut c_void, *mut c_double) -> c_int,
/// gotoTime
pub goto_time: unsafe extern "C" fn(*mut c_void, c_double) -> c_int,
/// getTimeBounds
pub get_time_bounds: unsafe extern "C" fn(*mut c_void, *mut c_double, *mut c_double) -> c_int,
}
/// 公共入口模板:handle 空检查 + panic 兜底。
unsafe fn caught(handle: *mut c_void, f: impl FnOnce() -> c_int) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if handle.is_null() {
return status::ERR_BAD_HANDLE;
}
f()
}))
.unwrap_or(status::FAILED)
}
unsafe extern "C" fn timeline_get_time(handle: *mut c_void, time: *mut c_double) -> c_int {
unsafe {
caught(handle, || {
if time.is_null() {
return status::ERR_VALUE;
}
// 渲染上下文缺省 → 0(headless 默认)。
*time = render_ctx().map_or(0.0, |c| c.time);
status::OK
})
}
}
/// gotoTime:第 1 期宿主不随插件移动时间线(渲染时间由驱动固定),
/// OK no-op。
unsafe extern "C" fn timeline_goto_time(handle: *mut c_void, _time: c_double) -> c_int {
unsafe { caught(handle, || status::OK) }
}
unsafe extern "C" fn timeline_get_time_bounds(
handle: *mut c_void,
min: *mut c_double,
max: *mut c_double,
) -> c_int {
unsafe {
caught(handle, || {
if min.is_null() || max.is_null() {
return status::ERR_VALUE;
}
let r = render_ctx().map_or((0.0, 0.0), |c| (c.range.min, c.range.max));
*min = r.0;
*max = r.1;
status::OK
})
}
}
/// 静态函数表实例。
pub fn suite_v1() -> &'static TimeLineSuiteV1 {
static SUITE: std::sync::OnceLock<TimeLineSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| TimeLineSuiteV1 {
get_time: timeline_get_time,
goto_time: timeline_goto_time,
get_time_bounds: timeline_get_time_bounds,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instance::{OfxRangeD, RenderScale};
use crate::suites::{RenderCtx, set_render_ctx};
#[test]
fn queries_forward_to_render_ctx() {
let s = suite_v1();
let handle = 0x10usize as *mut c_void;
let mut t = 0.0;
let mut min = 0.0;
let mut max = 0.0;
// 无上下文:0 / (0,0)。
unsafe {
assert_eq!((s.get_time)(handle, &mut t), status::OK);
assert_eq!(t, 0.0);
assert_eq!((s.get_time_bounds)(handle, &mut min, &mut max), status::OK);
assert_eq!((min, max), (0.0, 0.0));
}
// 有上下文:转发。
set_render_ctx(Some(RenderCtx {
time: 42.5,
scale: RenderScale { x: 1.0, y: 1.0 },
range: OfxRangeD { min: 10.0, max: 200.0 },
}));
unsafe {
assert_eq!((s.get_time)(handle, &mut t), status::OK);
assert_eq!(t, 42.5);
assert_eq!((s.get_time_bounds)(handle, &mut min, &mut max), status::OK);
assert_eq!((min, max), (10.0, 200.0));
assert_eq!((s.goto_time)(handle, 99.0), status::OK);
}
set_render_ctx(None);
// 空 handle / 空 out。
unsafe {
assert_eq!((s.get_time)(std::ptr::null_mut(), &mut t), status::ERR_BAD_HANDLE);
assert_eq!((s.get_time)(handle, std::ptr::null_mut()), status::ERR_VALUE);
}
}
}