refactor: drop internal bridge/ffi layers; exporter family lands
Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/ oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain Rust, CHandle marshalling shrinks to the oakengine boundary, and tests call the Rust APIs directly (pure C-ABI wrapper tests removed where the domain layer already covers the behavior). exporter.h family implemented: oakengine_export_render (CLI contract), oakengine_export_render_with_params (was a stub), last_error and progress callback; synchronous path reuses task_create_export + start_sync. Fixes on the way: oaktask video ticket self-deadlock, audio params dropped on the export path, codec encoder AAC slicing and H.264 time base. Real-mp4 tests cover both entry points, progress and the illegal-argument matrix. Also: oakstorage session maps null project handles to None (version- info path), configstore test double literal 3.14 -> 3.15 (clippy PI lint), oakaudio output callback scratch buffer + env-aware P1 test, cli media round-trip test uses a generated 16-frame clip (no more minute-long debug runs).
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,431 +0,0 @@
|
||||
// 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,950 +0,0 @@
|
||||
// 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(¶ms)];
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,17 +17,16 @@
|
||||
//! clip 实例:clip ↔ oakrender 纹理桥。
|
||||
//!
|
||||
//! 对应 C++ 的 `OliveClipInstance`。纹理数据经
|
||||
//! [`crate::bridge::render`] 的 oakrender C ABI 流动;OFX 侧只看到
|
||||
//! [`crate::image::Image`](CPU 路径)。
|
||||
//! [`crate::render`](oakrender 值类型:`Texture`/`Frame`)流动;
|
||||
//! 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_*`),
|
||||
//! 两处桥实现完成。
|
||||
//! 单库化后 oakrender 的 ffi 已删除:帧访问走
|
||||
//! [`oakrender::texture::Texture::to_frame`] 值路径(GPU 纹理经后端
|
||||
//! 下载、CPU 纹理克隆),帧释放随值 drop 自动发生(原
|
||||
//! `texture_get_frame`/`frame_free` 句柄调用面随桩删除)。
|
||||
|
||||
use crate::instance::{OfxRangeD, OfxRectD, RenderScale};
|
||||
use crate::property::PropertySet;
|
||||
@@ -40,12 +39,12 @@ pub struct ClipInstance {
|
||||
pub props: PropertySet,
|
||||
/// clip 名。
|
||||
pub name: String,
|
||||
/// 当前输入纹理(oakrender 句柄的借用拷贝;输出 clip 为 None)。
|
||||
input_texture: std::sync::Mutex<Option<crate::bridge::render::TextureHandle>>,
|
||||
/// 当前输入纹理(oakrender 值;输出 clip 为 None)。
|
||||
input_texture: std::sync::Mutex<Option<crate::render::Texture>>,
|
||||
/// 当前输出纹理(C++ `output_textures_` 的 phase 1 单槽;
|
||||
/// [`store_output_image`](Self::store_output_image) 的回写目标;
|
||||
/// 输入 clip 为 None)。
|
||||
output_texture: std::sync::Mutex<Option<crate::bridge::render::TextureHandle>>,
|
||||
output_texture: std::sync::Mutex<Option<crate::render::Texture>>,
|
||||
}
|
||||
|
||||
/// 从 clip 属性读协商分量(getClipPreferences 写入)。
|
||||
@@ -136,29 +135,19 @@ impl ClipInstance {
|
||||
}
|
||||
|
||||
/// 挂接输入纹理(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);
|
||||
}
|
||||
/// 调用)。`time` 用于多帧纹理选择。None 断开。
|
||||
pub fn set_input_texture(&self, texture: Option<crate::render::Texture>, _time: f64) {
|
||||
*self.input_texture.lock().unwrap_or_else(|e| e.into_inner()) = texture;
|
||||
}
|
||||
|
||||
/// 挂接输出纹理(render 驱动创建并经句柄传入;C++
|
||||
/// 挂接输出纹理(render 驱动创建并经值传入;C++
|
||||
/// `setOutputTexture` 的 phase 1 单槽版)。`time` 用于多帧纹理
|
||||
/// 选择(`// [P2]`)。空句柄断开。
|
||||
pub fn set_output_texture(&self, texture: crate::bridge::render::TextureHandle, _time: f64) {
|
||||
let mut slot = self
|
||||
/// 选择(`// [P2]`)。None 断开。
|
||||
pub fn set_output_texture(&self, texture: Option<crate::render::Texture>, _time: f64) {
|
||||
*self
|
||||
.output_texture
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if texture.is_null() {
|
||||
*slot = None;
|
||||
} else {
|
||||
*slot = Some(texture);
|
||||
}
|
||||
.unwrap_or_else(|e| e.into_inner()) = texture;
|
||||
}
|
||||
|
||||
/// 抓取本 clip 在 `time` 的图像(OFX clipGetImage 的宿主侧)。
|
||||
@@ -174,7 +163,7 @@ impl ClipInstance {
|
||||
scale: RenderScale,
|
||||
region: Option<OfxRectD>,
|
||||
) -> crate::error::Result<crate::image::Image> {
|
||||
use crate::bridge::render::*;
|
||||
use crate::render::PIXEL_FORMAT_F32;
|
||||
use crate::error::Error;
|
||||
|
||||
let _ = (time, scale);
|
||||
@@ -188,18 +177,13 @@ impl ClipInstance {
|
||||
.clone()
|
||||
.ok_or(Error::NotFound)?;
|
||||
// 占位纹理(dummy):视作无输入。
|
||||
if unsafe { crate::bridge::render::texture_is_dummy(texture) } != 0 {
|
||||
if texture.is_dummy() {
|
||||
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) };
|
||||
// 纹理 → CPU 帧(GPU 纹理后端下载;帧随 drop 释放)。
|
||||
let frame = crate::render::texture_get_frame(&texture)?;
|
||||
let params = frame.video_params();
|
||||
if params.format != PIXEL_FORMAT_F32 {
|
||||
unsafe { crate::bridge::render::frame_free(&mut frame) };
|
||||
return Err(Error::Failed(format!(
|
||||
"输入帧格式 {} 非 F32(第 1 期约束)",
|
||||
params.format
|
||||
@@ -220,9 +204,8 @@ impl ClipInstance {
|
||||
y2: h,
|
||||
},
|
||||
);
|
||||
let src = unsafe { crate::bridge::render::frame_data(frame) };
|
||||
let src = frame.data();
|
||||
if src.is_null() {
|
||||
unsafe { crate::bridge::render::frame_free(&mut frame) };
|
||||
return Err(Error::Failed("帧无数据".into()));
|
||||
}
|
||||
// 行优先拷贝(帧行跨度经 linesize 读取——真实 oakrender 帧可
|
||||
@@ -230,16 +213,15 @@ impl ClipInstance {
|
||||
// 行,对真实 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 = frame.linesize_bytes();
|
||||
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 src_bytes = unsafe { std::slice::from_raw_parts(src, 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)
|
||||
}
|
||||
|
||||
@@ -247,17 +229,19 @@ impl ClipInstance {
|
||||
/// [`crate::instance::Instance::render`] 的调用方使用)。
|
||||
///
|
||||
/// 输出纹理由 oakrender 侧创建并经 [`Self::set_output_texture`]
|
||||
/// 挂入——本函数取该纹理的 CPU 帧(`texture_get_frame`),按帧
|
||||
/// 挂入——本函数取该纹理的 CPU 帧(GPU 纹理经后端下载,写回后
|
||||
/// 对 `Texture::Gpu` 再经
|
||||
/// [`oakrender::backend::GpuContextLike::upload`] 上传),按帧
|
||||
/// 参数校验 F32 与尺寸后整帧拷贝图像像素(全链路 F32;C++
|
||||
/// pluginrenderer 的 `readback/wrap` 路径第 1 期以 CPU 拷贝表达,
|
||||
/// GL 走 [`crate::bridge::render`] 的 `// [P2]`)。未挂输出纹理
|
||||
/// GL 走 [`crate::render`] 的 `// [P2]`)。未挂输出纹理
|
||||
/// 或纹理为占位(dummy)→ [`crate::error::Error::NotFound`]。
|
||||
/// 成功返回纹理句柄(借用拷贝,调用方负责其生命周期)。
|
||||
/// 成功返回纹理值(克隆,随 drop 释放)。
|
||||
pub fn store_output_image(
|
||||
&self,
|
||||
image: &crate::image::Image,
|
||||
) -> crate::error::Result<crate::bridge::render::TextureHandle> {
|
||||
use crate::bridge::render::*;
|
||||
) -> crate::error::Result<crate::render::Texture> {
|
||||
use crate::render::{texture_get_frame, PIXEL_FORMAT_F32};
|
||||
use crate::error::Error;
|
||||
|
||||
let texture = self
|
||||
@@ -266,18 +250,12 @@ impl ClipInstance {
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone()
|
||||
.ok_or(Error::NotFound)?;
|
||||
if unsafe { texture_is_dummy(texture) } != 0 {
|
||||
if texture.is_dummy() {
|
||||
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) };
|
||||
let mut frame = texture_get_frame(&texture)?;
|
||||
let params = frame.video_params();
|
||||
if params.format != PIXEL_FORMAT_F32 {
|
||||
unsafe { frame_free(&mut frame) };
|
||||
return Err(Error::Failed(format!(
|
||||
"输出帧格式 {} 非 F32(第 1 期约束)",
|
||||
params.format
|
||||
@@ -287,33 +265,37 @@ impl ClipInstance {
|
||||
// 图像与帧必须同尺寸(全链路 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) };
|
||||
let dst = frame.data_mut();
|
||||
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 = frame.linesize_bytes();
|
||||
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 dst_bytes = unsafe { std::slice::from_raw_parts_mut(dst, 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) };
|
||||
// GPU 目标纹理:拷贝只落在下载帧上,经后端 upload 回写
|
||||
// (CPU 纹理无需上传)。
|
||||
if let crate::render::Texture::Gpu { token, ctx, .. } = &texture {
|
||||
ctx.upload(*token, &frame)
|
||||
.map_err(|e| Error::Failed(format!("输出纹理上传失败:{e}")))?;
|
||||
}
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
/// 本 clip 的时间域(clipGetFrameRange)。
|
||||
///
|
||||
/// `// TODO(bridge)`:输入范围经 oakrender 帧的时间基推导
|
||||
/// (time_base)——随 renderer 桥落地。
|
||||
/// `// TODO(value-model)`:输入范围经 oakrender 帧的时间基推导
|
||||
/// (time_base)——随 clip 迁移到 `oakrender::texture::Texture`
|
||||
/// 值模型落地。
|
||||
pub fn frame_range(&self) -> crate::error::Result<OfxRangeD> {
|
||||
let _ = OfxRangeD::default();
|
||||
Err(crate::error::Error::Failed(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -56,8 +56,8 @@ pub struct RefBox<T: ?Sized> {
|
||||
/// 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.
|
||||
/// here so the crate's handle scaffolding stays source-compatible.
|
||||
/// `Send + Sync` come from the shared type.
|
||||
pub use oakcore_rs::handle::CHandle;
|
||||
|
||||
/// addref 的实现:原子 +1。拥有型与借用型共用——借用型只延长盒子
|
||||
@@ -177,7 +177,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// 无返回值版本:panic 被吞并记录日志(经 bridge 的日志回调)。
|
||||
/// 无返回值版本:panic 被吞(日志回调待 message 出口接入后补)。
|
||||
pub fn guard_void<F>(f: F)
|
||||
where
|
||||
F: FnOnce(),
|
||||
|
||||
@@ -35,8 +35,8 @@ use crate::property::PropertySet;
|
||||
pub struct EditTransaction {
|
||||
/// 嵌套深度(editBegin/End 必须配对)。
|
||||
depth: i32,
|
||||
/// 事务累积的 multi 命令(空句柄 = 尚无子命令)。
|
||||
multi: crate::bridge::undo::CommandHandle,
|
||||
/// 事务累积的 multi 命令(None = 尚无子命令)。
|
||||
multi: Option<oakundo::undocommand::UndoCommand>,
|
||||
/// 事务内回写次数(标签计数)。
|
||||
param_count: i32,
|
||||
/// 第一条回写的标签。
|
||||
@@ -48,7 +48,7 @@ impl EditTransaction {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
depth: 0,
|
||||
multi: crate::bridge::undo::CommandHandle::null(),
|
||||
multi: None,
|
||||
param_count: 0,
|
||||
first_label: String::new(),
|
||||
}
|
||||
@@ -141,8 +141,9 @@ impl Drop for Instance {
|
||||
|
||||
impl Instance {
|
||||
/// 绑定 oaknode 节点(C++ `set_node_handle` 的 Rust 侧;装配期由
|
||||
/// facade/测试调用)。`identity` 为 [`crate::host::instance_registry`]
|
||||
/// 无关的 oaknode 节点身份(`oaknode_node_identity` 的地址语义);
|
||||
/// facade/测试调用)。`identity` 为 [`crate::node::register_node`]
|
||||
/// 返回的 oaknode 节点身份([`oaknode::id::NodeId::identity`] 的
|
||||
/// 打包值,经 [`crate::node::node_from_identity`] 反查);
|
||||
/// 0 解除绑定。
|
||||
pub fn bind_node(&self, identity: usize) {
|
||||
self.node_identity
|
||||
@@ -154,7 +155,7 @@ impl Instance {
|
||||
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.multi = None;
|
||||
e.param_count = 0;
|
||||
e.first_label.clear();
|
||||
}
|
||||
@@ -168,9 +169,12 @@ impl Instance {
|
||||
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) };
|
||||
if e.depth == 0 {
|
||||
if let Some(mut multi) = e.multi.take() {
|
||||
multi.redo_now();
|
||||
// drop(multi):释放 multi 与子命令(命令值语义)。
|
||||
drop(multi);
|
||||
}
|
||||
e.param_count = 0;
|
||||
e.first_label.clear();
|
||||
}
|
||||
@@ -185,28 +189,24 @@ impl Instance {
|
||||
/// 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;
|
||||
}
|
||||
pub(crate) fn submit_undo_command(&self, mut cmd: oakundo::undocommand::UndoCommand, label: &str) {
|
||||
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() };
|
||||
if e.multi.is_none() {
|
||||
e.multi = Some(oakundo::undocommand::UndoCommand::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) };
|
||||
cmd.redo_now();
|
||||
e.multi
|
||||
.as_mut()
|
||||
.expect("multi 已在上面初始化")
|
||||
.multi_add_child(cmd);
|
||||
} else {
|
||||
unsafe { crate::bridge::undo::command_redo_now(cmd) };
|
||||
unsafe { crate::bridge::undo::command_free(&mut cmd) };
|
||||
cmd.redo_now();
|
||||
drop(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,8 +651,8 @@ impl Instance {
|
||||
time: f64,
|
||||
scale: RenderScale,
|
||||
window: OfxRectD,
|
||||
renderer: crate::bridge::render::RendererHandle,
|
||||
output_texture: crate::bridge::render::TextureHandle,
|
||||
renderer: crate::render::Renderer,
|
||||
output_texture: crate::render::Texture,
|
||||
) -> crate::error::Result<()> {
|
||||
use crate::host::ACTION_RENDER;
|
||||
|
||||
|
||||
+21
-15
@@ -21,21 +21,27 @@
|
||||
//! - **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`。
|
||||
//! - **桥**:把 OFX 实例的参数接到 oaknode([`node`],节点值 POD +
|
||||
//! 身份注册表与 undoable 回写)、把 clip 的输入输出接到 oakrender
|
||||
//! 纹理([`render`],纹理/帧/渲染器值类型)、把参数修改包成 undo
|
||||
//! 命令(直接经 `oakundo::undocommand::UndoCommand`,见
|
||||
//! [`instance::Instance::submit_undo_command`])。
|
||||
//!
|
||||
//! ## FFI 纪律(全 crate 最高优先级约定)
|
||||
//! ## 单库化(single-lib unification)
|
||||
//!
|
||||
//! 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 为参照系;协商与时序实现点必须
|
||||
//! 本 crate 的 C ABI 出口(原 [`ffi`])与模块桥(原 [`bridge`])已
|
||||
//! 删除:oaknode/oakrender/oakundo 均以 path 依赖直接链接。桥以
|
||||
//! 直接 Rust 类型重建([`node`] 的身份注册表与
|
||||
//! `set_input_*_undoable`;[`render`] 的 `Texture`/`Frame`/`Renderer`
|
||||
//! 值类型),仅 GPU 相关且 wgpu 模型无等价物的调用面保留标注桩
|
||||
//! ([`render::texture_id`]——GL 命名空间不存在,见 `// STUB`
|
||||
//! 标记)。
|
||||
//!
|
||||
//! ## 句柄纪律(全 crate 最高优先级约定)
|
||||
//!
|
||||
//! 1. 插件可在任意自起线程回调 suite(multithread suite 存活期
|
||||
//! 内);一切共享状态走 `Mutex`。
|
||||
//! 2. OFX 语义以 openfx HostSupport 为参照系;协商与时序实现点必须
|
||||
//! 注释对应 HostSupport 文件与行号(格式:`// HS: ofxhImageEffect.cpp:2776`)。
|
||||
//!
|
||||
//! ## 第 1 期范围
|
||||
@@ -47,17 +53,17 @@
|
||||
#![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 node;
|
||||
pub mod param;
|
||||
pub mod progress;
|
||||
pub mod property;
|
||||
pub mod render;
|
||||
pub mod render_driver;
|
||||
pub mod suites;
|
||||
|
||||
@@ -0,0 +1,727 @@
|
||||
// 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 桥(single-lib unification):节点值 POD、身份注册表与
|
||||
//! undoable 参数回写。
|
||||
//!
|
||||
//! ## Value 布局冻结
|
||||
//!
|
||||
//! [`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 里
|
||||
//! 不携带字符串数据。
|
||||
//!
|
||||
//! ## 身份注册表(单库化重建)
|
||||
//!
|
||||
//! oaknode 的 C ABI 已删除(单库化):节点对象是
|
||||
//! `oaknode::project::Project`(Arc<Mutex<Project>>)里的
|
||||
//! `oaknode::graph::Graph` 条目,按 [`oaknode::id::NodeId`] 定位。
|
||||
//! 本 crate 的 [`NodeRef`] 即旧句柄盒
|
||||
//! `(Arc<Mutex<Project>>, NodeId)` 的值型复刻;进程级注册表
|
||||
//! [`register_node`]/[`node_from_identity`] 按
|
||||
//! [`oaknode::id::NodeId::identity`] 的打包身份(u64)做弱引用
|
||||
//! 映射(project 释放后条目自然失效,升级失败 → None)——
|
||||
//! 对应 M9 C++ 版 `oaknode_node_identity()` 注册表的地址语义。
|
||||
//! facade 装配期调 [`register_node`] 登记节点,把返回身份写进
|
||||
//! [`crate::instance::Instance::bind_node`](`node_identity`)。
|
||||
//!
|
||||
//! ## undoable 回写
|
||||
//!
|
||||
//! [`set_input_undoable`]/[`set_input_string_undoable`] 按
|
||||
//! `oaknode::ops::set_value_at_time_command` 的同一 closure-vtable
|
||||
//! 模式(`command_from_closures`)构造未执行的
|
||||
//! `oakundo::undocommand::UndoCommand`:redo 闭包锁 project、
|
||||
//! `graph.get_mut(id)`、`NodeCore::set_standard_value` 写标准值;
|
||||
//! undo 闭包回放创建期快照的旧值。命令由调用方
|
||||
//! ([`crate::param::notify_instance_changed`] →
|
||||
//! [`crate::instance::Instance::submit_undo_command`])提交。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, OnceLock, Weak};
|
||||
|
||||
use oakundo::undocommand::{OakUndoCommandVtable, UndoCommand};
|
||||
|
||||
/// oaknode 节点引用(single-lib):旧 C ABI 句柄盒
|
||||
/// `(Arc<Mutex<Project>>, NodeId)` 的值型复刻。
|
||||
#[derive(Clone)]
|
||||
pub struct NodeRef {
|
||||
/// 所属 project(节点生命周期随 project)。
|
||||
pub project: Arc<Mutex<oaknode::project::Project>>,
|
||||
/// 节点在 project.graph 里的 id。
|
||||
pub id: oaknode::id::NodeId,
|
||||
}
|
||||
|
||||
/// oaknode_value_type 的取值(node.h:74)。
|
||||
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],
|
||||
}
|
||||
}
|
||||
|
||||
/// POD → [`oaknode::value::NodeValue`],按 POD kind 映射(与
|
||||
/// `oaknode::value::OakNodeValue::to_node_value` 的 kind 分支一致,
|
||||
/// 不按声明类型重量化)。`STRING` 族 POD 不携带数据 → `None`;
|
||||
/// `NONE` → [`oaknode::value::NodeValue::None`]。
|
||||
pub fn to_node_value(&self) -> Option<oaknode::value::NodeValue> {
|
||||
use oaknode::value::NodeValue as NV;
|
||||
match self.r#type {
|
||||
node_value_type::NONE => Some(NV::None),
|
||||
node_value_type::INT => Some(NV::Int(self.num)),
|
||||
node_value_type::FLOAT => Some(NV::Float(self.f[0])),
|
||||
node_value_type::BOOL => Some(NV::Boolean(self.num != 0)),
|
||||
node_value_type::RATIONAL => Some(NV::Rational(oakcore_rs::Rational::new(
|
||||
self.num, self.den,
|
||||
))),
|
||||
node_value_type::COLOR => Some(NV::Color(self.f)),
|
||||
node_value_type::VEC2 => Some(NV::Vec2([self.f[0], self.f[1]])),
|
||||
node_value_type::VEC3 => Some(NV::Vec3([self.f[0], self.f[1], self.f[2]])),
|
||||
node_value_type::VEC4 => Some(NV::Vec4(self.f)),
|
||||
node_value_type::COMBO => Some(NV::Combo(self.num)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// [`oaknode::value::NodeValue`] → POD(`ValueType::to_oak` 的逆)。
|
||||
/// 无 POD 表达的类型(字符串族/纹理/采样/矩阵等)→ `None`。
|
||||
pub fn from_node_value(v: &oaknode::value::NodeValue) -> Option<Value> {
|
||||
use oaknode::value::NodeValue as NV;
|
||||
Some(match v {
|
||||
NV::None => Value {
|
||||
r#type: node_value_type::NONE,
|
||||
num: 0,
|
||||
den: 0,
|
||||
f: [0.0; 4],
|
||||
},
|
||||
NV::Int(i) => Value::int(*i),
|
||||
NV::Float(f) => Value::float(*f),
|
||||
NV::Boolean(b) => Value::bool_(*b),
|
||||
NV::Rational(r) => Value {
|
||||
r#type: node_value_type::RATIONAL,
|
||||
num: r.numerator(),
|
||||
den: r.denominator(),
|
||||
f: [0.0; 4],
|
||||
},
|
||||
NV::Color(c) => Value::color(c[0], c[1], c[2], c[3]),
|
||||
NV::Vec2(a) => Value::vec(&[a[0], a[1]]),
|
||||
NV::Vec3(a) => Value::vec(&[a[0], a[1], a[2]]),
|
||||
NV::Vec4(a) => Value::vec(&[a[0], a[1], a[2], a[3]]),
|
||||
NV::Combo(i) => Value::combo(*i),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 身份注册表 -----------------------------------------------------------
|
||||
|
||||
/// 注册表条目:弱 project 引用(project 释放后升级失败,条目变死)
|
||||
/// + 节点 id。
|
||||
struct RegistryEntry {
|
||||
/// 所属 project(弱引用;强引用由调用方/节点持有)。
|
||||
project: Weak<Mutex<oaknode::project::Project>>,
|
||||
/// 节点 id。
|
||||
id: oaknode::id::NodeId,
|
||||
}
|
||||
|
||||
/// 进程级身份注册表(OnceLock 惰性初始化;测试进程内可重复 register)。
|
||||
static NODE_REGISTRY: OnceLock<Mutex<HashMap<u64, RegistryEntry>>> = OnceLock::new();
|
||||
|
||||
fn registry() -> &'static Mutex<HashMap<u64, RegistryEntry>> {
|
||||
NODE_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
/// 登记 oaknode 节点(facade 装配期调用;对应 M9 C++ 版
|
||||
/// `oaknode_node_identity()` 注册表的登记侧)。返回打包身份
|
||||
/// ([`oaknode::id::NodeId::identity`]),写入
|
||||
/// [`crate::instance::Instance::bind_node`]。同一身份重复登记
|
||||
/// 覆盖旧条目(重绑定)。
|
||||
pub fn register_node(
|
||||
project: Arc<Mutex<oaknode::project::Project>>,
|
||||
id: oaknode::id::NodeId,
|
||||
) -> u64 {
|
||||
let identity = id.identity();
|
||||
let entry = RegistryEntry {
|
||||
project: Arc::downgrade(&project),
|
||||
id,
|
||||
};
|
||||
registry()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(identity, entry);
|
||||
identity
|
||||
}
|
||||
|
||||
/// 摘除身份(节点销毁路径调用;未知身份 no-op)。
|
||||
pub fn unregister_node(identity: u64) {
|
||||
registry()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.remove(&identity);
|
||||
}
|
||||
|
||||
/// 身份 → 节点引用(param 回写入口)。
|
||||
///
|
||||
/// `None`:身份未登记,或 project 已释放(弱引用升级失败——旧 C
|
||||
/// ABI 对悬垂句柄的可解释失败路径)。调用方
|
||||
/// ([`crate::param::notify_instance_changed`])按 "未绑定节点 →
|
||||
/// no-op" 处理。
|
||||
pub fn node_from_identity(identity: usize) -> Option<NodeRef> {
|
||||
let (project, id) = {
|
||||
let reg = registry()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let entry = reg.get(&(identity as u64))?;
|
||||
(entry.project.clone(), entry.id)
|
||||
};
|
||||
let project = project.upgrade()?;
|
||||
Some(NodeRef { project, id })
|
||||
}
|
||||
|
||||
// ---- 桥调用面(undoable 回写)---------------------------------------------
|
||||
|
||||
/// oaknode 桥错误(set_input* 的失败原因;调用方按失败跳过提交)。
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NodeBridgeError {
|
||||
/// 节点 id 已失效(图内无此节点)。
|
||||
#[error("node id is stale or not present in the graph")]
|
||||
StaleNode,
|
||||
/// 节点上无该输入。
|
||||
#[error("node has no input '{0}'")]
|
||||
UnknownInput(String),
|
||||
/// 值 POD 无法转换(STRING 族 POD 不携带数据;数值族恒可转换)。
|
||||
#[error("value POD of type {0} cannot be converted to a node value")]
|
||||
InvalidValue(i32),
|
||||
/// 输入不是字符串族类型(string setter 限定 Text/StrCombo)。
|
||||
#[error("input '{0}' is not a string-family input")]
|
||||
NotStringInput(String),
|
||||
}
|
||||
|
||||
/// 取锁(毒锁接管,一次 panic 不级联)。
|
||||
fn lock_any<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
m.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Userdata payload behind a closure-backed undo command: the boxed
|
||||
/// redo/undo closures(与 `oaknode::ops.rs` 的 `ClosureCommand` 同构;
|
||||
/// 该类型的构造器是 oaknode 私有,桥内自备一份)。
|
||||
struct ClosureCommand {
|
||||
/// The redo closure.
|
||||
redo: Box<dyn FnMut() + Send>,
|
||||
/// The undo closure.
|
||||
undo: Box<dyn FnMut() + Send>,
|
||||
}
|
||||
|
||||
/// `OakUndoCommandVtable` redo thunk for [`ClosureCommand`] userdata.
|
||||
unsafe extern "C" fn closure_redo(ud: *mut std::ffi::c_void) {
|
||||
// SAFETY: `ud` 是 `command_from_closures` 创建的 ClosureCommand
|
||||
// 盒,命令持有其所有权直至 free_fn。
|
||||
let c = unsafe { &mut *(ud as *mut ClosureCommand) };
|
||||
(c.redo)();
|
||||
}
|
||||
|
||||
/// `OakUndoCommandVtable` undo thunk for [`ClosureCommand`] userdata.
|
||||
unsafe extern "C" fn closure_undo(ud: *mut std::ffi::c_void) {
|
||||
// SAFETY: 见 closure_redo。
|
||||
let c = unsafe { &mut *(ud as *mut ClosureCommand) };
|
||||
(c.undo)();
|
||||
}
|
||||
|
||||
/// `OakUndoCommandVtable` free thunk for [`ClosureCommand`] userdata.
|
||||
unsafe extern "C" fn closure_free(ud: *mut std::ffi::c_void) {
|
||||
if !ud.is_null() {
|
||||
// SAFETY: 盒恰被命令析构一次。
|
||||
unsafe { drop(Box::from_raw(ud as *mut ClosureCommand)) };
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 redo/undo 闭包构造未执行的 [`UndoCommand`](closure-vtable
|
||||
/// 模式,与 `oaknode::ops::command_from_closures` 同构)。
|
||||
fn command_from_closures(
|
||||
redo: impl FnMut() + Send + 'static,
|
||||
undo: impl FnMut() + Send + 'static,
|
||||
) -> UndoCommand {
|
||||
let ud = Box::into_raw(Box::new(ClosureCommand {
|
||||
redo: Box::new(redo),
|
||||
undo: Box::new(undo),
|
||||
}));
|
||||
UndoCommand::from_vtable(
|
||||
OakUndoCommandVtable {
|
||||
redo: Some(closure_redo),
|
||||
undo: Some(closure_undo),
|
||||
free_fn: Some(closure_free),
|
||||
},
|
||||
ud as *mut std::ffi::c_void,
|
||||
)
|
||||
}
|
||||
|
||||
/// 以 undoable 方式设置节点的标准输入值(POD 路径;数值族输入)。
|
||||
///
|
||||
/// 返回**未执行**的 [`UndoCommand`](调用方
|
||||
/// [`crate::instance::Instance::submit_undo_command`] 提交):redo
|
||||
/// 锁 project、`graph.get_mut(id)`、`NodeCore::set_standard_value`
|
||||
/// 写新值;undo 回放创建期快照的旧值(与
|
||||
/// `oaknode::ops::set_value_at_time_command` 的 standard-value 分支
|
||||
/// 同构,element = -1 整值)。节点/输入失效时返回
|
||||
/// [`NodeBridgeError`](不产出命令)。
|
||||
pub fn set_input_undoable(
|
||||
node: &NodeRef,
|
||||
input: &str,
|
||||
value: &Value,
|
||||
) -> Result<UndoCommand, NodeBridgeError> {
|
||||
let nv = value
|
||||
.to_node_value()
|
||||
.ok_or(NodeBridgeError::InvalidValue(value.r#type))?;
|
||||
// 校验节点与输入存在性 + 旧值快照(创建期状态,redo/undo 据此
|
||||
// 回放;不校验声明类型——按 POD kind 存原样,与 ops.rs 一致)。
|
||||
let old = {
|
||||
let mut guard = lock_any(&node.project);
|
||||
let entry = guard
|
||||
.graph
|
||||
.get_mut(node.id)
|
||||
.ok_or(NodeBridgeError::StaleNode)?;
|
||||
if entry.core.input_data_type(input).is_none() {
|
||||
return Err(NodeBridgeError::UnknownInput(input.to_string()));
|
||||
}
|
||||
entry.core.standard_value(input, -1)
|
||||
};
|
||||
|
||||
let project = node.project.clone();
|
||||
let id = node.id;
|
||||
let input_redo = input.to_string();
|
||||
let input_undo = input.to_string();
|
||||
let value_redo = nv.clone();
|
||||
let project_redo = project.clone();
|
||||
let project_undo = project.clone();
|
||||
Ok(command_from_closures(
|
||||
move || {
|
||||
let mut guard = lock_any(&project_redo);
|
||||
if let Some(entry) = guard.graph.get_mut(id) {
|
||||
entry
|
||||
.core
|
||||
.set_standard_value(&input_redo, -1, value_redo.clone());
|
||||
}
|
||||
},
|
||||
move || {
|
||||
let mut guard = lock_any(&project_undo);
|
||||
if let Some(entry) = guard.graph.get_mut(id) {
|
||||
entry
|
||||
.core
|
||||
.set_standard_value(&input_undo, -1, old.clone());
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// 以 undoable 方式设置节点的标准输入值(字符串族输入:Text /
|
||||
/// StrCombo——POD 不携带字符串数据)。
|
||||
///
|
||||
/// 按输入的声明类型([`oaknode::value::ValueType`])选存储变体:
|
||||
/// `StrCombo` → [`oaknode::value::NodeValue::StrCombo`],其余
|
||||
/// 字符串族(Text)→ [`oaknode::value::NodeValue::Text`]。声明类型
|
||||
/// 不是字符串族 → [`NodeBridgeError::NotStringInput`]。命令语义与
|
||||
/// [`set_input_undoable`] 相同。
|
||||
pub fn set_input_string_undoable(
|
||||
node: &NodeRef,
|
||||
input: &str,
|
||||
value: &str,
|
||||
) -> Result<UndoCommand, NodeBridgeError> {
|
||||
let (old, declared) = {
|
||||
let guard = lock_any(&node.project);
|
||||
let entry = guard
|
||||
.graph
|
||||
.get(node.id)
|
||||
.ok_or(NodeBridgeError::StaleNode)?;
|
||||
let declared = entry
|
||||
.core
|
||||
.input_data_type(input)
|
||||
.ok_or_else(|| NodeBridgeError::UnknownInput(input.to_string()))?;
|
||||
(entry.core.standard_value(input, -1), declared)
|
||||
};
|
||||
let nv = match declared {
|
||||
oaknode::value::ValueType::StrCombo => oaknode::value::NodeValue::StrCombo(value.to_string()),
|
||||
oaknode::value::ValueType::Text => oaknode::value::NodeValue::Text(value.to_string()),
|
||||
_ => return Err(NodeBridgeError::NotStringInput(input.to_string())),
|
||||
};
|
||||
|
||||
let project = node.project.clone();
|
||||
let id = node.id;
|
||||
let input_redo = input.to_string();
|
||||
let input_undo = input.to_string();
|
||||
let value_redo = nv.clone();
|
||||
let project_redo = project.clone();
|
||||
let project_undo = project.clone();
|
||||
Ok(command_from_closures(
|
||||
move || {
|
||||
let mut guard = lock_any(&project_redo);
|
||||
if let Some(entry) = guard.graph.get_mut(id) {
|
||||
entry
|
||||
.core
|
||||
.set_standard_value(&input_redo, -1, value_redo.clone());
|
||||
}
|
||||
},
|
||||
move || {
|
||||
let mut guard = lock_any(&project_undo);
|
||||
if let Some(entry) = guard.graph.get_mut(id) {
|
||||
entry
|
||||
.core
|
||||
.set_standard_value(&input_undo, -1, old.clone());
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 建一个含 Text/StrCombo/Float 输入的测试节点。
|
||||
fn test_project() -> (Arc<Mutex<oaknode::project::Project>>, oaknode::id::NodeId) {
|
||||
let project = oaknode::project::Project::new();
|
||||
let mut guard = project.lock().unwrap();
|
||||
let id = guard.graph.add_node(
|
||||
oaknode::node::NodeCore::empty(),
|
||||
Box::new(oaknode::nodes::EmptyBehavior),
|
||||
);
|
||||
// NodeCore::empty 无输入;手动声明三种输入。
|
||||
use oaknode::input::Input;
|
||||
let core = guard.graph.get_mut(id).unwrap();
|
||||
core.core.add_input(Input::new(
|
||||
"label",
|
||||
oaknode::value::ValueType::Text,
|
||||
oaknode::value::NodeValue::Text(String::new()),
|
||||
));
|
||||
core.core.add_input(Input::new(
|
||||
"choice",
|
||||
oaknode::value::ValueType::StrCombo,
|
||||
oaknode::value::NodeValue::StrCombo(String::new()),
|
||||
));
|
||||
core.core.add_input(Input::new(
|
||||
"opacity",
|
||||
oaknode::value::ValueType::Float,
|
||||
oaknode::value::NodeValue::Float(1.0),
|
||||
));
|
||||
drop(guard);
|
||||
(project, id)
|
||||
}
|
||||
|
||||
/// 注册表:register → node_from_identity → unregister 全链。
|
||||
#[test]
|
||||
fn identity_register_roundtrip() {
|
||||
let (project, id) = test_project();
|
||||
let identity = register_node(project.clone(), id);
|
||||
let node = node_from_identity(identity as usize).expect("已登记身份应可解析");
|
||||
assert_eq!(node.id, id);
|
||||
let found = node.project.lock().unwrap().graph.get(id).is_some();
|
||||
assert!(found);
|
||||
|
||||
// 未登记身份 → None。
|
||||
assert!(node_from_identity(0xfeed_face).is_none());
|
||||
unregister_node(identity);
|
||||
assert!(node_from_identity(identity as usize).is_none());
|
||||
}
|
||||
|
||||
/// project 释放后弱条目升级失败 → None(悬垂语义);重复登记
|
||||
/// 覆盖死条目后重新解析到新 project。
|
||||
#[test]
|
||||
fn identity_project_dropped() {
|
||||
let (project, id) = test_project();
|
||||
let identity = register_node(project.clone(), id);
|
||||
drop(project);
|
||||
assert!(node_from_identity(identity as usize).is_none());
|
||||
// 新 project 同槽位身份重登记(覆盖死条目)。
|
||||
let (project2, id2) = test_project();
|
||||
register_node(project2.clone(), id2);
|
||||
let node = node_from_identity(identity as usize).expect("重登记后应可解析");
|
||||
assert_eq!(node.id, id2);
|
||||
let alive = node.project.lock().unwrap().graph.get(id2).is_some();
|
||||
assert!(alive);
|
||||
}
|
||||
|
||||
/// Value POD ↔ NodeValue 全类型映射;STRING 族 POD 无数据。
|
||||
#[test]
|
||||
fn pod_node_value_roundtrip() {
|
||||
use oaknode::value::NodeValue as NV;
|
||||
let cases: Vec<(Value, NV)> = vec![
|
||||
(Value::int(7), NV::Int(7)),
|
||||
(Value::float(1.5), NV::Float(1.5)),
|
||||
(Value::bool_(true), NV::Boolean(true)),
|
||||
(Value::combo(3), NV::Combo(3)),
|
||||
(
|
||||
Value {
|
||||
r#type: node_value_type::RATIONAL,
|
||||
num: 3,
|
||||
den: 2,
|
||||
f: [0.0; 4],
|
||||
},
|
||||
NV::Rational(oakcore_rs::Rational::new(3, 2)),
|
||||
),
|
||||
(Value::color(0.1, 0.2, 0.3, 0.4), NV::Color([0.1, 0.2, 0.3, 0.4])),
|
||||
(Value::vec(&[1.0, 2.0]), NV::Vec2([1.0, 2.0])),
|
||||
(Value::vec(&[1.0, 2.0, 3.0]), NV::Vec3([1.0, 2.0, 3.0])),
|
||||
(
|
||||
Value::vec(&[1.0, 2.0, 3.0, 4.0]),
|
||||
NV::Vec4([1.0, 2.0, 3.0, 4.0]),
|
||||
),
|
||||
];
|
||||
for (pod, nv) in cases {
|
||||
let to = pod.to_node_value().expect("POD 应可转换");
|
||||
assert_eq!(to, nv, "POD → NodeValue");
|
||||
assert_eq!(Value::from_node_value(&nv), Some(pod), "NodeValue → POD");
|
||||
}
|
||||
// STRING POD 无数据。
|
||||
assert!(Value::string().to_node_value().is_none());
|
||||
// 无 POD 表达的类型。
|
||||
assert_eq!(Value::from_node_value(&NV::Text("x".into())), None);
|
||||
assert_eq!(Value::from_node_value(&NV::Matrix([0.0; 16])), None);
|
||||
}
|
||||
|
||||
/// set_input_undoable:redo 写标准值、undo 回放旧值;错误路径。
|
||||
#[test]
|
||||
fn set_input_undoable_applies_and_undoes() {
|
||||
let (project, id) = test_project();
|
||||
let node = NodeRef {
|
||||
project: project.clone(),
|
||||
id,
|
||||
};
|
||||
let mut cmd = set_input_undoable(&node, "opacity", &Value::float(0.25)).unwrap();
|
||||
// 命令未执行时值未变。
|
||||
assert_eq!(
|
||||
project
|
||||
.lock()
|
||||
.unwrap()
|
||||
.graph
|
||||
.get(id)
|
||||
.unwrap()
|
||||
.core
|
||||
.standard_value("opacity", -1),
|
||||
oaknode::value::NodeValue::Float(1.0)
|
||||
);
|
||||
cmd.redo_now();
|
||||
assert_eq!(
|
||||
project
|
||||
.lock()
|
||||
.unwrap()
|
||||
.graph
|
||||
.get(id)
|
||||
.unwrap()
|
||||
.core
|
||||
.standard_value("opacity", -1),
|
||||
oaknode::value::NodeValue::Float(0.25)
|
||||
);
|
||||
cmd.undo_now();
|
||||
assert_eq!(
|
||||
project
|
||||
.lock()
|
||||
.unwrap()
|
||||
.graph
|
||||
.get(id)
|
||||
.unwrap()
|
||||
.core
|
||||
.standard_value("opacity", -1),
|
||||
oaknode::value::NodeValue::Float(1.0)
|
||||
);
|
||||
|
||||
// 未知输入 / STRING POD / 失效节点。
|
||||
assert!(matches!(
|
||||
set_input_undoable(&node, "nope", &Value::float(1.0)),
|
||||
Err(NodeBridgeError::UnknownInput(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
set_input_undoable(&node, "opacity", &Value::string()),
|
||||
Err(NodeBridgeError::InvalidValue(_))
|
||||
));
|
||||
let stale = NodeRef {
|
||||
project,
|
||||
id: oaknode::id::NodeId::from_identity(999).unwrap(),
|
||||
};
|
||||
assert!(matches!(
|
||||
set_input_undoable(&stale, "opacity", &Value::float(1.0)),
|
||||
Err(NodeBridgeError::StaleNode)
|
||||
));
|
||||
}
|
||||
|
||||
/// set_input_string_undoable:Text/StrCombo 变体选择与回放。
|
||||
#[test]
|
||||
fn set_input_string_undoable_variants() {
|
||||
let (project, id) = test_project();
|
||||
let node = NodeRef {
|
||||
project: project.clone(),
|
||||
id,
|
||||
};
|
||||
let mut cmd = set_input_string_undoable(&node, "label", "hello").unwrap();
|
||||
cmd.redo_now();
|
||||
assert_eq!(
|
||||
project
|
||||
.lock()
|
||||
.unwrap()
|
||||
.graph
|
||||
.get(id)
|
||||
.unwrap()
|
||||
.core
|
||||
.standard_value("label", -1),
|
||||
oaknode::value::NodeValue::Text("hello".to_string())
|
||||
);
|
||||
cmd.undo_now();
|
||||
assert_eq!(
|
||||
project
|
||||
.lock()
|
||||
.unwrap()
|
||||
.graph
|
||||
.get(id)
|
||||
.unwrap()
|
||||
.core
|
||||
.standard_value("label", -1),
|
||||
oaknode::value::NodeValue::Text(String::new())
|
||||
);
|
||||
|
||||
let mut cmd = set_input_string_undoable(&node, "choice", "low").unwrap();
|
||||
cmd.redo_now();
|
||||
assert_eq!(
|
||||
project
|
||||
.lock()
|
||||
.unwrap()
|
||||
.graph
|
||||
.get(id)
|
||||
.unwrap()
|
||||
.core
|
||||
.standard_value("choice", -1),
|
||||
oaknode::value::NodeValue::StrCombo("low".to_string())
|
||||
);
|
||||
|
||||
// 数值输入走字符串 setter → NotStringInput。
|
||||
assert!(matches!(
|
||||
set_input_string_undoable(&node, "opacity", "1.0"),
|
||||
Err(NodeBridgeError::NotStringInput(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -18,18 +18,16 @@
|
||||
//!
|
||||
//! 对应 C++ 的 `ParamInstance`/`OliveParamInstance`。桥的语义
|
||||
//! (M9 已定):节点输入值变化 → 写回 OFX 参数;OFX 参数被插件
|
||||
//! 改动 → 经 oaknode C ABI 回写节点(undoable 经
|
||||
//! [`crate::bridge::undo`])。
|
||||
//! 改动 → 经 oaknode 回写节点(undoable 经
|
||||
//! `oakundo::undocommand::UndoCommand`)。
|
||||
//!
|
||||
//! 句柄约定([`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
|
||||
//! 已实现。
|
||||
//! 节点值布局:随 [`crate::node`] 冻结(`include/node/node.h` 的
|
||||
//! `oaknode_value` POD);单库化后 oaknode 的身份注册表随其 ffi
|
||||
//! 删除,回写路径是保留失败路径的本地桩(见 [`crate::node`])。
|
||||
|
||||
use std::ffi::CString;
|
||||
|
||||
@@ -532,55 +530,55 @@ impl ParamInstance {
|
||||
/// (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 {
|
||||
pub fn set_from_node(&self, node_value: &crate::node::Value) {
|
||||
if let Some(pv) = param_value_from_node(node_value, &self.def.ofx_type) {
|
||||
self.set_ofx(pv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ParamValue → [`crate::bridge::node::Value`](插件→节点方向;
|
||||
/// 字符串族经 [`crate::bridge::node::set_input_string_undoable`])。
|
||||
/// 节点值 → ParamValue(按参数类型解释;字符串族走专用路径)。
|
||||
/// 镜像 `set_from_node` 的映射表;render 驱动的参数覆盖
|
||||
/// (apply_param_overrides)复用同一转换。
|
||||
pub(crate) fn param_value_from_node(
|
||||
v: &crate::node::Value,
|
||||
ofx_type: &str,
|
||||
) -> Option<ParamValue> {
|
||||
use crate::node::node_value_type as T;
|
||||
match ofx_type {
|
||||
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 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 无数据,不改值
|
||||
}
|
||||
}
|
||||
|
||||
/// ParamValue → [`crate::node::Value`](插件→节点方向;
|
||||
/// 字符串族经 [`crate::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();
|
||||
pub(crate) fn to_node_value(v: &ParamValue) -> Option<crate::node::Value> {
|
||||
use crate::node::node_value_type as T;
|
||||
let mut out = crate::node::Value::default();
|
||||
match v {
|
||||
ParamValue::Double(d, 1) => {
|
||||
out.r#type = T::FLOAT;
|
||||
@@ -659,15 +657,18 @@ impl ParamSetInstance {
|
||||
}
|
||||
|
||||
/// 插件 → 节点方向的回写入口(instanceChanged action 触发)。
|
||||
/// 经 [`crate::bridge::node`] 定位绑定节点(身份注册表),再经
|
||||
/// [`crate::bridge::undo`] 包成 undoable 修改。未绑定节点时 no-op。
|
||||
/// 经 [`crate::node`] 定位绑定节点(身份注册表),再经
|
||||
/// `oakundo::undocommand::UndoCommand` 包成 undoable 修改。未绑定
|
||||
/// 节点或身份查无时 no-op(单库化后身份注册表重建为
|
||||
/// [`crate::node::register_node`]/[`crate::node::node_from_identity`];
|
||||
/// facade 装配期登记,project 释放后弱条目自然失效)。
|
||||
///
|
||||
/// 语义对照 C++ `paraminstance.h` 的 `set()` 路径(`detail::node_set`/
|
||||
/// `node_set_at`):
|
||||
/// - 只回写 [`ChangeReason::PluginEdited`](插件自改)。UserEdited/
|
||||
/// TimeChanged 是宿主侧变更,值已由 [`ParamInstance::set_from_node`]
|
||||
/// 同步,不重复写回;
|
||||
/// - 字符串族经 `oaknode_node_set_input_string_undoable`
|
||||
/// - 字符串族经 [`crate::node::set_input_string_undoable`]
|
||||
/// (POD 不携带字符串数据);
|
||||
/// - 无值类(PushButton/Group/Page)与 Bytes 无节点对应 → no-op;
|
||||
/// - 编辑事务内([`crate::instance::Instance::in_edit`])并入 multi
|
||||
@@ -677,7 +678,7 @@ pub(crate) fn notify_instance_changed(
|
||||
param_name: &str,
|
||||
reason: ChangeReason,
|
||||
) {
|
||||
use crate::bridge::{node, undo};
|
||||
use crate::node;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
if !matches!(reason, ChangeReason::PluginEdited) {
|
||||
@@ -688,42 +689,27 @@ pub(crate) fn notify_instance_changed(
|
||||
if node_id == 0 {
|
||||
return;
|
||||
}
|
||||
// 身份查无(注册表无此项 / 桥符号缺失)→ no-op。
|
||||
let node_handle = unsafe { node::node_from_identity(node_id) };
|
||||
if node_handle.is_null() {
|
||||
// 身份查无(未登记 / project 已释放)→ no-op。
|
||||
let Some(node_ref) = node::node_from_identity(node_id) else {
|
||||
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() {
|
||||
if let Ok(cmd) = node::set_input_undoable(&node_ref, param_name, &nv) {
|
||||
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 {
|
||||
let Ok(s) = s.to_str() 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() {
|
||||
if let Ok(cmd) = node::set_input_string_undoable(&node_ref, param_name, s) {
|
||||
instance.submit_undo_command(cmd, &label);
|
||||
}
|
||||
}
|
||||
@@ -746,7 +732,7 @@ pub enum ChangeReason {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bridge::node::node_value_type as T;
|
||||
use crate::node::node_value_type as T;
|
||||
|
||||
fn param(ofx_type: &str) -> ParamInstance {
|
||||
ParamInstance::from_def(ParamDef::new("p", ofx_type))
|
||||
@@ -756,7 +742,7 @@ mod tests {
|
||||
/// paraminstance.h 的 value_* 构造)。
|
||||
#[test]
|
||||
fn to_node_value_mapping() {
|
||||
use crate::bridge::node::Value;
|
||||
use crate::node::Value;
|
||||
let c = |t: i32| Value {
|
||||
r#type: t,
|
||||
num: 0,
|
||||
@@ -834,15 +820,15 @@ mod tests {
|
||||
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]));
|
||||
p.set_from_node(&crate::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());
|
||||
p.set_from_node(&crate::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));
|
||||
p.set_from_node(&crate::node::Value::float(3.0));
|
||||
assert!(matches!(p.get(), ParamValue::Bytes(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
// 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 桥(single-lib unification):纹理/帧值类型与渲染调用面。
|
||||
//!
|
||||
//! oakrender 的 C ABI 已删除(单库化):纹理是
|
||||
//! [`oakrender::texture::Texture`](value enum,无句柄),CPU 帧是
|
||||
//! [`oakrender::texture::Frame`]。本 crate 的 render 驱动与 GL suite
|
||||
//! 直接持值类型:
|
||||
//!
|
||||
//! - [`Texture`] = [`oakrender::texture::Texture`](值别名;
|
||||
//! clone 即引用语义,drop 自动释放后端 token——原 `texture_free`/
|
||||
//! `frame_free` 调用面随值模型删除);
|
||||
//! - [`Frame`] = [`oakrender::texture::Frame`](值别名);
|
||||
//! - [`Renderer`] = `Arc<dyn oakrender::backend::GpuContextLike>`
|
||||
//! (渲染器即 oakrender 后端上下文,facade 经
|
||||
//! [`oakrender::backend::GpuContext::create`] 创建);
|
||||
//! - [`VideoParams`] 直接别名 oakrender 的
|
||||
//! [`oakrender::frame::VideoParamsPod`](同布局 POD);
|
||||
//! - 像素格式常量直接别名 [`oakcore_rs::PixelFormat`]。
|
||||
//!
|
||||
//! 保留桩(GPU 相关、wgpu 模型无直接 Rust 等价物):
|
||||
//! [`texture_id`]——wgpu 后端没有 OpenGL 纹理名(旧 C ABI 的
|
||||
//! `oakrender_texture_id` 语义是 GL 命名空间),恒 0;GL suite 的
|
||||
//! `OpenGLTextureIndex` 属性与 render 驱动的 use_opengl 决策据此
|
||||
//! 回退 CPU 路径。
|
||||
|
||||
/// `oakrender_video_params` POD — single-lib unification: aliases the
|
||||
/// oakrender crate's struct (identical layout;
|
||||
/// include/render/renderer.h:78).
|
||||
pub type VideoParams = oakrender::frame::VideoParamsPod;
|
||||
|
||||
/// olive::PixelFormat::Format 的 f32 值。
|
||||
pub const PIXEL_FORMAT_F32: i32 = oakcore_rs::PixelFormat::F32 as i32;
|
||||
/// olive::PixelFormat::Format 的 u8 值。
|
||||
pub const PIXEL_FORMAT_U8: i32 = oakcore_rs::PixelFormat::U8 as i32;
|
||||
|
||||
/// oakrender 渲染器(后端上下文;Arc 共享,GPU 纹理据此 upload/
|
||||
/// download/blit——无需独立渲染器句柄)。
|
||||
pub type Renderer = std::sync::Arc<dyn oakrender::backend::GpuContextLike>;
|
||||
|
||||
/// oakrender 纹理(值型;GPU 或 CPU 包装)。
|
||||
pub type Texture = oakrender::texture::Texture;
|
||||
|
||||
/// oakrender CPU 帧(值型)。
|
||||
pub type Frame = oakrender::texture::Frame;
|
||||
|
||||
// ---- 桥调用面(值型实现;原 CHandle 桩随单库化重写)----------------------
|
||||
|
||||
/// 纹理是否占位(dummy:透明黑、从未上传)。
|
||||
pub fn texture_is_dummy(texture: &Texture) -> bool {
|
||||
texture.is_dummy()
|
||||
}
|
||||
|
||||
/// 纹理的 CPU 帧:GPU 纹理经后端下载,CPU 纹理克隆
|
||||
/// (原 `texture_get_frame` 的 out 参数形态改为值返回;错误即旧
|
||||
/// "纹理无 CPU 帧" 失败路径)。
|
||||
pub fn texture_get_frame(texture: &Texture) -> crate::error::Result<Frame> {
|
||||
texture.to_frame().map_err(|e| {
|
||||
crate::error::Error::Failed(format!("纹理 readback 失败:{e}"))
|
||||
})
|
||||
}
|
||||
|
||||
/// 纹理的视频参数(尺寸 + 像素格式;原 `texture_get_params` 的
|
||||
/// out 参数形态改为值返回)。
|
||||
pub fn texture_get_params(texture: &Texture) -> VideoParams {
|
||||
let (w, h) = texture.size();
|
||||
let mut p = VideoParams::default();
|
||||
p.width = w;
|
||||
p.height = h;
|
||||
p.format = texture.format() as i32;
|
||||
p
|
||||
}
|
||||
|
||||
/// 从视频参数与像素数据构造 CPU 纹理(原 `texture_create` 的
|
||||
/// renderer+参数+像素形态改为值形态):按 `linesize` 行跨度做行优先
|
||||
/// 拷贝(0 = 紧凑行),F32/RGBA 4 通道为管线常规。GPU 上传由后端
|
||||
/// 延迟(wrapped frame 语义,`Texture::wrap_frame`)。
|
||||
pub fn texture_create(
|
||||
params: &VideoParams,
|
||||
pixels: &[u8],
|
||||
linesize: i32,
|
||||
) -> crate::error::Result<Texture> {
|
||||
use crate::error::Error;
|
||||
let mut frame = Frame::new();
|
||||
frame.set_video_params(*params);
|
||||
let w = frame.width.max(0) as usize;
|
||||
let h = frame.height.max(0) as usize;
|
||||
if w == 0 || h == 0 {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let bpc = frame.bytes_per_channel();
|
||||
if bpc == 0 {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
// 行跨度推导通道数(RGB 3 通道等非常规分量)。
|
||||
if linesize > 0 && w > 0 && (linesize as usize) % (w * bpc) == 0 {
|
||||
frame.channels = (linesize as usize / (w * bpc)) as i32;
|
||||
}
|
||||
let tight = frame.linesize_bytes();
|
||||
let row = if linesize > 0 { linesize as usize } else { tight };
|
||||
if pixels.len() < row.saturating_mul(h) {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
frame.data = vec![0u8; tight * h];
|
||||
for y in 0..h {
|
||||
let s = y * row;
|
||||
let d = y * tight;
|
||||
frame.data[d..d + tight].copy_from_slice(&pixels[s..s + tight]);
|
||||
}
|
||||
Ok(Texture::wrap_frame(frame))
|
||||
}
|
||||
|
||||
/// STUB: oakrender C ABI deleted (single-lib),且 wgpu 后端没有
|
||||
/// OpenGL 纹理名——旧 `oakrender_texture_id` 的 GL 命名空间语义无
|
||||
/// Rust 等价物。恒 0(GL suite 的 `OpenGLTextureIndex` 属性与
|
||||
/// render 驱动的 use_opengl 决策据此回退 CPU 路径;GPU 上传若落地
|
||||
/// 走 `oakrender::backend::GpuContextLike::upload` 的 wgpu token,
|
||||
/// 不暴露 GL id)。
|
||||
pub fn texture_id(_texture: &Texture) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
/// 渲染器是否为 OpenGL 后端([`oakrender::backend::BackendKind::Gl`];
|
||||
/// 原 `renderer_is_open_gl` 的句柄形态改为后端上下文 kind 查询)。
|
||||
pub fn renderer_is_open_gl(renderer: &Renderer) -> bool {
|
||||
renderer.kind() == oakrender::backend::BackendKind::Gl
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 测试渲染器:最小 GpuContextLike 假实现(无 GPU 适配器需求)。
|
||||
struct FakeGpu;
|
||||
impl oakrender::backend::GpuContextLike for FakeGpu {
|
||||
fn kind(&self) -> oakrender::backend::BackendKind {
|
||||
oakrender::backend::BackendKind::Cpu
|
||||
}
|
||||
fn destroy_texture(&self, _token: u64) {}
|
||||
fn upload(&self, _token: u64, _frame: &Frame) -> oakrender::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn download(&self, _token: u64) -> oakrender::error::Result<Frame> {
|
||||
Ok(Frame::new())
|
||||
}
|
||||
fn blit(
|
||||
&self,
|
||||
_src: u64,
|
||||
_dst: u64,
|
||||
_processor: Option<&oakrender::color::ColorProcessor>,
|
||||
) -> oakrender::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 纹理创建:参数 + 行跨度感知拷贝往返(含 RGB 3 通道推导)。
|
||||
#[test]
|
||||
fn texture_create_linesize_aware() {
|
||||
let mut params = VideoParams::default();
|
||||
params.width = 3;
|
||||
params.height = 2;
|
||||
params.format = PIXEL_FORMAT_F32;
|
||||
// 紧凑 3×2 RGBA F32:3*4*4 = 48 B/行。
|
||||
let tight = 3 * 4 * 4;
|
||||
let mut pixels = vec![0u8; tight * 2];
|
||||
for (i, b) in pixels.iter_mut().enumerate() {
|
||||
*b = (i % 251) as u8;
|
||||
}
|
||||
let tex = texture_create(¶ms, &pixels, 0).expect("紧凑行应可创建");
|
||||
assert!(!texture_is_dummy(&tex));
|
||||
let frame = texture_get_frame(&tex).expect("CPU 帧应可读");
|
||||
assert_eq!(frame.width, 3);
|
||||
assert_eq!(frame.height, 2);
|
||||
assert_eq!(frame.channels, 4);
|
||||
assert_eq!(frame.linesize_bytes(), tight);
|
||||
assert_eq!(frame.data, pixels);
|
||||
// 行跨度变体(每行 4 字节填充)。
|
||||
let row = tight + 4;
|
||||
let mut padded = vec![0u8; row * 2];
|
||||
for y in 0..2 {
|
||||
padded[y * row..y * row + tight].copy_from_slice(&pixels[y * tight..(y + 1) * tight]);
|
||||
}
|
||||
let tex = texture_create(¶ms, &padded, row as i32).expect("带填充行应可创建");
|
||||
let frame = texture_get_frame(&tex).unwrap();
|
||||
assert_eq!(frame.data, pixels, "填充行应剥离为紧凑存储");
|
||||
// 像素不足 → Invalid。
|
||||
assert!(texture_create(¶ms, &padded[..row * 2 - 1], row as i32).is_err());
|
||||
// 无效尺寸 → Invalid。
|
||||
let mut bad = params;
|
||||
bad.width = 0;
|
||||
assert!(texture_create(&bad, &pixels, 0).is_err());
|
||||
}
|
||||
|
||||
/// 纹理参数查询与 dummy 语义。
|
||||
#[test]
|
||||
fn texture_params_and_dummy() {
|
||||
let dummy = Texture::dummy();
|
||||
assert!(texture_is_dummy(&dummy));
|
||||
let p = texture_get_params(&dummy);
|
||||
assert_eq!((p.width, p.height), (0, 0));
|
||||
|
||||
let mut params = VideoParams::default();
|
||||
params.width = 8;
|
||||
params.height = 4;
|
||||
params.format = PIXEL_FORMAT_F32;
|
||||
let tex = texture_create(¶ms, &[0u8; 8 * 4 * 4 * 4], 0).unwrap();
|
||||
let p = texture_get_params(&tex);
|
||||
assert_eq!((p.width, p.height), (8, 4));
|
||||
assert_eq!(p.format, PIXEL_FORMAT_F32);
|
||||
}
|
||||
|
||||
/// 渲染器 kind 查询与 GL 判断;texture_id 桩恒 0(无 GL 命名空间)。
|
||||
#[test]
|
||||
fn renderer_kind_and_gl_id_stub() {
|
||||
let r: Renderer = std::sync::Arc::new(FakeGpu);
|
||||
assert!(!renderer_is_open_gl(&r));
|
||||
let dummy = Texture::dummy();
|
||||
assert_eq!(texture_id(&dummy), 0, "wgpu 无 GL 纹理名:桩恒 0");
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,10 @@
|
||||
//! `PluginRenderer::render_plugin`(1857 行 C++ 的一部分)的渲染流程
|
||||
//! 语义收编(M11 §4)。
|
||||
//!
|
||||
//! 目标:oakrender 的 PluginJob 退化为一次 C ABI 调用(
|
||||
//! [`crate::ffi::oakplugin_instance_render_job`]),本模块承载全部
|
||||
//! OFX 宿主渲染流程。逐段对照的 C++ 行号已注释。
|
||||
//! 目标:oakrender 的 PluginJob 语义全部收编进本模块(单库化后
|
||||
//! 原 `oakplugin_instance_render_job` C ABI 已删除,facade 直接构造
|
||||
//! [`RenderJob`] 调 [`render_frame`]),本模块承载全部 OFX 宿主
|
||||
//! 渲染流程。逐段对照的 C++ 行号已注释。
|
||||
//!
|
||||
//! ## 流程(render_frame,对应 render_plugin)
|
||||
//!
|
||||
@@ -56,28 +57,27 @@
|
||||
//! 的一批帧先 [`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;
|
||||
use crate::render::{self, Renderer, Texture};
|
||||
|
||||
/// 一帧渲染任务的输入(oakrender PluginJob 的 C ABI 载体;
|
||||
/// [`crate::ffi::OakPluginJob`] 的 Rust 侧视图)。
|
||||
/// 一帧渲染任务的输入(oakrender PluginJob 的 Rust 侧视图)。
|
||||
pub struct RenderJob {
|
||||
/// 帧时间(秒)。
|
||||
pub time: f64,
|
||||
/// 目标纹理(输出;oakrender 侧创建并经句柄传入)。
|
||||
pub dst: TextureHandle,
|
||||
/// 主输入纹理(effect_input_id / SimpleSource;可空)。
|
||||
pub src: TextureHandle,
|
||||
/// 目标纹理(输出;oakrender 侧创建并经值传入)。
|
||||
pub dst: Texture,
|
||||
/// 主输入纹理(effect_input_id / SimpleSource;无则 None)。
|
||||
pub src: Option<Texture>,
|
||||
/// effect 输入 clip 名(job.src 的落点;C++ `node->get_effect_input_id()`)。
|
||||
pub effect_input_id: Option<String>,
|
||||
/// 其余输入 clip 的纹理表(clip 名 → 纹理)。
|
||||
pub inputs: Vec<(String, TextureHandle)>,
|
||||
pub inputs: Vec<(String, Texture)>,
|
||||
/// 参数覆盖(参数名 → oaknode_value POD;对应 NodeValueRow)。
|
||||
pub values: Vec<(String, crate::ffi::OakNodeValue)>,
|
||||
pub values: Vec<(String, crate::node::Value)>,
|
||||
/// GL 渲染器(None → CPU 路径;Some → 视 use_opengl 决策)。
|
||||
pub renderer: Option<RendererHandle>,
|
||||
pub renderer: Option<Renderer>,
|
||||
/// 渲染前是否清空目标(信息性——C++ render_plugin 亦不处理,
|
||||
/// 由上层渲染器负责;见插件渲染器注释)。
|
||||
pub clear_destination: bool,
|
||||
@@ -90,8 +90,8 @@ impl Default for RenderJob {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
time: 0.0,
|
||||
dst: TextureHandle::null(),
|
||||
src: TextureHandle::null(),
|
||||
dst: Texture::dummy(),
|
||||
src: None,
|
||||
effect_input_id: None,
|
||||
inputs: Vec::new(),
|
||||
values: Vec::new(),
|
||||
@@ -109,7 +109,7 @@ impl Default for RenderJob {
|
||||
pub fn begin_sequence(
|
||||
inst: &Instance,
|
||||
range: OfxRangeD,
|
||||
gl: Option<RendererHandle>,
|
||||
gl: Option<Renderer>,
|
||||
) -> crate::error::Result<()> {
|
||||
if gl.is_some() {
|
||||
inst.begin_sequence_render_gl(range)
|
||||
@@ -122,7 +122,7 @@ pub fn begin_sequence(
|
||||
pub fn end_sequence(
|
||||
inst: &Instance,
|
||||
range: OfxRangeD,
|
||||
gl: Option<RendererHandle>,
|
||||
gl: Option<Renderer>,
|
||||
) -> crate::error::Result<()> {
|
||||
if gl.is_some() {
|
||||
inst.end_sequence_render_gl(range)
|
||||
@@ -153,21 +153,22 @@ pub fn render_frame(
|
||||
// 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 => {
|
||||
// `texture_id` 为桩恒 0(wgpu 无 GL 命名空间)→ 本决策恒回退
|
||||
// CPU 路径,GL 分支保留给 GL 后端落地。
|
||||
let use_opengl = match job.renderer.as_ref() {
|
||||
Some(r) if render::renderer_is_open_gl(r) => {
|
||||
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) };
|
||||
let dst_id = 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 (dst_params, w, h) = read_dst(&job.dst)?;
|
||||
let par = pixel_aspect(&dst_params);
|
||||
// 规范坐标的 RoI/RoD(pluginrenderer.cpp:1595-1603:x2 = 宽 × PAR)。
|
||||
let region_of_interest = OfxRectD {
|
||||
@@ -182,9 +183,10 @@ pub fn render_frame(
|
||||
if clip.name == "Output" {
|
||||
continue;
|
||||
}
|
||||
let tex = pick_input(&clip.name, job);
|
||||
if usable(tex) {
|
||||
clip.set_input_texture(tex, job.time);
|
||||
if let Some(tex) = pick_input(&clip.name, job) {
|
||||
if usable(&tex) {
|
||||
clip.set_input_texture(Some(tex), job.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +209,7 @@ pub fn render_frame(
|
||||
.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);
|
||||
output_clip.set_output_texture(Some(job.dst.clone()), job.time);
|
||||
|
||||
// 6. 输入 clip:RoD 与格式(pluginrenderer.cpp:1627-1665;Phase 2
|
||||
// 全链路 F32 → 格式选择恒等,无转换路径)。
|
||||
@@ -215,7 +217,7 @@ pub fn render_frame(
|
||||
if clip.name == "Output" {
|
||||
continue;
|
||||
}
|
||||
if usable(pick_input(&clip.name, job)) {
|
||||
if pick_input(&clip.name, job).map_or(false, |t| usable(&t)) {
|
||||
clip.set_region_of_definition(region_of_interest, job.time);
|
||||
clip.set_video_params(render::PIXEL_FORMAT_F32, 4);
|
||||
}
|
||||
@@ -242,8 +244,7 @@ pub fn render_frame(
|
||||
// 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)) };
|
||||
passthrough(inst, &clip_name, t, &job.dst)?;
|
||||
return Ok(zip_rois(inst, &rois));
|
||||
}
|
||||
|
||||
@@ -269,7 +270,7 @@ pub fn render_frame(
|
||||
output.clone(),
|
||||
)?;
|
||||
// 输出装配(pluginrenderer.cpp:1762-1834 的 CPU 路径)。
|
||||
write_output_frame(job.dst, &output)?;
|
||||
write_output_frame(&job.dst, &output)?;
|
||||
} else {
|
||||
// GL 路径:插件直接画进已附着的输出纹理(
|
||||
// pluginrenderer.cpp:1784-1834 的 GL 分支);无 CPU 回读。
|
||||
@@ -277,12 +278,11 @@ pub fn render_frame(
|
||||
job.time,
|
||||
RenderScale { x: 1.0, y: 1.0 },
|
||||
render_window,
|
||||
job.renderer.unwrap(),
|
||||
job.dst,
|
||||
job.renderer.clone().unwrap(),
|
||||
job.dst.clone(),
|
||||
)?;
|
||||
}
|
||||
|
||||
unsafe { render::frame_free(&mut (dst_frame)) };
|
||||
Ok(zip_rois(inst, &rois))
|
||||
}
|
||||
|
||||
@@ -296,22 +296,11 @@ fn zip_rois(inst: &Instance, rois: &[OfxRectD]) -> Vec<(String, OfxRectD)> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 读目标纹理的帧与参数(F32 校验)。返回 (帧句柄, 参数, 宽, 高)。
|
||||
fn read_dst(
|
||||
dst: TextureHandle,
|
||||
) -> crate::error::Result<(FrameHandle, render::VideoParams, f64, f64)> {
|
||||
/// 读目标纹理的参数(F32 校验)。返回 (参数, 宽, 高)。
|
||||
fn read_dst(dst: &Texture) -> crate::error::Result<(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()));
|
||||
}
|
||||
let params = render::texture_get_params(dst);
|
||||
if params.format != render::PIXEL_FORMAT_F32 {
|
||||
unsafe { render::frame_free(&mut frame) };
|
||||
return Err(Error::Failed(format!(
|
||||
"输出帧格式 {} 非 F32(Phase 2 约束)",
|
||||
params.format
|
||||
@@ -319,10 +308,9 @@ fn read_dst(
|
||||
}
|
||||
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))
|
||||
Ok((params, w, h))
|
||||
}
|
||||
|
||||
/// 目标参数的像素比(缺失 1.0)。
|
||||
@@ -351,30 +339,32 @@ fn plugin_supports_opengl(inst: &Instance) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 输入纹理是否可用(非空且非占位;pluginrenderer.cpp:1504-1513 的
|
||||
/// 输入纹理是否可用(非占位;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
|
||||
fn usable(tex: &Texture) -> bool {
|
||||
!tex.is_dummy()
|
||||
}
|
||||
|
||||
/// 按 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;
|
||||
fn pick_input(clip_name: &str, job: &RenderJob) -> Option<Texture> {
|
||||
if job.effect_input_id.as_deref() == Some(clip_name) {
|
||||
if let Some(src) = &job.src {
|
||||
return Some(src.clone());
|
||||
}
|
||||
}
|
||||
for (name, tex) in &job.inputs {
|
||||
if name == clip_name {
|
||||
return *tex;
|
||||
return Some(tex.clone());
|
||||
}
|
||||
}
|
||||
// SimpleSource 回退(pluginrenderer.cpp:1534-1543:
|
||||
// kOfxImageEffectSimpleSourceClipName 取 k_texture_input,再回退
|
||||
// job.src)。
|
||||
if clip_name == "Source" && !job.src.is_null() {
|
||||
return job.src;
|
||||
if clip_name == "Source" {
|
||||
return job.src.clone();
|
||||
}
|
||||
TextureHandle::null()
|
||||
None
|
||||
}
|
||||
|
||||
/// isIdentity 透传:把所引输入 clip 在 `t` 的帧拷入输出(CPU 拷贝;
|
||||
@@ -383,7 +373,7 @@ fn passthrough(
|
||||
inst: &Instance,
|
||||
clip_name: &str,
|
||||
t: f64,
|
||||
dst: TextureHandle,
|
||||
dst: &Texture,
|
||||
) -> crate::error::Result<()> {
|
||||
use crate::error::Error;
|
||||
let clip = inst
|
||||
@@ -397,59 +387,54 @@ fn passthrough(
|
||||
|
||||
/// 参数覆盖(pluginrenderer.cpp:132-290 `apply_param_overrides` 的
|
||||
/// Rust 移植):把每帧的节点值注入实例参数。字符串族(String/
|
||||
/// StrChoice)经专用 C ABI(set_param_string),不在此表的 oaknode
|
||||
/// StrChoice)经专用路径(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)]) {
|
||||
fn apply_param_overrides(inst: &Instance, values: &[(String, crate::node::Value)]) {
|
||||
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 {
|
||||
let Some(pv) = crate::param::param_value_from_node(v, &p.def.ofx_type) else {
|
||||
continue;
|
||||
};
|
||||
p.set_ofx(pv);
|
||||
}
|
||||
}
|
||||
|
||||
/// 把 CPU 图像写入目标纹理的帧(行优先、行跨度感知;F32 校验)。
|
||||
/// 把 CPU 图像写入目标纹理(行优先、行跨度感知;F32 校验)。
|
||||
/// Phase 2 输出装配的公共落点(CPU render 路径与 isIdentity 透传
|
||||
/// 共用)。
|
||||
pub(crate) fn write_output_frame(dst: TextureHandle, image: &Image) -> crate::error::Result<()> {
|
||||
/// 共用)。GPU 目标纹理经后端 upload 回写(`Texture::Gpu` 分支)。
|
||||
pub(crate) fn write_output_frame(dst: &Texture, 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()));
|
||||
}
|
||||
let mut frame = render::texture_get_frame(dst)?;
|
||||
let params = frame.video_params();
|
||||
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) };
|
||||
let dst_ptr = frame.data_mut();
|
||||
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 = frame.linesize_bytes();
|
||||
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 dst_bytes = unsafe { std::slice::from_raw_parts_mut(dst_ptr, 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) };
|
||||
// GPU 目标纹理:拷贝只落在下载帧上,经后端 upload 回写
|
||||
// (CPU 纹理无需上传)。
|
||||
if let Texture::Gpu { token, ctx, .. } = dst {
|
||||
ctx.upload(*token, &frame)
|
||||
.map_err(|e| Error::Failed(format!("输出纹理上传失败:{e}")))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
//! 已把输出纹理附着为渲染器输出目标(等价 C++ 的
|
||||
//! `PluginRenderer::attach_output_texture`)。clipFreeTexture 对
|
||||
//! Output 只释放句柄、不删纹理(宿主还要读它)。
|
||||
//! - 输入纹理经 [`crate::bridge::render`] 在渲染器上创建(CPU 帧 →
|
||||
//! - 输入纹理经 [`crate::render`] 在渲染器上创建(CPU 帧 →
|
||||
//! GL 上传)。纹理格式:全链路 F32 约束下,像素深度按 clip 协商
|
||||
//! 结果(恒 F32);`format` 参数(kOfxImageEffectGLFormat*)若
|
||||
//! 请求的分量与协商分量不符,Phase 2 不做转换 → Failed(规范要求
|
||||
@@ -164,19 +164,21 @@ mod tests {
|
||||
// ---- 存活纹理表 -----------------------------------------------------------
|
||||
|
||||
/// 存活 GL 纹理表:clipLoadTexture 产出(props 地址 → 属性集 +
|
||||
/// 纹理强引用 + 是否输出 clip);clipFreeTexture 摘除即释放——对应
|
||||
/// 纹理值 + 是否输出 clip);clipFreeTexture 摘除即释放——对应
|
||||
/// HS 的 get/release 配对(HS: ofxhImageEffect.cpp:2336-2351)。
|
||||
/// 纹理是 oakrender 值(drop 自动释放后端 token;原
|
||||
/// `texture_free` 调用面随值模型删除)。
|
||||
///
|
||||
/// 属性集必须**装箱**(Box 稳定堆地址):纹理句柄指向它,函数返回后
|
||||
/// 必须仍存活;栈上临时变量会悬垂(phase-2 实现初版的 bug)。
|
||||
static LIVE_TEXTURES: std::sync::LazyLock<
|
||||
Mutex<HashMap<usize, (Box<PropertySet>, crate::bridge::render::TextureHandle, bool)>>,
|
||||
Mutex<HashMap<usize, (Box<PropertySet>, crate::render::Texture, bool)>>,
|
||||
> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// 登记纹理(clipLoadTexture 内部;`props` 装箱后取地址为句柄基址)。
|
||||
fn register(
|
||||
props: Box<PropertySet>,
|
||||
texture: crate::bridge::render::TextureHandle,
|
||||
texture: crate::render::Texture,
|
||||
is_output: bool,
|
||||
) -> usize {
|
||||
let addr = &*props as *const PropertySet as usize;
|
||||
@@ -191,16 +193,8 @@ fn register(
|
||||
/// 插件在 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) };
|
||||
}
|
||||
}
|
||||
let mut live = LIVE_TEXTURES.lock().unwrap_or_else(|e| e.into_inner());
|
||||
live.retain(|_, (_, _, is_output)| *is_output);
|
||||
}
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
@@ -246,10 +240,12 @@ fn clip_string(clip: &ClipInstance, name: &str, default: &str) -> String {
|
||||
}
|
||||
|
||||
/// 构造纹理属性集(ofxGPURender.h 规定的属性;输入与输出共用,
|
||||
/// 只是数据来源不同)。
|
||||
/// 只是数据来源不同)。`OpenGLTextureIndex` 经
|
||||
/// [`crate::render::texture_id`](桩恒 0——wgpu 后端无 GL 命名空间;
|
||||
/// CPU 回退下插件按规范回退,纹理内容仍可经 clipGetImage 取用)。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn make_texture_props(
|
||||
texture: crate::bridge::render::TextureHandle,
|
||||
texture: &crate::render::Texture,
|
||||
width: f64,
|
||||
height: f64,
|
||||
components: crate::image::Components,
|
||||
@@ -262,7 +258,7 @@ fn make_texture_props(
|
||||
let props = PropertySet::new();
|
||||
props.set_one(
|
||||
GL_TEXTURE_INDEX,
|
||||
Value::Int(unsafe { crate::bridge::render::texture_id(texture) }),
|
||||
Value::Int(crate::render::texture_id(texture)),
|
||||
);
|
||||
props.set_one(GL_TEXTURE_TARGET, Value::Int(GL_TEXTURE_2D));
|
||||
props.set_one(
|
||||
@@ -342,16 +338,13 @@ unsafe extern "C" fn clip_load_texture(
|
||||
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 tex = gl.output_texture.clone();
|
||||
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,
|
||||
&tex,
|
||||
w,
|
||||
h,
|
||||
crate::image::Components::Rgba,
|
||||
@@ -393,25 +386,16 @@ unsafe extern "C" fn clip_load_texture(
|
||||
if w <= 0.0 || h <= 0.0 {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
let params = crate::bridge::render::VideoParams {
|
||||
let params = crate::render::VideoParams {
|
||||
width: w as i32,
|
||||
height: h as i32,
|
||||
format: crate::bridge::render::PIXEL_FORMAT_F32,
|
||||
format: crate::render::PIXEL_FORMAT_F32,
|
||||
..Default::default()
|
||||
};
|
||||
let tex = unsafe {
|
||||
crate::bridge::render::texture_create(
|
||||
gl.renderer,
|
||||
¶ms,
|
||||
image.pixels().as_ptr() as *const c_void,
|
||||
image.row_bytes() as i32,
|
||||
)
|
||||
};
|
||||
if tex.is_null() {
|
||||
return Err(status::ERR_MEMORY);
|
||||
}
|
||||
let tex = crate::render::texture_create(¶ms, image.pixels(), image.row_bytes() as i32)
|
||||
.map_err(|_| status::ERR_MEMORY)?;
|
||||
let props = make_texture_props(
|
||||
tex,
|
||||
&tex,
|
||||
w,
|
||||
h,
|
||||
components,
|
||||
@@ -427,10 +411,10 @@ unsafe extern "C" fn clip_load_texture(
|
||||
})
|
||||
}
|
||||
|
||||
/// 纹理尺寸(经 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 {
|
||||
/// 纹理尺寸(经 [`crate::render::texture_get_params`];失败回退 0,0)。
|
||||
fn texture_size(tex: &crate::render::Texture) -> (f64, f64) {
|
||||
let p = crate::render::texture_get_params(tex);
|
||||
if p.width > 0 {
|
||||
(p.width as f64, p.height as f64)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
@@ -456,8 +440,9 @@ fn clip_par(c: &ClipInstance) -> f64 {
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
/// clipFreeTexture:释放纹理句柄(输入 clip 删除 GL 纹理;Output 只
|
||||
/// 释放句柄不删纹理——宿主还要读它)。
|
||||
/// clipFreeTexture:释放纹理(输入 clip 删除纹理值;Output 只释放
|
||||
/// 句柄不删纹理——宿主还要读它)。纹理是值:摘除表条目即 drop
|
||||
/// (原 `texture_free` 调用面随值模型删除)。
|
||||
unsafe extern "C" fn clip_free_texture(texture_handle: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
if texture_handle.is_null() {
|
||||
@@ -469,13 +454,7 @@ unsafe extern "C" fn clip_free_texture(texture_handle: *mut c_void) -> c_int {
|
||||
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(())
|
||||
}
|
||||
Some((_props, _texture, _is_output)) => Ok(()),
|
||||
None => Err(status::ERR_BAD_HANDLE),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
//! 语义,第 1 期账本不需要 → OK no-op,见 memory.rs 文档)。
|
||||
//!
|
||||
//! `// TODO(clip)`:clipGetImage 依赖 [`crate::clip::ClipInstance::fetch_image`]
|
||||
//! (bridge::render 帧访问 C ABI 未冻结),代码已齐、运行时待 clip.rs。
|
||||
//! (oakrender 帧访问随单库化改为本地桩,见 [`crate::render`]),代码已齐、运行时待 clip.rs。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, c_double, c_int, c_void, CStr};
|
||||
@@ -258,7 +258,8 @@ unsafe extern "C" fn clip_get_property_set(clip: *mut c_void, out: *mut *mut c_v
|
||||
/// clipGetImage:抓取输入图像并登记到存活表,返回图像属性集 handle
|
||||
/// (HS:2003-2049;`getImage` 失败 → Failed)。
|
||||
///
|
||||
/// `// TODO(clip)`:fetch_image 待 bridge::render 帧访问 C ABI。
|
||||
/// `// TODO(clip)`:fetch_image 待 clip 迁移到
|
||||
/// `oakrender::texture::Texture` 值模型(当前帧访问为本地桩)。
|
||||
unsafe extern "C" fn clip_get_image(
|
||||
clip: *mut c_void,
|
||||
time: c_double,
|
||||
|
||||
@@ -23,10 +23,8 @@
|
||||
//! 到本模块的 `oak_ofx_message_impl`。
|
||||
//!
|
||||
//! 消息出口 = facade 注册的 `oakplugin_message_fn`
|
||||
//! (include/plugin/host.h)。注意:骨架 ffi.rs 声明的 `MessageFn`
|
||||
//! 与头文件不符(头文件是 `(type, message, userdata)`,骨架是
|
||||
//! `(userdata, level, message)`)——做 ffi.rs 时以头文件为准修正。
|
||||
//! 本模块按头文件契约建模。
|
||||
//! (include/plugin/host.h)。本模块按头文件契约建模。
|
||||
//! (原 C ABI 出口层已随单库化删除;注册点由 facade 直接调用。)
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void, CStr};
|
||||
|
||||
@@ -37,14 +35,12 @@ use crate::suites::status;
|
||||
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 推导)。
|
||||
/// 消息出口注册表(facade 写入,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`
|
||||
/// 调用;公开:测试直接注入捕获器)。
|
||||
/// 注册/注销消息出口(facade 调用;公开:测试直接注入捕获器)。
|
||||
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);
|
||||
|
||||
@@ -171,13 +171,14 @@ pub(crate) fn current_output() -> Option<std::sync::Arc<crate::image::Image>> {
|
||||
/// 纹理(对应 C++ 里实例渲染期的 GL 状态;ofxGPURender.h
|
||||
/// "OpenGL Current Context" 一节要求宿主在 Render 期间持有 GL
|
||||
/// 上下文——本设计的约定是调用方(oakrender 的 PluginJob 路径)在
|
||||
/// 进入 render_job 前已把渲染器上下文置为 current,本表只传递句柄)。
|
||||
#[derive(Clone, Copy)]
|
||||
/// 进入 render_job 前已把渲染器上下文置为 current,本表只传递
|
||||
/// 渲染器引用与输出纹理值)。
|
||||
#[derive(Clone)]
|
||||
pub struct GlCtx {
|
||||
/// 当前渲染器(oakrender 句柄)。
|
||||
pub renderer: crate::bridge::render::RendererHandle,
|
||||
/// 当前渲染器(oakrender 后端上下文)。
|
||||
pub renderer: crate::render::Renderer,
|
||||
/// 已附着的输出纹理(渲染目标;GL 模式下插件把结果画进它)。
|
||||
pub output_texture: crate::bridge::render::TextureHandle,
|
||||
pub output_texture: crate::render::Texture,
|
||||
/// 当前 GL 纹理的实际像素深度(kOfxBitDepth* 静态串;Phase 2
|
||||
/// 全链路 F32,由 render_gl 按插件 kOfxOpenGLPropPixelDepth 协商
|
||||
/// 后填入——纹理句柄的 kOfxImageEffectPropPixelDepth 以它为准)。
|
||||
@@ -197,7 +198,7 @@ pub fn set_gl_ctx(ctx: Option<GlCtx>) {
|
||||
/// 读取 GL 渲染上下文(无上下文返回 None——clipLoadTexture 在非
|
||||
/// GL 渲染期调用时按规范返回 kOfxStatErrMissingHostFeature)。
|
||||
pub(crate) fn gl_ctx() -> Option<GlCtx> {
|
||||
GL_CTX.with(|c| *c.borrow())
|
||||
GL_CTX.with(|c| c.borrow().clone())
|
||||
}
|
||||
|
||||
/// 宿主进程身份(fetchSuite 的 version 检查用;= OFX API 1.5)。
|
||||
@@ -307,16 +308,45 @@ mod tests {
|
||||
#[test]
|
||||
fn gl_ctx_tls() {
|
||||
assert!(gl_ctx().is_none());
|
||||
let renderer = crate::handle::CHandle::null();
|
||||
let tex = crate::handle::CHandle::null();
|
||||
// 最小 GpuContextLike 假实现(无 GPU 适配器需求)。
|
||||
struct FakeGpu;
|
||||
impl oakrender::backend::GpuContextLike for FakeGpu {
|
||||
fn kind(&self) -> oakrender::backend::BackendKind {
|
||||
oakrender::backend::BackendKind::Cpu
|
||||
}
|
||||
fn destroy_texture(&self, _token: u64) {}
|
||||
fn upload(
|
||||
&self,
|
||||
_token: u64,
|
||||
_frame: &oakrender::texture::Frame,
|
||||
) -> oakrender::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn download(&self, _token: u64) -> oakrender::error::Result<oakrender::texture::Frame> {
|
||||
Ok(oakrender::texture::Frame::new())
|
||||
}
|
||||
fn blit(
|
||||
&self,
|
||||
_src: u64,
|
||||
_dst: u64,
|
||||
_processor: Option<&oakrender::color::ColorProcessor>,
|
||||
) -> oakrender::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
let renderer: crate::render::Renderer = std::sync::Arc::new(FakeGpu);
|
||||
let tex = crate::render::Texture::dummy();
|
||||
set_gl_ctx(Some(GlCtx {
|
||||
renderer,
|
||||
output_texture: tex,
|
||||
output_texture: tex.clone(),
|
||||
gl_pixel_depth: "OfxBitDepthFloat",
|
||||
}));
|
||||
let got = gl_ctx().unwrap();
|
||||
assert!(got.renderer.is_null());
|
||||
assert!(got.output_texture.is_null());
|
||||
assert_eq!(
|
||||
got.renderer.kind(),
|
||||
oakrender::backend::BackendKind::Cpu
|
||||
);
|
||||
assert!(got.output_texture.is_dummy());
|
||||
assert_eq!(got.gl_pixel_depth, "OfxBitDepthFloat");
|
||||
set_gl_ctx(None);
|
||||
assert!(gl_ctx().is_none());
|
||||
|
||||
Reference in New Issue
Block a user