feat(rust): oakcodec/oaktask/oakplugin crates + oakotio + node/render/storage skeletons

- oakcodec: full crate incl. real FFmpeg decode/encode via ffmpeg-next
  (162 tests, 84.4% cov); new oakcodec_encoding_* metadata family
  (include/codec/format.h, C++ + Rust sides)
- oaktask: manager/tasks/project load-save incl. OTIO via oakotio
  (82 tests, 86.4% cov); concurrent render loop with reorder buffer
- oakplugin: M11 phase 1+2 — self-contained OFX host, GL path,
  ofxColour, pluginrenderer absorbed as render_driver (99 tests,
  81.7% cov); instance.h additions documented
- oakotio: native serde-based OTIO read/write (24 tests)
- oaknode gap fill: dragger/keyframe-helper/multicam C ABI families
  (113/113 gtest); fixes a latent NodeInputDragger segfault
- oaknode Rust skeleton: 43 built-in node type declarations
- oakrender/oakstorage: declaration skeletons (implementation pending)
- notes.md: gap analysis + tech-debt ledger
This commit is contained in:
2026-08-09 05:49:15 +08:00
parent 4b24aa9d67
commit cac41d92c1
270 changed files with 68323 additions and 10 deletions
+32
View File
@@ -0,0 +1,32 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cc"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
[[package]]
name = "oakplugin"
version = "0.1.0"
dependencies = [
"cc",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "oakplugin"
version = "0.1.0"
edition = "2021"
description = "Oak Video Editor plugin module: self-contained OFX host and plugin bridge (Rust)"
license = "GPL-3.0-or-later"
# staticlib: linked into liboakplugin by CMake (see M11 §3.3).
# cdylib is NOT used: the module dylib stays a CMake target so the
# standalone-tree wiring (rpath, force_load, tests) is unchanged.
[lib]
crate-type = ["staticlib", "rlib"]
[profile.release]
# FFI 纪律(M11 §0):panic 必须能被 catch_unwind 兜住并映射为错误码,
# 绝不允许 unwind/abort 出 FFI 边界。
panic = "unwind"
[dependencies]
# 只允许零依赖起步。确需引入的 crate 必须在 README 登记理由。
[features]
# oakrender 测试桩(库内 no_mangle 符号 + 状态访问器):cargo test
# --features test-stubs 时桥直连桩,像素路径可在无 liboakrender 的
# 环境下跑通;真实链路仍走 dlsym 解析。
test-stubs = []
[build-dependencies]
# C shim 编译(message suite 的 variadic 入口);理由已登记 README。
cc = "1"
+265
View File
@@ -0,0 +1,265 @@
# oakplugin Rust crateM11 第 1+2 期实现状态)
> 状态:**M11 第 1、2 期已实现**(第 2 期:GL 渲染路径 +
> ofxColour + pluginrenderer 渲染驱动收编)。原声明底稿(类型与
> 函数签名 + 文档注释即规约)已由实现方填充;`src/` 内已无
> `todo!()`。
> 计划:docs/zh/plans/riir/M11-ofx-host.md。
## 结构
```
src/
lib.rs crate 文档、模块图、FFI 纪律
error.rs 错误码(与 include/plugin/error.h 一一对应)
handle.rs 引用计数句柄脚手架({ctx,addref,release,abi_version}
property.rs PropertySetOFX 属性集的存储与类型化读写
suites/ 插件调进来的 C 函数表(unsafe trampoline 层)
mod.rs fetchSuite 注册表 + 渲染/GL 上下文 TLS
property.rs memory.rs image_effect.rs param.rs
message.rs progress.rs timeline.rs multithread.rs
gl_render.rs OfxImageEffectOpenGLRenderSuiteV1M11 §4 新增)
host.rs Host 单例:bundle 扫描、插件缓存、action 分发
descriptor.rs EffectDescriptor/ClipDescriptordescribe 的产物)
instance.rs Instanceaction 调用面(render/协商/RoD/RoI/isIdentity/
render_gl/GetOutputColourspace
render_driver.rs pluginrenderer.cpp 渲染流程收编(M11 §4 新增)
clip.rs ClipInstanceclip↔oakrender 纹理桥(bridge::render
image.rs Image:帧缓冲/纹理的 OFX 视图
param.rs 12 种参数实例 + param↔oaknode 桥(bridge::node/undo
progress.rs PluginProgressReporter
bridge/ oak 其余模块的 C ABI 导入(extern "C" 声明)
node.rs render.rs undo.rs
ffi.rs include/plugin/*.h 的全部导出(C ABI 出口层)
```
## 桥布局决策(M11 第 1 期冻结;第 2 期增补)
- **`bridge::node::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)无 POD 表示,走 `*_input_string_*` 专用桥函数
`set_input_string_undoable`)。`ffi::OakNodeValue` 是同布局的
出口层镜像(两处独立声明避免模块环)。
- **帧访问 C ABI**`bridge::render`= `oakrender_display_texture_*`
`get_frame`/`is_dummy`+ `oakrender_codec_frame_*`
`get_params`/`data`/`free`),与 `include/render/renderer.h` 一致;
`oakrender_display_texture_wrap_native` 是 C++ 专属符号(TexturePtr
引用),Rust 不可调——**输出纹理由 oakrender 侧创建并经句柄传入**
`ClipInstance::set_output_texture` 挂入后,`store_output_image`
其 CPU 帧整帧拷贝(全链路 F32,尺寸不符明确报错)。
- **GL 纹理桥**M11 §4 增补):`bridge::render` 增加渲染器族
`renderer_create_dynamic`/`init`/`is_open_gl`/`destroy`)与纹理族
`texture_create`/`upload`/`download`/`retain`/`free`/`id`/
`get_params`/`renderer_download_from_texture`),以及
`frame_linesize_bytes`(CPU 拷贝的行跨度感知修复)。行跨度单位
统一为**字节**renderer.h:206 明文契约;C++ 调用点传像素行跨度,
由 oakrender 侧实现 C ABI 时换算)。
- **undo 打包**`bridge::undo`= `oakundo_command_init_multi`/
`redo_now`/`multi_add_child`/`free`paramEditBegin/End 的编辑事务
`Instance` 上维护深度与累积 multi(C++ `submit_undo_command` 语义:
事务内子命令立即 redo 生效并并入 multieditEnd 整体 redo+释放)。
- **param↔node 值转换**`param.rs`):`set_from_node`(节点→插件,
按参数类型映射,维度截断/补零)、`to_node_value`(插件→节点,
RGB 补 alpha=1,与 C++ `RGBInstance::set` 一致)。字符串参数经
`oaknode_node_set_input_string_undoable`
- **节点绑定**`Instance::bind_node(identity)`C++ `set_node_handle`
的 Rust 侧);回写只在 `ChangeReason::PluginEdited` 触发,未绑定/
身份查无时 no-op。
## M11 第 2 期:GL 路径 + ofxColour + 渲染驱动
### GL 渲染路径
- `suites/gl_render.rs`OfxImageEffectOpenGLRenderSuiteV1
clipLoadTexture / clipFreeTexture / flushResources),语义对照
ofxGPURender.hvendored)与 HostSupport 插件侧(ofxhImageEffect.cpp:
2296-2367)。纹理句柄是属性集(12 个属性:OpenGLTextureIndex/
OpenGLTextureTarget/PixelDepth/Components/PreMultiplication/
RenderScale/PixelAspectRatio/Bounds/ROD/RowBytes/Field/
UniqueIdentifier);存活表持有 Box<PropertySet>(句柄地址稳定)。
Output clip 的渲染目标绑定由调用方契约保证(等价 C++
`attach_output_texture`);clipFreeTexture 对 Output 不删纹理。
- GL render action`Instance::render_gl`(与 CPU `render` 并存)——
action 序列 kOfxActionOpenGLContextAttached → renderin args 带
kOfxImageEffectPropOpenGLEnabled=1)→ OpenGLContextDetached
GL 模式无 CPU 输出回读,渲染结果留在附着纹理上。
- **GL 上下文规则**ofxGPURender.h "OpenGL Current Context"):宿主
只在 Render/Begin/EndSequenceRender/Attach/Detach 期间要求上下文
current——本实现的约定是调用方(oakrender PluginJob 路径)在
进入 render_job 前把渲染器上下文置为 current 并附着输出纹理
(文档见 include/plugin/instance.h 的新增声明)。
- 格式协商:插件描述符声明 kOfxImageEffectPropOpenGLRenderSupported
"false"/"true"/"needed")与 kOfxOpenGLPropPixelDepth(可选位深
列表);宿主 `pick_gl_pixel_depth` 按管线 F32 约束选型(声明列表
不含 Float → GL 模式不可行,回退 CPU)。use_opengl 决策在
render_driver。
### ofxColourOFX 1.4
- 宿主描述符声明 OCIO 色彩管理(kOfxImageEffectPropColourManagementStyle
= OCIO + AvailableConfigs = ofx-native-v1.5_aces-v1.3_ocio-v2.3);
实例期协商属性(style/config/OCIOConfig=ocio://default,工作空间
ACEScg 经 clip 色彩空间属性传达)。
- 输入 clip 的 kOfxImageClipPropColourspace 宿主写为 ACEScg
GetOutputColourspace action`Instance::get_output_colourspace`):
偏好列表采纳 + "OfxColourspace_<clip>" 交叉引用解析
`resolve_colourspace`),结果写回输出 clip
`set_output_colourspace`);插件未实现 → 第一个输入 clip 的
色彩空间(规范默认)。
- 描述符预定义 GL/colour 属性(ofxGPURender.h/ofxColour.h):HostSupport
的 propSet 不创建属性(返回 ErrUnknown),宿主预定义属性宇宙——
第 1 期缺 GL/colour 声明,第 2 期补齐(见"第 1 期修复")。
### render_driverpluginrenderer.cpp 收编)
- `render_driver.rs``PluginRenderer::render_plugin`1857 行 C++
的渲染流程部分)的 Rust 移植——实例锁、use_opengl 决策、多输入
clip 收集(effect_input_id / inputs 表 / SimpleSource 回退)、
getClipPreferences、RoI/RoD 设定、getRegionOfInterest(失败按默认
整帧继续,C++ 对 BadHandle 即如此)、输出 clip 格式、isIdentity
短路(透传帧直接拷贝,不调 render action)、参数覆盖
apply_param_overrides 移植)、CPU/GL 渲染与输出装配。
- 序列括号:`render_begin_sequence`/`render_end_sequence` 的 C ABI
(oakrender 对同一实例的一批帧先 begin 后 end,中间逐帧
`render_job`)。
- 目标形态:oakrender 的 PluginJob 退化为一次 C ABI 调用
`oakplugin_instance_render_job`instance.h 新增,见下)。
### include/plugin/instance.h 新增(M11 §4,既有签名不变)
- `oakplugin_instance_render_begin_sequence` / `render_end_sequence`
序列括号。
- `oakplugin_instance_render_job(instance, dst, time, clear, interactive,
effect_input_id, src, inputs[], values[], renderer)`:一帧渲染的
单一调用;`oakplugin_job_value`(参数覆盖)/`oakplugin_job_texture`
(输入 clip 纹理)两个 POD 随附。GL 契约(上下文 current + 输出
附着)见头文件文档注释。
## 与 M11 §3.5 验收的对照(第 1+2 期现状)
| 验收项 | 状态 |
| --- | --- |
| 0 期 golden master 全绿(描述符 diff 空、CPU 渲染 bit 一致) | **未达成**:0 期基建(`tests/ofx/` 的快照/帧库)尚未生成,`golden_test.rs` 对应用例 `#[ignore]`(原因见各用例 doc);渲染 golden 另依赖真 liboakrender 的桥。 |
| 测试插件 round-trip + CImg 全量 describe/协商冒烟 | **部分**:测试插件 round-trip 绿(suites/ffi/lifecycle/negotiation/colour/gl/driver);CImg 全量冒烟被本机 CImg bundle(Natron 分支,属性套件句柄语义不兼容)所阻——`#[ignore]``system_misc_ofx_bundle_smoke` 在本机 Misc bundle(同样为 Natron 分支,describe 返回 MissingHostFeature)上 skip,兼容 bundle 的机器照常断言。 |
| 生命周期:create/destroy 配对、重复 scan、render 中途 cancel、alive 无泄漏 | **达成**lifecycle_test 全绿(含 256 次循环与 stub 渲染取消)。 |
| nmliboaknode/liboakrender→liboakplugin C++ 符号为 0HostSupport 移出构建 | 由外层 CMake 接线验证(本 crate 侧:ffi.rs 仅导出 C ABI)。 |
| cargo testhost 内部单测)+ ctestC ABI 层)双绿 | **达成**cargo test 两种模式全绿;ctest 属 standalone 树外层)。 |
| M11 §4GL 路径 + ofxColour + pluginrenderer 收编 | **达成**(本机验证):OpenGLRender suite v1 + render_glGL 上下文/格式协商规则按 ofxGPURender.h);ofxColour 属性族 + GetOutputColourspaceACEScg 工作空间);render_driver 收编 + render_job C ABI(见上)。GL goldenEXR 容差)需 0 期基建 + 本机 GPU 人工确认(`OAK_GPU_TESTS` 门)。 |
## 实现纪律(实现方必读)
1. 所有 `extern "C"` 函数体必须包 `crate::handle::guard(..)`
catch_unwind + 错误码映射),禁止 panic 越过 FFI。
2. 句柄全部经 `handle.rs` 的 `RefBox<T>``ctx` 永不裸指针外露含义。
3. 共享状态(插件缓存、instance 注册表、线程表)一律 `Mutex`
插件可能在其自起线程回调任意 suiteMultiThread suite 存活期)。
4. OFX 语义以 HostSupport 的行为为参照系;每个协商/时序实现点
必须注释对应 HostSupport 文件行号。
5. 错误码、句柄布局、字符串两段式与 `include/plugin/*.h` 逐字一致。
6. 依赖政策:**优先使用成熟第三方 crate**serde/quick-xml/thiserror
等),不重复造轮子。选型要求:crates.io 有维护、许可
MIT/Apache-2.0/BSDGPL 兼容);新增依赖在 README 登记名称与
理由。OTIO 等大型既有 C++ 库绝不重写——继续经其 C ABI/桥接层
使用。
7. 跨期句柄(suite 返回的纹理/图像句柄)必须指向**堆上稳定对象**
(Box/Arc)——栈上临时对象在 suite 返回后悬垂(M11 第 2 期
gl_render 初版踩过,见"第 1 期修复")。
## 已知技术点(实现方注意)
- **C 变长参数**`paramGetValue`/`paramSetValue` 等是 variadic。
stable Rust 不能 *定义* C-variadic 函数(`c_variadic` 仍不稳定)。
两个选项:(a) 这几个函数用 nightly 的 `c_variadic`(b) 用
build.rs 编一个几十行的 C shim 只承载 variadic 入口再转发。
选 (b) 保持 stable toolchain。`core::ffi::VaList` 类型本身稳定,
仅"定义 variadic fn"受限。
**已落地**message suite 的 v1/v2 入口即 C shim
cbits/ofx_message_shim.cbuild.rs 经 `cc` 编译;`cc` 仅 build
依赖,理由见下)。param suite 的 variadic 同理(届时同方案)。
- **依赖登记**`cc`build-dependencies)——编译 C shim 的唯一
稳妥方式(手写 `Command::new("cc")` 无法处理跨平台 flag/交叉编译;
零运行时依赖不变)。M11 第 2 期未新增依赖。
- suite 函数表经 `suite_v1()` 等 accessor 暴露(`static` 初始化
放 lazy/OnceLock 里),`fetch_suite` 只查表。
- **GL 上下文归属**oakplugin 不持有 GL 上下文(C ABI 无
make-current 函数);render_job 的 GL 契约要求调用方(oakrender
置上下文 current 并附着输出纹理。GL 纹理上传/回读经 oakrender C
ABI(其内部确保上下文)。
## 第 1 期修复(M11 第 2 期发现并修复的 phase-1 缺陷)
1. **ffi::oakplugin_instance_render 泄漏帧句柄**render 失败路径
`?` 提前返回时不释放 `frame`texture_get_frame 的保留引用)。
修复:统一走 `render_driver::write_output_frame`,所有路径释放。
2. **CPU 拷贝假设紧凑行**fetch_image / store_output_image / ffi
render 用紧凑行宽拷贝——真实 oakrender 帧可有行填充。修复:
新增 `frame_linesize_bytes` 导入,行优先拷贝按 linesize。
3. **描述符未预定义 GL/colour 属性**:属性 suite 的 propSet 对未
定义属性返回 ErrUnknown(与 HostSupport 一致:宿主预定义属性
宇宙);第 1 期描述符表缺 kOfxImageEffectPropOpenGLRenderSupported/
kOfxOpenGLPropPixelDepth/ofxColour 族 → 插件无法声明 GL/colour
能力。修复:`init_descriptor_props` 补齐预定义(默认
"false"/空数组/None)。
## 测试
运行(全量,含渲染像素路径的 oakrender 测试桩):
```sh
cargo test --features test-stubs
cargo tarpaulin --out stdout --features test-stubs # 覆盖率门槛
```
- 默认 `cargo test` 亦可:桥经 dlsym 解析 oakrender/oaknode/oakundo
符号(缺失时渲染/回写边界给出可解释错误),桩相关用例不编译;
- `--features test-stubs`:库内 oakrender/oaknode/oakundo 桩
`bridge::*::stub`),ffi/clip/param 的桥调用(含像素读写、
节点回写、undo 打包)全链路可跑——**覆盖率以该模式为准**
M11 第 1 期实测 82.93% 行覆盖;第 2 期门槛 ≥80%,见 COVERAGE);
- 最小测试插件(cbits/oak_test_plugin.c,三个入口:
org.oak.test-plugin / org.oak.test-plugin.gl /
org.oak.test-plugin.identity)由 build.rs 编译为共享库,
`common::test_plugin_dir` 运行时装配成 bundle;不可用时相关用例
skip
- 宿主单例无锁:触碰宿主面的用例经 `common::with_host` 串行化。
- **系统级冒烟**`bridge_test::system_misc_ofx_bundle_smoke` 在
`/Library/OFX/Plugins/Misc.ofx.bundle` 存在时扫描→describe→
create_instance→协商;本机 bundle 为 Natron 分支(describe 返回
MissingHostFeature)时 skipREADME 已记录),兼容 bundle 的机器
照常断言。
- **GL 用例策略**`gl_render_test.rs`):`test-stubs` 模式下桩 GL
渲染器(renderer_is_open_gl/texture_create/texture_id 等)模拟
GPU——suite 往返、GL render 路径(attach/detach 配对、插件经
message 上报纹理索引)、错误路径全链路可跑;默认模式(无
liboakrender)断言优雅降级(GL suite 无上下文 →
MissingHostFeature、render 回退 CPU)。真实 GPU golden 为
`OAK_GPU_TESTS` 门(`common::gpu_available`),CI 跳过。
- **golden 用例**`golden_test.rs`)因 M11 0 期基建(tests/ofx/ 的
快照与帧库)尚未生成而 `#[ignore]`(原因写在各用例 doc);其中
CImg 全量冒烟另受本机 CImg bundle 的 Natron 分支句柄语义所阻
(详见用例注释),真实插件路径由 Misc 系统冒烟覆盖。
TDD:测试声明与实现声明同步冻结(tests/,函数体 `todo!()`):
- `handle_test.rs` / `property_test.rs` — 基础设施契约(引用计数、
free 容错、Registry、属性集语义、并发)。
- `suites_test.rs` — 八张 suite 的 round-trip(经最小测试插件,
"插件视角"的 HostSupport 兼容性背书)。
- `ffi_host_test.rs` / `ffi_instance_test.rs` — C ABI 出口契约
(每函数一正常一错误路径;两段式字符串边界)。
- `lifecycle_test.rs` — 创建/销毁配对、重复扫描、渲染取消原子性、
256 次循环无泄漏、并发实例。
- `negotiation_test.rs` — 协商重灾区专项(分量/位深矩阵、RoD/RoI、
isIdentity、field 透传、sequence 括号)。
- `colour_test.rs` — ofxColour 属性族 + GetOutputColourspace 往返
(偏好采纳、交叉引用解析、输出写回;ACEScg 工作空间)。
- `gl_render_test.rs` — GL suite 往返/错误路径/像素深度协商矩阵 +
GL render 路径端到端(无 GPU 优雅跳过策略见上)。
- `render_driver_test.rs` — render_job CPU 路径(序列括号、多输入、
参数覆盖、isIdentity 透传像素断言、无桩降级)。
- `bridge_test.rs` — node/render/undo 三桥(`--features test-stubs`
全链路;默认模式走 dlsym 缺失的降级路径)+ Misc 系统冒烟。
- `golden_test.rs` — 描述符快照 diff + CPU bit 级/GL 容差渲染
golden + F32+ACEScg 链路断言(0 期基建未落地,见上)。
- `common/mod.rs` — 测试插件定位、golden 目录、GPU 门、skip 约定。
+61
View File
@@ -0,0 +1,61 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! 构建脚本:编译 C shimcbits/)。动机见 README 的依赖登记
//! "C 变长参数"条目):stable Rust 不能定义 C-variadic 函数,
//! 入口留在 C。`cc` crate 只参与构建,不进产物。
fn main() {
println!("cargo:rerun-if-changed=cbits/ofx_message_shim.c");
cc::Build::new()
.file("cbits/ofx_message_shim.c")
.warnings(true)
.compile("ofx_message_shim");
println!("cargo:rerun-if-changed=cbits/ofx_param_shim.c");
cc::Build::new()
.file("cbits/ofx_param_shim.c")
.warnings(true)
.compile("ofx_param_shim");
// 最小测试插件(M11 §2.4):共享库(dlopen 目标),运行时由
// common::test_plugin_dir 装配成 bundle 目录。
// cc 的 compile() 只产静态库,直接调系统编译器出 dylib/so。
println!("cargo:rerun-if-changed=cbits/oak_test_plugin.c");
build_test_plugin();
}
/// 编译最小测试插件为共享库($OUT_DIR/oak_test_plugin.{dylib,so})。
fn build_test_plugin() {
use std::process::Command;
let out = std::env::var("OUT_DIR").expect("OUT_DIR");
let cc = std::env::var("CC").unwrap_or_else(|_| "cc".into());
let (link_flag, ext) = if cfg!(target_os = "macos") {
("-dynamiclib", "dylib")
} else {
("-shared", "so")
};
let status = Command::new(&cc)
.args([
"-fPIC",
"-I../../../third_party/openfx/include",
"cbits/oak_test_plugin.c",
link_flag,
"-o",
&format!("{out}/oak_test_plugin.{ext}"),
])
.status()
.expect("编译测试插件失败");
assert!(status.success(), "测试插件编译失败");
}
+545
View File
@@ -0,0 +1,545 @@
/*
* 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/>.
*/
/*
* 最小测试插件(M11 §2.4 的交付物;build.rs 编译为共享库,
* 运行时装配成 oak-test-plugin.ofx.bundle 供 host 扫描)。
*
* filter 上下文:
* - 参数:gainDouble,默认 0
* - clipSource(输入,RGBA)、Output
* - describe/describeInContext 建参数与 clip
* - getClipPreferences:全链路 F32+RGBA、帧率 24
* - render:把输出图像填成常量 0.5RGBA float),alpha=1
* - getRoDproject sizegetRoI:原样返回 region
* - isIdentity:不设 → 非透传;
* - begin/endSequenceRender:记日志(状态码 OK)。
*/
#include <stdio.h>
#include <string.h>
#include "ofxCore.h"
#include "ofxColour.h"
#include "ofxGPURender.h"
#include "ofxImageEffect.h"
#include "ofxMessage.h"
#include "ofxParam.h"
#include "ofxProgress.h"
#include "ofxProperty.h"
/* kOfxImageEffectGLFormatRGBA 在 vendored ofxOpenGLRender.h 是 stub
* 未收录(OpenFX 1.4 规范名),按规范定义。kOfxImageEffectPropIsIdentity
* 同理(vendored ofxImageEffect.h 只文档化了该属性,未给宏)。 */
#define GL_FORMAT_RGBA "OfxImageEffectGLFormatRGBA"
#define kOfxImageEffectPropIsIdentity "OfxImageEffectPropIsIdentity"
static OfxHost *g_host = NULL;
static const OfxPropertySuiteV1 *g_propSuite = NULL;
static const OfxImageEffectSuiteV1 *g_imageEffectSuite = NULL;
static const OfxParameterSuiteV1 *g_paramSuite = NULL;
static const OfxMessageSuiteV1 *g_messageSuite = NULL;
static const OfxProgressSuiteV1 *g_progressSuite = NULL;
static const OfxImageEffectOpenGLRenderSuiteV1 *g_glSuite = NULL;
/* ---------- suite 便捷封装 ---------- */
static OfxStatus propSetString(OfxPropertySetHandle h, const char *name, int index, const char *v)
{
return g_propSuite->propSetString(h, name, index, v);
}
static OfxStatus propSetStringN(OfxPropertySetHandle h, const char *name, int count, const char **v)
{
return g_propSuite->propSetStringN(h, name, count, v);
}
static OfxStatus propSetDoubleN(OfxPropertySetHandle h, const char *name, int count, const double *v)
{
return g_propSuite->propSetDoubleN(h, name, count, v);
}
static OfxStatus propGetDouble(OfxPropertySetHandle h, const char *name, int index, double *v)
{
return g_propSuite->propGetDouble(h, name, index, v);
}
static OfxStatus propGetInt(OfxPropertySetHandle h, const char *name, int index, int *v)
{
return g_propSuite->propGetInt(h, name, index, v);
}
static OfxStatus propGetIntN(OfxPropertySetHandle h, const char *name, int count, int *v)
{
return g_propSuite->propGetIntN(h, name, count, v);
}
static OfxStatus propGetPointer(OfxPropertySetHandle h, const char *name, int index, void **v)
{
return g_propSuite->propGetPointer(h, name, index, v);
}
/* 协商期 per-clip 属性名:前缀_clip */
static void setClipPref(OfxPropertySetHandle out, const char *prefix, const char *clip, const char *value)
{
char name[256];
snprintf(name, sizeof(name), "%s_%s", prefix, clip);
propSetString(out, name, 0, value);
}
/* ---------- setHostmandatory 第一个调用) ---------- */
static void setHost(OfxHost *host)
{
g_host = host;
if (!host)
return;
g_propSuite = (const OfxPropertySuiteV1 *)host->fetchSuite(host->host, kOfxPropertySuite, 1);
g_imageEffectSuite =
(const OfxImageEffectSuiteV1 *)host->fetchSuite(host->host, kOfxImageEffectSuite, 1);
g_paramSuite = (const OfxParameterSuiteV1 *)host->fetchSuite(host->host, kOfxParameterSuite, 1);
g_messageSuite = (const OfxMessageSuiteV1 *)host->fetchSuite(host->host, kOfxMessageSuite, 1);
g_progressSuite = (const OfxProgressSuiteV1 *)host->fetchSuite(host->host, kOfxProgressSuite, 1);
g_glSuite = (const OfxImageEffectOpenGLRenderSuiteV1 *)host->fetchSuite(
host->host, kOfxOpenGLRenderSuite, 1);
}
/* ---------- action 实现 ---------- */
static OfxStatus actionDescribe(const void *handle, int is_gl)
{
const char *comps[] = { kOfxImageComponentRGBA, kOfxImageComponentRGB, kOfxImageComponentAlpha };
propSetString(handle, kOfxPropLabel, 0, "Oak Test Plugin");
propSetStringN(handle, kOfxImageEffectPropSupportedContexts, 1,
&(const char *){ kOfxImageEffectContextFilter });
/* ofxColourM11 §4):声明 OCIO 色彩管理能力与可用配置。 */
propSetString(handle, kOfxImageEffectPropColourManagementStyle, 0,
kOfxImageEffectColourManagementOCIO);
{
const char *configs[] = { "ofx-native-v1.5_aces-v1.3_ocio-v2.3" };
propSetStringN(handle, kOfxImageEffectPropColourManagementAvailableConfigs, 1, configs);
}
/* GL 能力(M11 §4):GL 变体声明 "true" + 支持位深(F32)。
* CPU 变体保持默认 "false"。 */
if (is_gl) {
propSetString(handle, kOfxImageEffectPropOpenGLRenderSupported, 0, "true");
{
const char *depths[] = { kOfxBitDepthFloat };
propSetStringN(handle, kOfxOpenGLPropPixelDepth, 1, depths);
}
}
/* 参数族:gainDouble,带 display min/max)、modeChoice
* 两选项)、debugDoublesecret)、labelString)。 */
OfxPropertySetHandle paramProps = NULL;
OfxStatus st = g_paramSuite->paramDefine((OfxParamSetHandle)handle, kOfxParamTypeDouble, "gain", &paramProps);
if (st != kOfxStatOK)
return st;
propSetString(paramProps, kOfxPropLabel, 0, "Gain");
{
const double min = -2.0, max = 2.0;
g_propSuite->propSetDouble(paramProps, kOfxParamPropDisplayMin, 0, min);
g_propSuite->propSetDouble(paramProps, kOfxParamPropDisplayMax, 0, max);
}
st = g_paramSuite->paramDefine((OfxParamSetHandle)handle, kOfxParamTypeChoice, "mode", &paramProps);
if (st != kOfxStatOK)
return st;
propSetString(paramProps, kOfxPropLabel, 0, "Mode");
{
const char *options[] = { "Fast", "High" };
g_propSuite->propSetStringN(paramProps, kOfxParamPropChoiceOption, 2, options);
}
st = g_paramSuite->paramDefine((OfxParamSetHandle)handle, kOfxParamTypeDouble, "debug", &paramProps);
if (st != kOfxStatOK)
return st;
g_propSuite->propSetInt(paramProps, kOfxParamPropSecret, 0, 1);
st = g_paramSuite->paramDefine((OfxParamSetHandle)handle, kOfxParamTypeString, "label", &paramProps);
if (st != kOfxStatOK)
return st;
propSetString(paramProps, kOfxPropLabel, 0, "Label");
/* clipSource + Output。 */
OfxPropertySetHandle clipProps = NULL;
st = g_imageEffectSuite->clipDefine((OfxImageEffectHandle)handle, "Source", &clipProps);
if (st != kOfxStatOK)
return st;
propSetStringN(clipProps, kOfxImageEffectPropSupportedComponents, 3, comps);
propSetString(clipProps, kOfxPropLabel, 0, "Source");
st = g_imageEffectSuite->clipDefine((OfxImageEffectHandle)handle, "Output", &clipProps);
if (st != kOfxStatOK)
return st;
propSetStringN(clipProps, kOfxImageEffectPropSupportedComponents, 3, comps);
return kOfxStatOK;
}
static OfxStatus actionGetClipPreferences(OfxPropertySetHandle outArgs)
{
setClipPref(outArgs, "OfxImageClipPropComponents", "Source", kOfxImageComponentRGBA);
setClipPref(outArgs, "OfxImageClipPropDepth", "Source", kOfxBitDepthFloat);
setClipPref(outArgs, "OfxImageClipPropComponents", "Output", kOfxImageComponentRGBA);
setClipPref(outArgs, "OfxImageClipPropDepth", "Output", kOfxBitDepthFloat);
g_propSuite->propSetDouble(outArgs, kOfxImageEffectPropFrameRate, 0, 24.0);
g_propSuite->propSetString(outArgs, kOfxImageClipPropFieldOrder, 0, kOfxImageFieldNone);
return kOfxStatOK;
}
static OfxStatus actionGetRoD(OfxPropertySetHandle outArgs)
{
const double rod[4] = { 0.0, 0.0, 1920.0, 1080.0 };
propSetDoubleN(outArgs, kOfxImageEffectPropRegionOfDefinition, 4, rod);
return kOfxStatOK;
}
static OfxStatus actionGetRoI(OfxImageEffectHandle inst, OfxPropertySetHandle inArgs,
OfxPropertySetHandle outArgs)
{
/* 对每个输入 clip 回填 region(此处原样返回 Output 的 window 即可)。 */
OfxImageClipHandle clip = NULL;
OfxPropertySetHandle clipProps = NULL;
OfxStatus st = g_imageEffectSuite->clipGetHandle(inst, "Source", &clip, &clipProps);
if (st != kOfxStatOK)
return st;
double region[4] = { 0.0, 0.0, 0.0, 0.0 };
st = g_propSuite->propGetDoubleN(inArgs, kOfxImageEffectPropRegionOfInterest, 4, region);
if (st != kOfxStatOK)
return st;
char name[256];
snprintf(name, sizeof(name), "OfxImageEffectPropRegionOfInterest_%s", "Source");
st = propSetDoubleN(outArgs, name, 4, region);
fprintf(stderr, "DBG getRoI read=(%g,%g,%g,%g) set st=%d\n", region[0], region[1], region[2], region[3], st);
if (st != kOfxStatOK)
return st;
return kOfxStatOK;
}
static OfxStatus actionRender(OfxImageEffectHandle inst, OfxPropertySetHandle inArgs)
{
double time = 0.0;
propGetDouble(inArgs, kOfxPropTime, 0, &time);
OfxImageClipHandle clip = NULL;
OfxPropertySetHandle clipProps = NULL;
OfxStatus st = g_imageEffectSuite->clipGetHandle(inst, "Output", &clip, &clipProps);
if (st != kOfxStatOK)
return st;
OfxPropertySetHandle image = NULL;
st = g_imageEffectSuite->clipGetImage(clip, time, NULL, &image);
if (st != kOfxStatOK)
return st;
void *data = NULL;
int rowBytes = 0;
int bounds[4] = { 0, 0, 0, 0 };
propGetPointer(image, kOfxImagePropData, 0, &data);
propGetInt(image, kOfxImagePropRowBytes, 0, &rowBytes);
propGetIntN(image, kOfxImagePropBounds, 4, bounds);
/* 进度:Start → Update(0.5)(取消则中止)→ End。 */
if (g_progressSuite) {
g_progressSuite->progressStart((OfxImageEffectHandle)inst, "render");
OfxStatus ps = g_progressSuite->progressUpdate((OfxImageEffectHandle)inst, 0.5);
if (ps != kOfxStatOK) {
g_progressSuite->progressEnd((OfxImageEffectHandle)inst);
return kOfxStatFailed;
}
}
int w = bounds[2] - bounds[0];
int h = bounds[3] - bounds[1];
if (data && w > 0 && h > 0) {
/* 常量 0.5RGBA float);alpha=1。 */
for (int y = 0; y < h; y++) {
float *row = (float *)((char *)data + (size_t)y * (size_t)rowBytes);
for (int x = 0; x < w; x++) {
row[x * 4 + 0] = 0.5f;
row[x * 4 + 1] = 0.5f;
row[x * 4 + 2] = 0.5f;
row[x * 4 + 3] = 1.0f;
}
}
}
g_imageEffectSuite->clipReleaseImage(image);
if (g_progressSuite) {
g_progressSuite->progressEnd((OfxImageEffectHandle)inst);
}
return kOfxStatOK;
}
/* ---------- ofxColourM11 §4):GetOutputColourspace ---------- */
static OfxStatus actionGetOutputColourspace(OfxPropertySetHandle inArgs,
OfxPropertySetHandle outArgs)
{
/* 优先采纳宿主偏好的第一个色彩空间;否则交叉引用 Source clip
* ofxColour.hcross-reference 是合法回写,宿主须解析)。 */
int n = 0;
g_propSuite->propGetDimension(inArgs, kOfxImageClipPropPreferredColourspaces, &n);
if (n > 0) {
char *pref = NULL;
OfxStatus st = g_propSuite->propGetString(
inArgs, kOfxImageClipPropPreferredColourspaces, 0, &pref);
if (st == kOfxStatOK && pref) {
propSetString(outArgs, kOfxImageClipPropColourspace, 0, pref);
return kOfxStatOK;
}
}
propSetString(outArgs, kOfxImageClipPropColourspace, 0, "OfxColourspace_Source");
return kOfxStatOK;
}
/* ---------- isIdentity 变体:透传 Source ---------- */
static OfxStatus actionIsIdentity(OfxPropertySetHandle inArgs, OfxPropertySetHandle outArgs)
{
/* 恒透传 Source(时间不变)——宿主应短路 render 直接拷贝 Source
* 的帧。 */
propSetString(outArgs, kOfxImageEffectPropIsIdentity, 0, "Source");
return kOfxStatOK;
}
/* ---------- GL 变体(M11 §4):attach/detach 与 GL render ---------- */
static OfxStatus actionGLAttached(OfxImageEffectHandle handle)
{
if (g_messageSuite) {
g_messageSuite->message(handle, kOfxMessageMessage, "gl-test", "gl-attached");
}
return kOfxStatOK;
}
static OfxStatus actionGLDetached(OfxImageEffectHandle handle)
{
if (g_messageSuite) {
g_messageSuite->message(handle, kOfxMessageMessage, "gl-test", "gl-detached");
}
return kOfxStatOK;
}
/* GL render:经 OpenGL suite 取 Source 与 Output 纹理并上报索引
* (宿主侧测试经 message 捕获断言)。GL 未使能时回退 CPU render。 */
static OfxStatus actionRenderGL(OfxImageEffectHandle inst, OfxPropertySetHandle inArgs)
{
int gl_enabled = 0;
g_propSuite->propGetInt(inArgs, kOfxImageEffectPropOpenGLEnabled, 0, &gl_enabled);
if (!gl_enabled) {
return actionRender(inst, inArgs);
}
if (!g_glSuite) {
return kOfxStatErrMissingHostFeature;
}
double time = 0.0;
propGetDouble(inArgs, kOfxPropTime, 0, &time);
/* 输入 clipclipLoadTexture(请求 RGBA)。 */
OfxImageClipHandle clip = NULL;
OfxPropertySetHandle clipProps = NULL;
OfxStatus st = g_imageEffectSuite->clipGetHandle(inst, "Source", &clip, &clipProps);
if (st != kOfxStatOK)
return st;
OfxPropertySetHandle tex = NULL;
st = g_glSuite->clipLoadTexture(clip, time, GL_FORMAT_RGBA, NULL, &tex);
if (st != kOfxStatOK)
return st;
int src_index = 0;
g_propSuite->propGetInt(tex, kOfxImageEffectPropOpenGLTextureIndex, 0, &src_index);
if (g_messageSuite) {
g_messageSuite->message(inst, kOfxMessageMessage, "gl-test", "gl-source-index=%d", src_index);
}
st = g_glSuite->clipFreeTexture(tex);
if (st != kOfxStatOK)
return st;
/* 输出 clipclipLoadTexture(Output)format 忽略)。 */
st = g_imageEffectSuite->clipGetHandle(inst, "Output", &clip, &clipProps);
if (st != kOfxStatOK)
return st;
tex = NULL;
st = g_glSuite->clipLoadTexture(clip, time, NULL, NULL, &tex);
if (st != kOfxStatOK)
return st;
int out_index = 0;
g_propSuite->propGetInt(tex, kOfxImageEffectPropOpenGLTextureIndex, 0, &out_index);
if (g_messageSuite) {
g_messageSuite->message(inst, kOfxMessageMessage, "gl-test", "gl-output-index=%d", out_index);
}
st = g_glSuite->clipFreeTexture(tex);
if (st != kOfxStatOK)
return st;
return kOfxStatOK;
}
static OfxStatus mainEntry(const char *action, const void *handle, OfxPropertySetHandle inArgs,
OfxPropertySetHandle outArgs)
{
if (strcmp(action, kOfxActionDescribe) == 0 ||
strcmp(action, kOfxImageEffectActionDescribeInContext) == 0) {
return actionDescribe(handle, 0);
}
if (strcmp(action, kOfxActionCreateInstance) == 0) {
/* 经 message suite 发一条(facade 处理器断言用)。 */
if (g_messageSuite) {
g_messageSuite->message(handle, kOfxMessageMessage, "test-plugin",
"created id=%d", 7);
}
return kOfxStatOK;
}
if (strcmp(action, kOfxActionDestroyInstance) == 0 ||
strcmp(action, kOfxActionLoad) == 0 ||
strcmp(action, kOfxActionUnload) == 0 ||
strcmp(action, kOfxImageEffectActionBeginSequenceRender) == 0 ||
strcmp(action, kOfxImageEffectActionEndSequenceRender) == 0 ||
strcmp(action, kOfxImageEffectActionIsIdentity) == 0) {
return kOfxStatOK;
}
if (strcmp(action, kOfxImageEffectActionGetClipPreferences) == 0) {
return actionGetClipPreferences(outArgs);
}
if (strcmp(action, kOfxImageEffectActionGetRegionOfDefinition) == 0) {
return actionGetRoD(outArgs);
}
if (strcmp(action, kOfxImageEffectActionGetRegionsOfInterest) == 0) {
return actionGetRoI((OfxImageEffectHandle)handle, inArgs, outArgs);
}
if (strcmp(action, kOfxImageEffectActionGetOutputColourspace) == 0) {
return actionGetOutputColourspace(inArgs, outArgs);
}
if (strcmp(action, kOfxImageEffectActionRender) == 0) {
return actionRender((OfxImageEffectHandle)handle, inArgs);
}
return kOfxStatReplyDefault;
}
/* GL 变体入口:describe 带 GL 声明;render 走 GL suiteattach/
* detach 上报。 */
static OfxStatus mainEntryGL(const char *action, const void *handle,
OfxPropertySetHandle inArgs, OfxPropertySetHandle outArgs)
{
if (strcmp(action, kOfxActionDescribe) == 0 ||
strcmp(action, kOfxImageEffectActionDescribeInContext) == 0) {
return actionDescribe(handle, 1);
}
if (strcmp(action, kOfxActionCreateInstance) == 0) {
return kOfxStatOK;
}
if (strcmp(action, kOfxActionDestroyInstance) == 0 ||
strcmp(action, kOfxActionLoad) == 0 ||
strcmp(action, kOfxActionUnload) == 0 ||
strcmp(action, kOfxImageEffectActionBeginSequenceRender) == 0 ||
strcmp(action, kOfxImageEffectActionEndSequenceRender) == 0 ||
strcmp(action, kOfxImageEffectActionIsIdentity) == 0) {
return kOfxStatOK;
}
if (strcmp(action, kOfxActionOpenGLContextAttached) == 0) {
return actionGLAttached((OfxImageEffectHandle)handle);
}
if (strcmp(action, kOfxActionOpenGLContextDetached) == 0) {
return actionGLDetached((OfxImageEffectHandle)handle);
}
if (strcmp(action, kOfxImageEffectActionGetClipPreferences) == 0) {
return actionGetClipPreferences(outArgs);
}
if (strcmp(action, kOfxImageEffectActionGetRegionOfDefinition) == 0) {
return actionGetRoD(outArgs);
}
if (strcmp(action, kOfxImageEffectActionGetRegionsOfInterest) == 0) {
return actionGetRoI((OfxImageEffectHandle)handle, inArgs, outArgs);
}
if (strcmp(action, kOfxImageEffectActionGetOutputColourspace) == 0) {
return actionGetOutputColourspace(inArgs, outArgs);
}
if (strcmp(action, kOfxImageEffectActionRender) == 0) {
return actionRenderGL((OfxImageEffectHandle)handle, inArgs);
}
return kOfxStatReplyDefault;
}
/* 恒透传变体入口:isIdentity 返回 Source;其余同 CPU 插件。 */
static OfxStatus mainEntryID(const char *action, const void *handle,
OfxPropertySetHandle inArgs, OfxPropertySetHandle outArgs)
{
if (strcmp(action, kOfxActionDescribe) == 0 ||
strcmp(action, kOfxImageEffectActionDescribeInContext) == 0) {
return actionDescribe(handle, 0);
}
if (strcmp(action, kOfxImageEffectActionIsIdentity) == 0) {
return actionIsIdentity(inArgs, outArgs);
}
if (strcmp(action, kOfxImageEffectActionGetOutputColourspace) == 0) {
return actionGetOutputColourspace(inArgs, outArgs);
}
return mainEntry(action, handle, inArgs, outArgs);
}
/* ---------- 导出 ---------- */
static const OfxPlugin test_plugin = {
/* pluginApi */ kOfxImageEffectPluginApi,
/* apiVersion */ kOfxImageEffectPluginApiVersion,
/* pluginIdentifier */ "org.oak.test-plugin",
/* pluginVersionMajor */ 1,
/* pluginVersionMinor */ 0,
/* setHost */ setHost,
/* mainEntry */ mainEntry,
};
static const OfxPlugin test_plugin_gl = {
/* pluginApi */ kOfxImageEffectPluginApi,
/* apiVersion */ kOfxImageEffectPluginApiVersion,
/* pluginIdentifier */ "org.oak.test-plugin.gl",
/* pluginVersionMajor */ 1,
/* pluginVersionMinor */ 0,
/* setHost */ setHost,
/* mainEntry */ mainEntryGL,
};
static const OfxPlugin test_plugin_id = {
/* pluginApi */ kOfxImageEffectPluginApi,
/* apiVersion */ kOfxImageEffectPluginApiVersion,
/* pluginIdentifier */ "org.oak.test-plugin.identity",
/* pluginVersionMajor */ 1,
/* pluginVersionMinor */ 0,
/* setHost */ setHost,
/* mainEntry */ mainEntryID,
};
OfxExport int OfxGetNumberOfPlugins(void)
{
return 3;
}
OfxExport OfxPlugin *OfxGetPlugin(int nth)
{
if (nth == 0)
return (OfxPlugin *)&test_plugin;
if (nth == 1)
return (OfxPlugin *)&test_plugin_gl;
if (nth == 2)
return (OfxPlugin *)&test_plugin_id;
return NULL;
}
+69
View File
@@ -0,0 +1,69 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2026 Oak Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* OFX Message suite 的 C 入口。stable Rust 无法定义 C-variadic 函数
* c_variadic 仍不稳定):v1 的 '...' 与 v2 的 va_list 都在这里
* vsnprintf 成定长缓冲,再转发给 Rust 实现 oak_ofx_message_impl。
*
* 编译:build.rscc crate);符号随 staticlib 进入 liboakplugin
* (corrosion 链接期无需额外接线)。
*/
#include <stdarg.h>
#include <stdio.h>
extern int oak_ofx_message_impl(void *handle, const char *type,
const char *id, const char *message);
/* 消息缓冲上限(镜像 C++ 侧 format_message 的 1024 惯例,
* olivehost.cpp:99;超长截断不报错)。 */
#define OAK_MSG_BUF_SIZE 1024
static int forward(void *handle, const char *type, const char *id,
const char *format, va_list args)
{
if (!format) {
/* C++ 侧 !format → kOfxStatFailedolivehost.cpp:267);
* 以 NULL message 通知 Rust 侧。 */
return oak_ofx_message_impl(handle, type, id, NULL);
}
char buf[OAK_MSG_BUF_SIZE];
int n = vsnprintf(buf, sizeof(buf), format, args);
if (n < 0) {
return oak_ofx_message_impl(handle, type, id, NULL);
}
return oak_ofx_message_impl(handle, type, id, buf);
}
int ofx_message_shim_v1(void *handle, const char *type, const char *id,
const char *format, ...)
{
va_list args;
va_start(args, format);
int r = forward(handle, type, id, format, args);
va_end(args);
return r;
}
int ofx_message_shim_v2(void *handle, const char *type, const char *id,
const char *format, va_list args)
{
return forward(handle, type, id, format, args);
}
+268
View File
@@ -0,0 +1,268 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2026 Oak Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* OfxParameterSuite 的 variadic 入口。stable Rust 不能定义
* C-variadic 函数(c_variadic 仍不稳定):paramGetValue /
* paramSetValue / *AtTime / derivative / integral 的 '...' 在这里
* 按参数类型解析(类型经 Rust 导出的 ofx_param_kind_of 查询),
* 再转发给 Rust 的类型化实现。
*
* 变长参数形状(HS: ofxhParam.cpp 各类型的 getV/setV):
* getInteger/Boolean/Choice → int*Double→double*
* 2D/3D/颜色 → 按维度的 int* / double* 指针序列;String →
* char**(写内驻指针)
* set:同形状但按值传(int/double 值序列;String → char*
*
* KIND_* 与 Rust 侧 param::ParamKind 枚举逐字对应。
*/
#include <stdarg.h>
/* Rust 导出 */
extern int ofx_param_kind_of(void *param);
extern int ofx_param_get_impl(void *param, int kind, void *out);
extern int ofx_param_set_impl(void *param, int kind, const void *in);
extern int ofx_param_get_string_impl(void *param, char **out);
extern int ofx_param_set_string_impl(void *param, const char *in);
extern int ofx_param_missing_feature_impl(void *param);
enum {
KIND_INT = 1,
KIND_INT2 = 2,
KIND_INT3 = 3,
KIND_DOUBLE = 4,
KIND_DOUBLE2 = 5,
KIND_DOUBLE3 = 6,
KIND_BOOL = 7,
KIND_CHOICE = 8,
KIND_RGB = 9,
KIND_RGBA = 10,
KIND_STRING = 11,
KIND_STRCHOICE = 12
};
/* get:把插件传的指针序列收进局部缓冲,调 Rust,再散回。 */
static int get_dispatch(void *param, int kind, va_list ap)
{
switch (kind) {
case KIND_INT:
case KIND_BOOL:
case KIND_CHOICE: {
int *v = va_arg(ap, int *);
return ofx_param_get_impl(param, kind, v);
}
case KIND_INT2: {
int *a = va_arg(ap, int *);
int *b = va_arg(ap, int *);
int buf[2] = { *a, *b };
int r = ofx_param_get_impl(param, kind, buf);
*a = buf[0];
*b = buf[1];
return r;
}
case KIND_INT3: {
int *a = va_arg(ap, int *);
int *b = va_arg(ap, int *);
int *c = va_arg(ap, int *);
int buf[3] = { *a, *b, *c };
int r = ofx_param_get_impl(param, kind, buf);
*a = buf[0];
*b = buf[1];
*c = buf[2];
return r;
}
case KIND_DOUBLE: {
double *v = va_arg(ap, double *);
return ofx_param_get_impl(param, kind, v);
}
case KIND_DOUBLE2: {
double *a = va_arg(ap, double *);
double *b = va_arg(ap, double *);
double buf[2] = { *a, *b };
int r = ofx_param_get_impl(param, kind, buf);
*a = buf[0];
*b = buf[1];
return r;
}
case KIND_DOUBLE3: {
double *a = va_arg(ap, double *);
double *b = va_arg(ap, double *);
double *c = va_arg(ap, double *);
double buf[3] = { *a, *b, *c };
int r = ofx_param_get_impl(param, kind, buf);
*a = buf[0];
*b = buf[1];
*c = buf[2];
return r;
}
case KIND_RGB: {
double *a = va_arg(ap, double *);
double *b = va_arg(ap, double *);
double *c = va_arg(ap, double *);
double buf[3] = { *a, *b, *c };
int r = ofx_param_get_impl(param, kind, buf);
*a = buf[0];
*b = buf[1];
*c = buf[2];
return r;
}
case KIND_RGBA: {
double *a = va_arg(ap, double *);
double *b = va_arg(ap, double *);
double *c = va_arg(ap, double *);
double *d = va_arg(ap, double *);
double buf[4] = { *a, *b, *c, *d };
int r = ofx_param_get_impl(param, kind, buf);
*a = buf[0];
*b = buf[1];
*c = buf[2];
*d = buf[3];
return r;
}
case KIND_STRING:
case KIND_STRCHOICE: {
char **p = va_arg(ap, char **);
return ofx_param_get_string_impl(param, p);
}
default:
return 1; /* kOfxStatFailed:未知类型 */
}
}
/* set:按值收进局部缓冲,调 Rust。 */
static int set_dispatch(void *param, int kind, va_list ap)
{
switch (kind) {
case KIND_INT:
case KIND_BOOL:
case KIND_CHOICE: {
int v = va_arg(ap, int);
return ofx_param_set_impl(param, kind, &v);
}
case KIND_INT2: {
int a = va_arg(ap, int);
int b = va_arg(ap, int);
int buf[2] = { a, b };
return ofx_param_set_impl(param, kind, buf);
}
case KIND_INT3: {
int a = va_arg(ap, int);
int b = va_arg(ap, int);
int c = va_arg(ap, int);
int buf[3] = { a, b, c };
return ofx_param_set_impl(param, kind, buf);
}
case KIND_DOUBLE: {
double v = va_arg(ap, double);
return ofx_param_set_impl(param, kind, &v);
}
case KIND_DOUBLE2: {
double a = va_arg(ap, double);
double b = va_arg(ap, double);
double buf[2] = { a, b };
return ofx_param_set_impl(param, kind, buf);
}
case KIND_DOUBLE3: {
double a = va_arg(ap, double);
double b = va_arg(ap, double);
double c = va_arg(ap, double);
double buf[3] = { a, b, c };
return ofx_param_set_impl(param, kind, buf);
}
case KIND_RGB: {
double a = va_arg(ap, double);
double b = va_arg(ap, double);
double c = va_arg(ap, double);
double buf[3] = { a, b, c };
return ofx_param_set_impl(param, kind, buf);
}
case KIND_RGBA: {
double a = va_arg(ap, double);
double b = va_arg(ap, double);
double c = va_arg(ap, double);
double d = va_arg(ap, double);
double buf[4] = { a, b, c, d };
return ofx_param_set_impl(param, kind, buf);
}
case KIND_STRING:
case KIND_STRCHOICE: {
const char *s = va_arg(ap, const char *);
return ofx_param_set_string_impl(param, s);
}
default:
return 1; /* kOfxStatFailed */
}
}
int ofx_param_get_value_shim(void *param, ...)
{
va_list ap;
va_start(ap, param);
int kind = ofx_param_kind_of(param);
int r = get_dispatch(param, kind, ap);
va_end(ap);
return r;
}
int ofx_param_get_value_at_time_shim(void *param, double time, ...)
{
(void)time; /* 第 1 期无动画:AtTime 即当前值(Rust 侧实现) */
va_list ap;
va_start(ap, time);
int kind = ofx_param_kind_of(param);
int r = get_dispatch(param, kind, ap);
va_end(ap);
return r;
}
int ofx_param_set_value_shim(void *param, ...)
{
va_list ap;
va_start(ap, param);
int kind = ofx_param_kind_of(param);
int r = set_dispatch(param, kind, ap);
va_end(ap);
return r;
}
int ofx_param_set_value_at_time_shim(void *param, double time, ...)
{
(void)time; /* 第 1 期无动画 */
va_list ap;
va_start(ap, time);
int kind = ofx_param_kind_of(param);
int r = set_dispatch(param, kind, ap);
va_end(ap);
return r;
}
/* derivative/integral:第 1 期无动画支持;不消费 va_args,
* 统一返回 kOfxStatErrMissingHostFeature。 */
int ofx_param_get_derivative_shim(void *param, double time, ...)
{
(void)time;
return ofx_param_missing_feature_impl(param);
}
int ofx_param_get_integral_shim(void *param, double time1, double time2, ...)
{
(void)time1;
(void)time2;
return ofx_param_missing_feature_impl(param);
}
+77
View File
@@ -0,0 +1,77 @@
// 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/>.
//! bridgeoak 其余模块的 C ABI 导入。
//!
//! 每个子模块(node/render/undo)只声明对应公共头的子集并包一层
//! 极薄的 safe 封装。链接在模块 dylib 装配时完成(liboaknode/
//! liboakrender/liboakundo)。
//!
//! ## 双态实现(所有子模块统一)
//!
//! - 默认:`dlsym(RTLD_DEFAULT)` 运行时解析(本模块被 force_load
//! 进宿主进程,符号在全局作用域;cargo test 无这些库时符号缺失
//! → 可解释错误,测试可走通至桥边界);
//! - `--features test-stubs`:库内 no_mangle 桩 + 状态访问器
//! (各子模块的 [`node::stub`]/[`render::stub`]/[`undo::stub`])——
//! 桥路径在 cargo test 里全链路可跑。两种形态的调用面完全一致。
//!
//! [`dlsym`] 是三个子模块共享的解析实现(macOS/Linux 的 RTLD_DEFAULT
//! 取值不同;函数指针按调用方签名转换)。
pub mod node;
pub mod render;
pub mod undo;
/// 共享的 dlsym 运行时解析(`#[cfg(not(feature = "test-stubs"))]`)。
#[cfg(not(feature = "test-stubs"))]
pub(crate) mod dlsym {
use std::ffi::{c_char, c_void};
/// RTLD_DEFAULTmacOS: -2Linux: 0)。
#[cfg(target_os = "macos")]
pub(crate) const RTLD_DEFAULT: *mut c_void = -2isize as *mut c_void;
#[cfg(target_os = "linux")]
pub(crate) const RTLD_DEFAULT: *mut c_void = 0isize as *mut c_void;
extern "C" {
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
}
/// 解析全局作用域符号;缺失返回 None。
pub(crate) fn resolve(name: &str) -> Option<*mut c_void> {
let c = std::ffi::CString::new(name).ok()?;
let p = unsafe { dlsym(RTLD_DEFAULT, c.as_ptr()) };
if p.is_null() {
None
} else {
Some(p)
}
}
/// 解析并按签名调用;符号缺失返回 None。
///
/// # Safety
/// 调用方保证 `T` 与符号的真实函数类型一致。
pub(crate) fn call<T, R>(name: &str, f: impl FnOnce(T) -> R) -> Option<R>
where
T: Copy,
{
let p = resolve(name)?;
let f_ptr: T = unsafe { std::mem::transmute_copy(&p) };
Some(f(f_ptr))
}
}
+420
View File
@@ -0,0 +1,420 @@
// 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_combonode.h:48-52)没有
//! POD 表示——走 `*_input_string_*` 专用函数(本桥的
//! [`set_input_string_undoable`]);`OAKNODE_VALUE_STRING` 的 POD 里
//! 不携带字符串数据。`crate::ffi::OakNodeValue` 与此同布局(出口层
//! 的镜像,两处独立声明避免模块环)。
//!
//! ## 双态实现(同 [`crate::bridge::render`]
//!
//! - 默认:`dlsym(RTLD_DEFAULT)` 运行时解析(cargo test 无 liboaknode
//! 时符号缺失 → 错误码/空句柄,桥路径可解释地失败);
//! - `--features test-stubs`:库内桩([`stub`])——节点值、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_valueinclude/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],
}
}
/// 类型化构造:choiceCOMBO)。
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"))]
{
type F = unsafe fn(usize) -> NodeHandle;
crate::bridge::dlsym::call::<F, NodeHandle>("oaknode_node_from_identity", |f| unsafe { f(id) })
.unwrap_or_else(CHandle::null)
}
}
/// 写输入值的标准值并产出一条 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"))]
{
type F = unsafe fn(NodeHandle, *const c_char, *const Value, *mut CommandHandle) -> i32;
crate::bridge::dlsym::call::<F, i32>("oaknode_node_set_input_undoable", |f| {
unsafe { f(node, input, value, out) }
})
.unwrap_or(-30001)
}
}
/// 写字符串族输入的标准值并产出一条 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"))]
{
type F = unsafe fn(NodeHandle, *const c_char, *const c_char, *mut CommandHandle) -> i32;
crate::bridge::dlsym::call::<F, i32>("oaknode_node_set_input_string_undoable", |f| {
unsafe { f(node, input, value, out) }
})
.unwrap_or(-30001)
}
}
// ---- 测试桩(--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
}
}
// ---- 默认模式单测(无桩;dlsym 缺失的可解释失败路径)----------------------
#[cfg(all(test, not(feature = "test-stubs")))]
mod tests {
use super::*;
/// cargo test 无 liboaknode:符号缺失 → 空句柄/负错误码;空句柄
/// 与空指针参数被拒。
#[test]
fn dlsym_missing_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
);
}
}
}
+976
View File
@@ -0,0 +1,976 @@
// 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 侧
//! 创建并经句柄传入,宿主只写其帧。
//!
//! ## 双态实现
//!
//! - 默认:`dlsym(RTLD_DEFAULT)` 运行时解析(本模块被 force_load
//! 进宿主进程,符号在全局作用域;cargo test 无 liboakrender 时
//! 符号缺失 → 可解释错误,测试可走通至渲染边界);
//! - `--features test-stubs`:库内 no_mangle 桩 + 状态访问器
//! [`stub`])——像素路径在 cargo test 里全链路可跑。
//! 两种形态的调用面(`texture_get_frame` 等)完全一致。
use std::ffi::c_void;
/// oakrender 视频参数(include/render/renderer.h:78,字段逐字一致)。
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
pub struct VideoParams {
/// 宽。
pub width: i32,
/// 高。
pub height: i32,
/// 帧时长分子(如 1001/30000 秒)。
pub time_base_num: i32,
/// 帧时长分母。
pub time_base_den: i32,
/// `olive::PixelFormat::Format`invalid=-1, u8=0, u10=1, u16=2,
/// f16=3, f32=4)。
pub format: i32,
/// 像素比分子。
pub pixel_aspect_num: i32,
/// 像素比分母。
pub pixel_aspect_den: i32,
/// `olive::VideoParams::Interlacing`。
pub interlacing: i32,
/// `olive::VideoParams::ColorRange`。
pub color_range: i32,
/// 预览分辨率除数(1 = 全分辨率)。
pub divider: i32,
/// `olive::VideoParams::Type`0 = video)。
pub video_type: i32,
/// 预乘 alpha0/1)。
pub premultiplied_alpha: i32,
}
/// 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"))]
{
type F = unsafe fn(TextureHandle, *mut FrameHandle) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_display_texture_get_frame", |f| {
unsafe { f(texture, out) }
})
.unwrap_or(-1)
}
}
/// 纹理是否占位(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"))]
{
type F = unsafe fn(TextureHandle) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_display_texture_is_dummy", |f| unsafe { f(texture) })
.unwrap_or(1)
}
}
/// 帧宽(空帧为 0)。
pub(crate) unsafe fn frame_width(frame: FrameHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::frame_width(frame) }
}
#[cfg(not(feature = "test-stubs"))]
{
type F = unsafe fn(FrameHandle) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_codec_frame_width", |f| unsafe { f(frame) })
.unwrap_or(0)
}
}
/// 帧高。
pub(crate) unsafe fn frame_height(frame: FrameHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::frame_height(frame) }
}
#[cfg(not(feature = "test-stubs"))]
{
type F = unsafe fn(FrameHandle) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_codec_frame_height", |f| unsafe { f(frame) })
.unwrap_or(0)
}
}
/// 借用的像素数据指针(最终 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"))]
{
type F = unsafe fn(FrameHandle) -> *mut c_void;
crate::bridge::dlsym::call::<F, *mut c_void>("oakrender_codec_frame_data", |f| unsafe { f(frame) })
.unwrap_or(std::ptr::null_mut())
}
}
/// 帧的视频参数。
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"))]
{
type F = unsafe fn(FrameHandle, *mut VideoParams) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_codec_frame_get_params", |f| unsafe { f(frame, out) })
.unwrap_or(-1)
}
}
/// 按参数分配像素缓冲。
pub(crate) unsafe fn frame_allocate(frame: FrameHandle) -> i32 {
#[cfg(feature = "test-stubs")]
{
unsafe { stub::frame_allocate(frame) }
}
#[cfg(not(feature = "test-stubs"))]
{
type F = unsafe fn(FrameHandle) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_codec_frame_allocate", |f| unsafe { f(frame) })
.unwrap_or(-1)
}
}
/// 释放一次帧引用并清空句柄(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"))]
{
type F = unsafe fn(*mut FrameHandle);
if let Some(f) = crate::bridge::dlsym::call::<F, ()>("oakrender_codec_frame_free", |f| {
unsafe { f(frame) }
}) {
// 调用已完成;返回值忽略。
let _ = f;
}
}
}
/// 帧的行跨度(字节;空帧 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"))]
{
type F = unsafe fn(FrameHandle) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_codec_frame_linesize_bytes", |f| {
unsafe { f(frame) }
})
.unwrap_or(0)
}
}
// ---- 渲染器族(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"))]
{
type F = unsafe fn(*const std::ffi::c_char) -> RendererHandle;
crate::bridge::dlsym::call::<F, RendererHandle>("oakrender_display_renderer_create_dynamic", |f| {
unsafe { f(backend) }
})
.unwrap_or_else(RendererHandle::null)
}
}
/// 初始化渲染器(`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"))]
{
type F = unsafe fn(RendererHandle, *mut std::ffi::c_void) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_display_renderer_init", |f| {
unsafe { f(renderer, gl_context) }
})
.unwrap_or(-1)
}
}
/// 渲染器是否 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"))]
{
type F = unsafe fn(RendererHandle) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_display_renderer_is_open_gl", |f| {
unsafe { f(renderer) }
})
.unwrap_or(0)
}
}
/// 释放一次渲染器引用并清空句柄(`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"))]
{
type F = unsafe fn(*mut RendererHandle);
if let Some(f) = crate::bridge::dlsym::call::<F, ()>("oakrender_display_renderer_destroy", |f| {
unsafe { f(renderer) }
}) {
let _ = f;
}
}
}
/// 在渲染器上创建纹理(`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"))]
{
type F = unsafe fn(RendererHandle, *const VideoParams, *const std::ffi::c_void, i32) -> TextureHandle;
crate::bridge::dlsym::call::<F, TextureHandle>("oakrender_display_texture_create", |f| {
unsafe { f(renderer, params, pixels, linesize) }
})
.unwrap_or_else(TextureHandle::null)
}
}
/// 纹理的原生 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"))]
{
type F = unsafe fn(TextureHandle) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_display_texture_id", |f| unsafe { f(texture) })
.unwrap_or(0)
}
}
/// 纹理参数(`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"))]
{
type F = unsafe fn(TextureHandle, *mut VideoParams) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_display_texture_get_params", |f| {
unsafe { f(texture, out) }
})
.unwrap_or(-1)
}
}
/// 从纹理下载像素(`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"))]
{
type F = unsafe fn(TextureHandle, *mut std::ffi::c_void, i32) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_display_texture_download", |f| {
unsafe { f(texture, pixels, linesize) }
})
.unwrap_or(-1)
}
}
/// 再取一次引用(`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"))]
{
type F = unsafe fn(TextureHandle) -> TextureHandle;
crate::bridge::dlsym::call::<F, TextureHandle>("oakrender_display_texture_retain", |f| {
unsafe { f(texture) }
})
.unwrap_or_else(TextureHandle::null)
}
}
/// 释放一次纹理引用并清空句柄(`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"))]
{
type F = unsafe fn(*mut TextureHandle);
if let Some(f) = crate::bridge::dlsym::call::<F, ()>("oakrender_display_texture_free", |f| {
unsafe { f(texture) }
}) {
let _ = f;
}
}
}
/// 上传像素到纹理(`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"))]
{
type F = unsafe fn(TextureHandle, *const std::ffi::c_void, i32) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_display_texture_upload", |f| {
unsafe { f(texture, pixels, linesize) }
})
.unwrap_or(-1)
}
}
/// 按 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"))]
{
type F = unsafe fn(RendererHandle, i32, *const VideoParams, *mut std::ffi::c_void, i32) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakrender_display_renderer_download_from_texture", |f| {
unsafe { f(renderer, texture_id, params, dst, linesize) }
})
.unwrap_or(-1)
}
}
// ---- 测试桩(--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 {
/// 原生 idtexture_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)的纹理 ctxtexture_is_dummy 用)。
dummy: std::collections::HashSet<usize>,
/// GL 渲染器是否可用(renderer_is_open_gl 的返回值)。
gl_available: bool,
/// GL 纹理注册表(ctx = GL_TEX_BASE + id)。
gl_textures: Vec<StubGlTexture>,
/// 下一个 GL 纹理 id(从 1 起)。
next_gl_id: i32,
}
static STATE: std::sync::LazyLock<Mutex<StubState>> = std::sync::LazyLock::new(|| {
Mutex::new(StubState {
dst: StubFrame::new(0, 0, 0),
src: StubFrame::new(0, 0, 0),
dummy: std::collections::HashSet::new(),
gl_available: false,
gl_textures: Vec::new(),
next_gl_id: 1,
})
});
fn lock() -> std::sync::MutexGuard<'static, StubState> {
STATE.lock().unwrap_or_else(|e| e.into_inner())
}
/// 重置全部桩状态(测试隔离)。
pub fn reset() {
let mut s = lock();
s.dst = StubFrame::new(0, 0, 0);
s.src = StubFrame::new(0, 0, 0);
s.dummy.clear();
s.gl_available = false;
s.gl_textures.clear();
s.next_gl_id = 1;
}
/// 把某纹理 ctx 标记为占位(dummy);`dummy=false` 取消标记。
/// clip 桥把 dummy 输入视作空输入(NotFound)。
pub fn set_dummy(ctx: usize, dummy: bool) {
let mut s = lock();
if dummy {
s.dummy.insert(ctx);
} else {
s.dummy.remove(&ctx);
}
}
/// 配置输出帧(宽/高/格式;F32 = 全链路主路径)。
pub fn setup_dst(width: i32, height: i32, format: i32) {
lock().dst = StubFrame::new(width, height, format);
}
/// 配置输入帧并填充像素。
pub fn setup_src(width: i32, height: i32, format: i32, pixels: Vec<u8>) {
let mut s = lock();
s.src = StubFrame::new(width, height, format);
s.src.data = pixels;
}
/// 输出帧像素(断言用)。
pub fn dst_pixels() -> Vec<u8> {
lock().dst.data.clone()
}
/// 输入帧像素(断言用)。
pub fn src_pixels() -> Vec<u8> {
lock().src.data.clone()
}
/// 输出帧参数。
pub fn dst_params() -> VideoParams {
lock().dst.params
}
/// GL 渲染器可用标记(renderer_is_open_gl 的桩返回值)。
pub fn set_gl_available(available: bool) {
lock().gl_available = available;
}
/// 构造桩 GL 渲染器句柄(ctx = 0xD1)。
pub fn make_gl_renderer() -> RendererHandle {
magic_handle(GL_RENDERER)
}
/// 已注册的 GL 纹理 id 列表(断言用)。
pub fn gl_texture_ids() -> Vec<i32> {
lock().gl_textures.iter().map(|t| t.id).collect()
}
/// 某 GL 纹理 id 的像素(断言用;未知 id 返回空)。
pub fn gl_texture_data(id: i32) -> Vec<u8> {
lock()
.gl_textures
.iter()
.find(|t| t.id == id)
.map(|t| t.frame.data.clone())
.unwrap_or_default()
}
/// 某 GL 纹理 id 的参数(断言用)。
pub fn gl_texture_params(id: i32) -> Option<VideoParams> {
lock()
.gl_textures
.iter()
.find(|t| t.id == id)
.map(|t| t.frame.params)
}
/// 直接构造一个 GL 纹理(GL 测试的目标纹理/输入纹理模拟;
/// oakrender 侧创建纹理的 C ABI 等价物)。返回句柄(ctx =
/// GL_TEX_BASE + id)。
pub fn make_gl_texture(width: i32, height: i32, format: i32) -> TextureHandle {
let mut s = lock();
let id = s.next_gl_id;
s.next_gl_id += 1;
s.gl_textures.push(StubGlTexture {
id,
frame: StubFrame::new(width, height, format),
});
magic_handle(GL_TEX_BASE + id as usize)
}
fn frame_of(state: &mut StubState, ctx: usize) -> &mut StubFrame {
if ctx == SRC_FRAME {
&mut state.src
} else if let Some(id) = gl_id_of_ctx(ctx) {
match state.gl_textures.iter_mut().find(|t| t.id == id) {
Some(t) => &mut t.frame,
None => &mut state.dst,
}
} else {
&mut state.dst
}
}
fn magic_handle(ctx: usize) -> CHandle {
CHandle {
ctx: ctx as *mut c_void,
addref: None,
release: None,
abi_version: 1,
}
}
const DST_TEX: usize = 0xA1;
const SRC_TEX: usize = 0xA2;
const DST_FRAME: usize = 0xB1;
const SRC_FRAME: usize = 0xB2;
/// 桩 GL 渲染器 ctx。
const GL_RENDERER: usize = 0xD1;
/// 桩 GL 纹理 ctx 基址(ctx = GL_TEX_BASE + id)。
const GL_TEX_BASE: usize = 0xC0;
/// GL 纹理 ctx → id。
fn gl_id_of_ctx(ctx: usize) -> Option<i32> {
if ctx > GL_TEX_BASE {
Some((ctx - GL_TEX_BASE) as i32)
} else {
None
}
}
/// 行拷贝:`src`(行跨度 src_linesize 字节)→ `dst`(行跨度
/// dst_linesize 字节),共 `rows` 行、每行 `row_bytes` 字节。
fn copy_rows(
src: &[u8],
src_linesize: usize,
dst: &mut [u8],
dst_linesize: usize,
row_bytes: usize,
rows: usize,
) {
for y in 0..rows {
let s = y * src_linesize;
let d = y * dst_linesize;
if row_bytes == src_linesize && src_linesize == dst_linesize {
dst[d..d + row_bytes].copy_from_slice(&src[s..s + row_bytes]);
} else {
dst[d..d + row_bytes].copy_from_slice(&src[s..s + row_bytes.min(src.len().saturating_sub(s))]);
}
}
}
pub(super) unsafe fn texture_get_frame(texture: TextureHandle, out: *mut FrameHandle) -> i32 {
if out.is_null() {
return -1;
}
// GL 纹理:帧句柄即纹理自身 ctx(frame_of 按 ctx 反查镜像帧)。
let frame_ctx = if texture.ctx as usize == SRC_TEX {
SRC_FRAME
} else if gl_id_of_ctx(texture.ctx as usize).is_some() {
texture.ctx as usize
} else {
DST_FRAME
};
unsafe { *out = magic_handle(frame_ctx) };
0
}
pub(super) unsafe fn texture_is_dummy(texture: TextureHandle) -> i32 {
lock().dummy.contains(&(texture.ctx as usize)) as i32
}
pub(super) unsafe fn frame_width(frame: FrameHandle) -> i32 {
let mut s = lock();
frame_of(&mut s, frame.ctx as usize).params.width
}
pub(super) unsafe fn frame_height(frame: FrameHandle) -> i32 {
let mut s = lock();
frame_of(&mut s, frame.ctx as usize).params.height
}
pub(super) unsafe fn frame_data(frame: FrameHandle) -> *mut c_void {
let mut s = lock();
let f = frame_of(&mut s, frame.ctx as usize);
f.data.as_mut_ptr() as *mut c_void
}
pub(super) unsafe fn frame_get_params(frame: FrameHandle, out: *mut VideoParams) -> i32 {
if out.is_null() {
return -1;
}
let mut s = lock();
unsafe { *out = frame_of(&mut s, frame.ctx as usize).params };
0
}
pub(super) unsafe fn frame_allocate(frame: FrameHandle) -> i32 {
let mut s = lock();
let f = frame_of(&mut s, frame.ctx as usize);
let len = f.params.width as usize
* f.params.height as usize
* 4
* bytes_per_pixel(f.params.format);
f.data.resize(len, 0);
0
}
pub(super) unsafe fn frame_free(frame: *mut FrameHandle) {
if !frame.is_null() {
unsafe { (*frame).ctx = std::ptr::null_mut() };
}
}
pub(super) unsafe fn frame_linesize_bytes(frame: FrameHandle) -> i32 {
let mut s = lock();
let f = frame_of(&mut s, frame.ctx as usize);
(f.params.width * 4 * bytes_per_pixel(f.params.format) as i32) as i32
}
// ---- 渲染器族桩 ---------------------------------------------------------
pub(super) unsafe fn renderer_create_dynamic(backend: *const std::ffi::c_char) -> RendererHandle {
if backend.is_null() {
return RendererHandle::null();
}
let id = unsafe { std::ffi::CStr::from_ptr(backend) }.to_str().unwrap_or("");
if id != "opengl" {
return RendererHandle::null();
}
magic_handle(GL_RENDERER)
}
pub(super) unsafe fn renderer_init(
_renderer: RendererHandle,
_gl_context: *mut std::ffi::c_void,
) -> i32 {
0
}
pub(super) unsafe fn renderer_is_open_gl(renderer: RendererHandle) -> i32 {
if renderer.ctx as usize != GL_RENDERER {
return 0;
}
lock().gl_available as i32
}
pub(super) unsafe fn renderer_destroy(renderer: *mut RendererHandle) {
if !renderer.is_null() {
unsafe { (*renderer).ctx = std::ptr::null_mut() };
}
}
// ---- 纹理族桩(GL 纹理注册表)------------------------------------------
pub(super) unsafe fn texture_create(
renderer: RendererHandle,
params: *const VideoParams,
pixels: *const std::ffi::c_void,
linesize: i32,
) -> TextureHandle {
if renderer.ctx as usize != GL_RENDERER || params.is_null() {
return TextureHandle::null();
}
let params = unsafe { *params };
if params.width <= 0 || params.height <= 0 {
return TextureHandle::null();
}
let mut s = lock();
let id = s.next_gl_id;
s.next_gl_id += 1;
let mut data = vec![0u8; tight_len(&params)];
if !pixels.is_null() {
// linesize 字节/行(renderer.h 的 bytes 契约;0 → 紧凑行)。
let bpp = bytes_per_pixel(params.format);
let row_bytes = (params.width as usize) * 4 * bpp;
let src_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
let src = unsafe { std::slice::from_raw_parts(pixels as *const u8, src_linesize * params.height as usize) };
copy_rows(src, src_linesize, &mut data, row_bytes, row_bytes.min(src_linesize), params.height as usize);
}
s.gl_textures.push(StubGlTexture {
id,
frame: StubFrame {
params,
data,
},
});
magic_handle(GL_TEX_BASE + id as usize)
}
pub(super) unsafe fn texture_id(texture: TextureHandle) -> i32 {
gl_id_of_ctx(texture.ctx as usize).unwrap_or(0)
}
pub(super) unsafe fn texture_get_params(texture: TextureHandle, out: *mut VideoParams) -> i32 {
if out.is_null() {
return -1;
}
let mut s = lock();
let Some(id) = gl_id_of_ctx(texture.ctx as usize) else {
return -1;
};
let Some(t) = s.gl_textures.iter().find(|t| t.id == id) else {
return -1;
};
unsafe { *out = t.frame.params };
0
}
pub(super) unsafe fn texture_download(
texture: TextureHandle,
pixels: *mut std::ffi::c_void,
linesize: i32,
) -> i32 {
let mut s = lock();
let Some(id) = gl_id_of_ctx(texture.ctx as usize) else {
return -1;
};
let Some(t) = s.gl_textures.iter().find(|t| t.id == id) else {
return -1;
};
if pixels.is_null() {
return -1;
}
let bpp = bytes_per_pixel(t.frame.params.format);
let row_bytes = (t.frame.params.width as usize) * 4 * bpp;
let dst_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
let dst = unsafe { std::slice::from_raw_parts_mut(pixels as *mut u8, dst_linesize * t.frame.params.height as usize) };
let src = t.frame.data.clone();
copy_rows(&src, row_bytes, dst, dst_linesize, row_bytes, t.frame.params.height as usize);
0
}
pub(super) unsafe fn texture_upload(
texture: TextureHandle,
pixels: *const std::ffi::c_void,
linesize: i32,
) -> i32 {
let mut s = lock();
let Some(id) = gl_id_of_ctx(texture.ctx as usize) else {
return -1;
};
let Some(t) = s.gl_textures.iter_mut().find(|t| t.id == id) else {
return -1;
};
if pixels.is_null() {
return -1;
}
let bpp = bytes_per_pixel(t.frame.params.format);
let row_bytes = (t.frame.params.width as usize) * 4 * bpp;
let src_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
let src = unsafe { std::slice::from_raw_parts(pixels as *const u8, src_linesize * t.frame.params.height as usize) };
let mut data = vec![0u8; tight_len(&t.frame.params)];
copy_rows(src, src_linesize, &mut data, row_bytes, row_bytes.min(src_linesize), t.frame.params.height as usize);
t.frame.data = data;
0
}
pub(super) unsafe fn texture_retain(texture: TextureHandle) -> TextureHandle {
texture
}
pub(super) unsafe fn texture_free(texture: *mut TextureHandle) {
if texture.is_null() {
return;
}
let ctx = unsafe { (*texture).ctx as usize };
if let Some(id) = gl_id_of_ctx(ctx) {
let mut s = lock();
s.gl_textures.retain(|t| t.id != id);
}
unsafe { (*texture).ctx = std::ptr::null_mut() };
}
pub(super) unsafe fn renderer_download_from_texture(
renderer: RendererHandle,
texture_id: i32,
params: *const VideoParams,
dst: *mut std::ffi::c_void,
linesize: i32,
) -> i32 {
if renderer.ctx as usize != GL_RENDERER || params.is_null() || dst.is_null() {
return -1;
}
let req = unsafe { *params };
let s = lock();
let Some(t) = s.gl_textures.iter().find(|t| t.id == texture_id) else {
return -1;
};
let bpp = bytes_per_pixel(req.format);
let row_bytes = (req.width as usize) * 4 * bpp;
let dst_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
let out = unsafe { std::slice::from_raw_parts_mut(dst as *mut u8, dst_linesize * req.height as usize) };
let src = t.frame.data.clone();
copy_rows(&src, row_bytes, out, dst_linesize, row_bytes, req.height as usize);
0
}
}
// ---- 默认模式单测(无桩;dlsym 缺失的可解释失败路径)----------------------
#[cfg(all(test, not(feature = "test-stubs")))]
mod tests {
use super::*;
/// cargo test 无 liboakrender:符号缺失 → 明确错误码/空指针/占位
/// 判定(1),全部生命周期函数对空句柄容错不崩。
#[test]
fn dlsym_missing_error_paths() {
let mut frame = FrameHandle::null();
let tex = TextureHandle::null();
unsafe {
assert_eq!(texture_get_frame(tex, &mut frame), -1);
assert_eq!(texture_is_dummy(tex), 1, "符号缺失 → 视为占位");
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()), -1);
assert_eq!(frame_allocate(frame), -1);
frame_free(&mut frame); // 空句柄 no-op
frame_free(std::ptr::null_mut()); // NULL no-op
}
}
}
+389
View File
@@ -0,0 +1,389 @@
// 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`]
//!
//! - 默认:`dlsym(RTLD_DEFAULT)` 运行时解析;
//! - `--features test-stubs`:库内桩([`stub`])——命令表 + 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"))]
{
type F = unsafe fn() -> CommandHandle;
crate::bridge::dlsym::call::<F, CommandHandle>("oakundo_command_init_multi", |f| unsafe { f() })
.unwrap_or_else(CHandle::null)
}
}
/// 直接 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"))]
{
type F = unsafe fn(CommandHandle) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakundo_command_redo_now", |f| unsafe { f(command) })
.unwrap_or(-40001)
}
}
/// 把子命令并入 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"))]
{
type F = unsafe fn(CommandHandle, CommandHandle) -> i32;
crate::bridge::dlsym::call::<F, i32>("oakundo_command_multi_add_child", |f| unsafe {
f(multi, child)
})
.unwrap_or(-40001)
}
}
/// 释放命令句柄(`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"))]
{
type F = unsafe fn(*mut CommandHandle);
if let Some(f) = crate::bridge::dlsym::call::<F, ()>("oakundo_command_free", |f| {
unsafe { f(command) }
}) {
let _ = f;
}
}
}
// ---- 测试桩(--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();
}
}
// ---- 默认模式单测(无桩;dlsym 缺失的可解释失败路径)----------------------
#[cfg(all(test, not(feature = "test-stubs")))]
mod tests {
use super::*;
/// cargo test 无 liboakundo:符号缺失 → 空句柄/负错误码,命令
/// 生命周期函数对空句柄/NULL 容错不崩。
#[test]
fn dlsym_missing_error_paths() {
unsafe {
let mut m = command_init_multi();
assert!(m.is_null(), "无 liboakundo 时 init_multi 应返回空句柄");
assert_eq!(command_redo_now(CommandHandle::null()), -40001);
assert_eq!(
command_multi_add_child(CommandHandle::null(), CommandHandle::null()),
-40001
);
command_free(&mut m); // 空句柄 no-op
command_free(std::ptr::null_mut()); // NULL no-op
}
}
}
+323
View File
@@ -0,0 +1,323 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! clip 实例:clip ↔ oakrender 纹理桥。
//!
//! 对应 C++ 的 `OliveClipInstance`。纹理数据经
//! [`crate::bridge::render`] 的 oakrender C ABI 流动;OFX 侧只看到
//! [`crate::image::Image`]CPU 路径)。
//!
//! `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`]
//! clip handle 即 `&props`)。
//!
//! `// TODO(bridge)``fetch_image`/`store_output_image` 依赖
//! bridge::render 的帧访问 C ABI(声明未冻结),保留 todo!()。
//! **已落地(M11 第 1 期)**:帧访问 C ABI 在 [`crate::bridge::render`]
//! 冻结(`oakrender_display_texture_*`/`oakrender_codec_frame_*`),
//! 两处桥实现完成。
use crate::instance::{OfxRangeD, OfxRectD, RenderScale};
use crate::property::PropertySet;
/// clip 实例。
#[repr(C)]
pub struct ClipInstance {
/// 实例级 clip 属性(当前分量/位深/像素比,协商结果写入;
/// 偏移 0,句柄约定)。
pub props: PropertySet,
/// clip 名。
pub name: String,
/// 当前输入纹理(oakrender 句柄的借用拷贝;输出 clip 为 None)。
input_texture: std::sync::Mutex<Option<crate::bridge::render::TextureHandle>>,
/// 当前输出纹理(C++ `output_textures_` 的 phase 1 单槽;
/// [`store_output_image`](Self::store_output_image) 的回写目标;
/// 输入 clip 为 None)。
output_texture: std::sync::Mutex<Option<crate::bridge::render::TextureHandle>>,
}
/// 从 clip 属性读协商分量(getClipPreferences 写入)。
fn components_from_props(props: &PropertySet) -> Option<crate::image::Components> {
use crate::property::Value;
match props.get(crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS, 0)? {
Value::String(s) => match s.to_string_lossy().as_ref() {
"OfxImageComponentRGBA" => Some(crate::image::Components::Rgba),
"OfxImageComponentRGB" => Some(crate::image::Components::Rgb),
"OfxImageComponentAlpha" => Some(crate::image::Components::Alpha),
_ => None,
},
_ => None,
}
}
impl ClipInstance {
/// 按描述符实例化(createInstance 路径调用;公开:宿主与测试
/// 都需要构造 clip 实例)。实例 props 是描述符 props 的深拷贝
/// HS: ClipBase 的实例构造,ofxhClip.cpp:57-70——插件在实例期
/// 读 supported components 等)。
///
/// ofxColourM11 §4):输入 clip 的 kOfxImageClipPropColourspace
/// 由宿主写为工作空间(ACEScgofxColour.h "Hosts should set this
/// property to the colourspace of the input clip. Typically it will
/// be set to the working colourspace");输出 clip 由
/// GetOutputColourspace action 后写。
pub fn from_descriptor(desc: &crate::descriptor::ClipDescriptor) -> Self {
let props = desc.props.clone();
let name = desc.name.clone();
if name != "Output" {
props.set_one(
crate::host::PROP_CLIP_COLOURSPACE,
crate::property::Value::String(
std::ffi::CString::new(crate::host::WORKING_COLOURSPACE).unwrap(),
),
);
}
Self {
props,
name,
input_texture: std::sync::Mutex::new(None),
output_texture: std::sync::Mutex::new(None),
}
}
/// 写协商后的像素格式(C++ `OliveClipInstance::setParams` 的 props
/// 侧:setPixelDepth/setComponentsoliveclip.cpp:700-709)。
/// `format` 为 olive::PixelFormat::Format0=u8, 2=u16, 3=f16,
/// 4=f32);`channels` 为分量数。
pub fn set_video_params(&self, format: i32, channels: i32) {
let depth = match format {
0 => "OfxBitDepthByte",
2 => "OfxBitDepthShort",
3 => "OfxBitDepthHalf",
_ => "OfxBitDepthFloat",
};
let comps = match channels {
1 => "OfxImageComponentAlpha",
3 => "OfxImageComponentRGB",
_ => "OfxImageComponentRGBA",
};
self.props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
crate::property::Value::String(std::ffi::CString::new(depth).unwrap()),
);
self.props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS,
crate::property::Value::String(std::ffi::CString::new(comps).unwrap()),
);
}
/// 写协商 RoDC++ `setRegionOfDefinition` 的单槽版,
/// oliveclip.cpp:674-678——C++ 按 time 存 map,本驱动一帧一槽;
/// 落点与 image effect suite 的 clipGetRegionOfDefinition 读取处
/// 一致)。
pub fn set_region_of_definition(&self, rod: OfxRectD, _time: f64) {
use crate::property::Value;
self.props.define(
"OfxImageEffectPropRegionOfDefinition",
vec![
Value::Double(rod.x1),
Value::Double(rod.y1),
Value::Double(rod.x2),
Value::Double(rod.y2),
],
);
}
/// 挂接输入纹理(oaknode 侧 clip 输入值变化时由 param/render 桥
/// 调用)。`time` 用于多帧纹理选择。空句柄断开。
pub fn set_input_texture(
&self,
texture: crate::bridge::render::TextureHandle,
_time: f64,
) {
let mut slot = self.input_texture.lock().unwrap_or_else(|e| e.into_inner());
if texture.is_null() {
*slot = None;
} else {
*slot = Some(texture);
}
}
/// 挂接输出纹理(render 驱动创建并经句柄传入;C++
/// `setOutputTexture` 的 phase 1 单槽版)。`time` 用于多帧纹理
/// 选择(`// [P2]`)。空句柄断开。
pub fn set_output_texture(
&self,
texture: crate::bridge::render::TextureHandle,
_time: f64,
) {
let mut slot = self
.output_texture
.lock()
.unwrap_or_else(|e| e.into_inner());
if texture.is_null() {
*slot = None;
} else {
*slot = Some(texture);
}
}
/// 抓取本 clip 在 `time` 的图像(OFX clipGetImage 的宿主侧)。
/// CPU 路径:把 oakrender 纹理 readback 成 [`crate::image::Image`]
/// (像素格式按协商结果,全链路 F32)。
/// `// [P2]` GL 路径:clipLoadTexture 语义在此扩展。
///
/// 第 1 期约束:帧必须是 f32 格式(全链路 F32);`region` 只支持
/// None(整帧)——转换(u8→f32 等)与子区域随 renderer 桥落地。
pub fn fetch_image(
&self,
time: f64,
scale: RenderScale,
region: Option<OfxRectD>,
) -> crate::error::Result<crate::image::Image> {
use crate::bridge::render::*;
use crate::error::Error;
let _ = (time, scale);
if region.is_some() {
return Err(Error::Failed("fetch_image 子区域第 1 期不支持".into()));
}
let texture = self
.input_texture
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
.ok_or(Error::NotFound)?;
// 占位纹理(dummy):视作无输入。
if unsafe { crate::bridge::render::texture_is_dummy(texture) } != 0 {
return Err(Error::NotFound);
}
let mut frame = FrameHandle::null();
let r = unsafe { crate::bridge::render::texture_get_frame(texture, &mut frame) };
if r != 0 || frame.is_null() {
return Err(Error::Failed("纹理无 CPU 帧".into()));
}
let mut params = VideoParams::default();
unsafe { crate::bridge::render::frame_get_params(frame, &mut params) };
if params.format != PIXEL_FORMAT_F32 {
unsafe { crate::bridge::render::frame_free(&mut frame) };
return Err(Error::Failed(format!(
"输入帧格式 {} 非 F32(第 1 期约束)",
params.format
)));
}
let (w, h) = (params.width as f64, params.height as f64);
// 分量按协商结果(getClipPreferences 已写入 clip.props)。
let components = components_from_props(&self.props).unwrap_or(crate::image::Components::Rgba);
let mut image = crate::image::Image::allocate(
crate::image::BitDepth::Float,
components,
OfxRectD { x1: 0.0, y1: 0.0, x2: w, y2: h },
);
let src = unsafe { crate::bridge::render::frame_data(frame) };
if src.is_null() {
unsafe { crate::bridge::render::frame_free(&mut frame) };
return Err(Error::Failed("帧无数据".into()));
}
// 行优先拷贝(帧行跨度经 linesize 读取——真实 oakrender 帧可
// 有行填充;目标 Image 恒紧凑。M11 §4 修复:phase 1 假设紧凑
// 行,对真实 oakrender 的填充帧会写错列)。
let channels = components.channel_count();
let tight = (w as usize) * channels * 4;
let row = unsafe { crate::bridge::render::frame_linesize_bytes(frame) } as usize;
let row = if row > 0 { row } else { tight };
let src_bytes = unsafe { std::slice::from_raw_parts(src as *const u8, row * h as usize) };
let dst = image.pixels_mut();
for y in 0..h as usize {
let s = y * row;
let d = y * tight;
dst[d..d + tight].copy_from_slice(&src_bytes[s..s + tight]);
}
unsafe { crate::bridge::render::frame_free(&mut frame) };
Ok(image)
}
/// 把输出图像回写为 oakrender 纹理(render 完成后由
/// [`crate::instance::Instance::render`] 的调用方使用)。
///
/// 输出纹理由 oakrender 侧创建并经 [`Self::set_output_texture`]
/// 挂入——本函数取该纹理的 CPU 帧(`texture_get_frame`),按帧
/// 参数校验 F32 与尺寸后整帧拷贝图像像素(全链路 F32;C++
/// pluginrenderer 的 `readback/wrap` 路径第 1 期以 CPU 拷贝表达,
/// GL 走 [`crate::bridge::render`] 的 `// [P2]`)。未挂输出纹理
/// 或纹理为占位(dummy)→ [`crate::error::Error::NotFound`]。
/// 成功返回纹理句柄(借用拷贝,调用方负责其生命周期)。
pub fn store_output_image(
&self,
image: &crate::image::Image,
) -> crate::error::Result<crate::bridge::render::TextureHandle> {
use crate::bridge::render::*;
use crate::error::Error;
let texture = self
.output_texture
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
.ok_or(Error::NotFound)?;
if unsafe { texture_is_dummy(texture) } != 0 {
return Err(Error::NotFound);
}
let mut frame = FrameHandle::null();
let r = unsafe { texture_get_frame(texture, &mut frame) };
if r != 0 || frame.is_null() {
return Err(Error::Failed("输出纹理无 CPU 帧".into()));
}
let mut params = VideoParams::default();
unsafe { frame_get_params(frame, &mut params) };
if params.format != PIXEL_FORMAT_F32 {
unsafe { frame_free(&mut frame) };
return Err(Error::Failed(format!(
"输出帧格式 {} 非 F32(第 1 期约束)",
params.format
)));
}
let (w, h) = (params.width as usize, params.height as usize);
// 图像与帧必须同尺寸(全链路 F32;宽高/行宽/总长逐项校验)。
let tight = w * image.components().channel_count() * 4;
if tight != image.row_bytes() || tight * h != image.pixels().len() {
unsafe { frame_free(&mut frame) };
return Err(Error::Failed("图像尺寸与输出帧不一致".into()));
}
let dst = unsafe { frame_data(frame) };
if dst.is_null() {
unsafe { frame_free(&mut frame) };
return Err(Error::Failed("输出帧无数据".into()));
}
// 行优先拷贝(目标帧行跨度经 linesize 读取——真实 oakrender
// 帧可有行填充;M11 §4 修复同 fetch_image)。
let row = unsafe { frame_linesize_bytes(frame) } as usize;
let row = if row > 0 { row } else { tight };
let dst_bytes = unsafe { std::slice::from_raw_parts_mut(dst as *mut u8, row * h) };
let pixels = image.pixels();
for y in 0..h {
let d = y * row;
let s = y * tight;
dst_bytes[d..d + tight].copy_from_slice(&pixels[s..s + tight]);
}
unsafe { frame_free(&mut frame) };
Ok(texture)
}
/// 本 clip 的时间域(clipGetFrameRange)。
///
/// `// TODO(bridge)`:输入范围经 oakrender 帧的时间基推导
/// time_base)——随 renderer 桥落地。
pub fn frame_range(&self) -> crate::error::Result<OfxRangeD> {
let _ = OfxRangeD::default();
Err(crate::error::Error::Failed("frame_range 待 renderer 桥".into()))
}
}
+130
View File
@@ -0,0 +1,130 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! describe 的产物:效果描述符与 clip 描述符。
//!
//! 参照:HS: ofxhImageEffect.cpp `Descriptor`describe action 期间
//! 插件可读写的属性容器)。
//!
//! 句柄约定([`crate::suites::tag`]):`props` 在偏移 0 且
//! `#[repr(C)]``params`/`clips` 元素装箱(Vec 重分配不移动对象)。
use std::ffi::CString;
use crate::param::ParamDef;
use crate::property::{PropertySet, Value};
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
// ---- clip 描述符属性(ofxImageEffect.h / ofxCore.h----
/// kOfxImageClipPropOptional。
pub(crate) const CLIP_OPTIONAL: &str = "OfxImageClipPropOptional";
/// kOfxImageClipPropIsMask。
pub(crate) const CLIP_IS_MASK: &str = "OfxImageClipPropIsMask";
/// kOfxImageClipPropFieldExtraction。
pub(crate) const CLIP_FIELD_EXTRACTION: &str = "OfxImageClipPropFieldExtraction";
/// kOfxImageEffectPropSupportedComponents。
pub(crate) const CLIP_SUPPORTED_COMPONENTS: &str = "OfxImageEffectPropSupportedComponents";
/// kOfxImageEffectPropTemporalClipAccess。
pub(crate) const CLIP_TEMPORAL_ACCESS: &str = "OfxImageEffectPropTemporalClipAccess";
/// kOfxImageEffectPropSupportsTiles。
pub(crate) const CLIP_SUPPORTS_TILES: &str = "OfxImageEffectPropSupportsTiles";
/// kOfxImageFieldDoubledFieldExtraction 默认值)。
pub(crate) const CLIP_FIELD_DOUBLED: &str = "OfxImageFieldDoubled";
/// kOfxTypeClip。
pub(crate) const CLIP_TYPE: &str = "OfxTypeClip";
/// clip 描述符(describe 期间由插件定义)。
/// `#[repr(C)]` + props 在偏移 0(句柄约定;describe 期 clip handle
/// 即 &props,与实例期 [`crate::clip::ClipInstance`] 共用 CLIP 标签)。
#[repr(C)]
pub struct ClipDescriptor {
/// 描述属性:label、可选性(kOfxImageClipPropOptional)、支持分量
/// kOfxImageEffectPropSupportedComponents)、field 支持等。
pub props: PropertySet,
/// clip 名(如 "Source"/"Output")。
pub name: String,
}
impl ClipDescriptor {
/// 按名构建(镜像 HS ofxhClip.cpp `ClipDescriptor::ClipDescriptor`
/// + clipDescriptorStuffs 属性表,ofxhClip.cpp:22-38)。公开:
/// 测试构造 clip 实例(GL suite 往返)。
pub fn new(name: &str) -> Self {
let props = PropertySet::new();
props.set_one(crate::param::PROP_TYPE, Value::String(cs(CLIP_TYPE)));
props.set_one(crate::param::PROP_NAME, Value::String(cs(name)));
props.set_one(crate::param::PROP_LABEL, Value::String(cs(name)));
props.set_one(crate::param::PROP_SHORT_LABEL, Value::String(cs("")));
props.set_one(crate::param::PROP_LONG_LABEL, Value::String(cs("")));
props.define(CLIP_SUPPORTED_COMPONENTS, vec![]);
props.set_one(CLIP_TEMPORAL_ACCESS, Value::Int(0));
props.set_one(CLIP_OPTIONAL, Value::Int(0));
props.set_one(CLIP_IS_MASK, Value::Int(0));
props.set_one(CLIP_FIELD_EXTRACTION, Value::String(cs(CLIP_FIELD_DOUBLED)));
props.set_one(CLIP_SUPPORTS_TILES, Value::Int(1));
// ofxColourM11 §4):clip 色彩空间属性族。Colourspace 由宿主
// 在实例化时写入(输入 clip = 工作空间 ACEScg);Preferred 由
// 插件在 GetClipPreferences 写(宿主侧预置空数组)。
props.set_one(
crate::host::PROP_CLIP_COLOURSPACE,
Value::String(cs("")),
);
props.define(crate::host::PROP_CLIP_PREFERRED_COLOURSPACES, vec![]);
Self {
props,
name: name.to_string(),
}
}
}
/// 效果描述符。
/// `#[repr(C)]` + props 在偏移 0(句柄约定:describe 期 effect/
/// param-set handle 即 &props)。
#[repr(C)]
pub struct EffectDescriptor {
/// 效果级属性:label、描述、分组、单帧/时间域标记等。
pub props: PropertySet,
/// 参数定义集(describe 期间插件经 Param suite 填充;
/// 值类型见 [`crate::param::ParamDef`]Box 保证句柄地址稳定)。
pub params: Vec<Box<ParamDef>>,
/// clip 定义集(含 "Output"Box 保证句柄地址稳定)。
pub clips: Vec<Box<ClipDescriptor>>,
}
impl EffectDescriptor {
/// 空描述符(describe action 前由 host 创建并预置根属性)。
pub fn new() -> Self {
Self {
props: PropertySet::new(),
params: Vec::new(),
clips: Vec::new(),
}
}
/// 按名找 clip。
pub fn clip(&self, name: &str) -> Option<&ClipDescriptor> {
self.clips.iter().find(|c| c.name == name).map(|b| b.as_ref())
}
/// 按名找参数定义。
pub fn param(&self, name: &str) -> Option<&ParamDef> {
self.params.iter().find(|p| p.name == name).map(|b| b.as_ref())
}
}
+62
View File
@@ -0,0 +1,62 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! 错误码。与 `include/*/error.h` 逐字对应;项目统一 -MMCCCC 方案
//! (模块号注册表见 include/common/error.h),跨模块透传不翻译。
/// 成功。
pub const OAKPLUGIN_OK: i32 = 0;
/// 空句柄或非法参数。
pub const OAKPLUGIN_E_INVALID: i32 = -90001;
/// 当前状态不允许该调用(如未扫描就创建实例)。
pub const OAKPLUGIN_E_STATE: i32 = -90002;
/// 底层操作失败(OFX action 返回非 kOfxStatOK、插件入口拒绝等)。
pub const OAKPLUGIN_E_FAILED: i32 = -90003;
/// 索引越界 / 指定标识的插件不存在。
pub const OAKPLUGIN_E_NOT_FOUND: i32 = -90004;
/// 分配失败。
pub const OAKPLUGIN_E_NOMEM: i32 = -90005;
/// crate 内部统一的结果类型;FFI 层把它映射为上述 i32 码。
pub type Result<T> = std::result::Result<T, Error>;
/// crate 内部错误。`code()` 给出对外错误码。
#[derive(Debug)]
pub enum Error {
/// 空句柄或非法参数。
Invalid,
/// 状态不允许。
State,
/// 底层失败,附人类可读上下文(仅日志,不出界)。
Failed(String),
/// 未找到。
NotFound,
/// 分配失败。
NoMem,
}
impl Error {
/// 映射为 `include/plugin/error.h` 的错误码。
pub fn code(&self) -> i32 {
match self {
Error::Invalid => OAKPLUGIN_E_INVALID,
Error::State => OAKPLUGIN_E_STATE,
Error::Failed(_) => OAKPLUGIN_E_FAILED,
Error::NotFound => OAKPLUGIN_E_NOT_FOUND,
Error::NoMem => OAKPLUGIN_E_NOMEM,
}
}
}
File diff suppressed because it is too large Load Diff
+258
View File
@@ -0,0 +1,258 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! 引用计数句柄脚手架。
//!
//! 对应 C 侧布局(`include/plugin/instance.h`,与 oak 全项目约定一致):
//!
//! ```c
//! typedef struct OakPluginInstance {
//! void *ctx;
//! void (*addref)(void *ctx);
//! void (*release)(void *ctx);
//! uint32_t abi_version;
//! } OakPluginInstance;
//! ```
//!
//! 句柄按值传;`ctx` 指向本 crate 堆上的 [`RefBox<T>`]。`addref`/
//! `release` 函数指针永远指向本 crate 的代码(所有权不出 DLL)。
use std::any::Any;
use std::collections::HashMap;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex, Weak};
use crate::error::OAKPLUGIN_E_FAILED;
/// 当前 ABI 版本,写进每个句柄的 `abi_version` 字段。
pub const OAKPLUGIN_ABI_VERSION: u32 = 1;
/// 句柄背后的堆盒子。`owns == false` 的盒子(借用包装)在计数归零时
/// 只释放盒子本身,不销毁内含对象。
pub struct RefBox<T: ?Sized> {
/// 引用计数(原子;release 可在任意线程发生)。
pub refs: AtomicU32,
/// 内含对象。
pub value: T,
}
/// C 句柄的 Rust 镜像。`#[repr(C)]`,与 C 头文件布局一致。
///
/// 生命周期:`*_init`/`*_create` 返回计数 1 的拥有型句柄;
/// `*_free(&h)` 释放一次并清空 `ctx``free(NULL)`/空句柄是 no-op。
/// 值型(Clone/CopyC 侧按值传句柄,复制后再 addref 是调用方
/// 契约)。
#[derive(Clone, Copy)]
#[repr(C)]
pub struct CHandle {
/// 不透明盒子指针(`RefBox<T>` 擦型后的 `*mut c_void`)。
pub ctx: *mut std::ffi::c_void,
/// 原子 +1。
pub addref: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
/// 原子 -1,归零销毁。
pub release: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
/// [`OAKPLUGIN_ABI_VERSION`]。
pub abi_version: u32,
}
// CHandle 是 C 侧值型句柄(按值传、可跨线程复制);`ctx` 是
// 不透明盒子指针,跨线程搬运是宿主分发语义(multithread suite
// 允许插件线程回调任意 suite)。
unsafe impl Send for CHandle {}
unsafe impl Sync for CHandle {}
impl CHandle {
/// 空句柄(`ctx == NULL`)。
pub fn null() -> Self {
Self {
ctx: std::ptr::null_mut(),
addref: None,
release: None,
abi_version: OAKPLUGIN_ABI_VERSION,
}
}
/// 是否为空调用面(`ctx` 为空)。
pub fn is_null(&self) -> bool {
self.ctx.is_null()
}
}
/// addref 的实现:原子 +1。拥有型与借用型共用——借用型只延长盒子
/// 的寿命,不延长被借用对象。
unsafe extern "C" fn refbox_addref<T: Any + Send>(ctx: *mut std::ffi::c_void) {
unsafe {
let rb = ctx as *const RefBox<T>;
// 调用方保证句柄在借用期内有效(ctx 非空且未被释放)。
(*rb).refs.fetch_add(1, Ordering::Relaxed);
}
}
/// release 的实现(拥有型):原子 -1,归零时回收盒子并销毁内含对象。
unsafe extern "C" fn refbox_release_owned<T: Any + Send>(ctx: *mut std::ffi::c_void) {
unsafe {
let rb = ctx as *mut RefBox<T>;
// AcqRel:归零这一侧要能看见最后一次引用前的全部写(含对象
// 析构所需的内部状态)。
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
drop(Box::from_raw(rb));
}
}
}
/// release 的实现(借用型,[`make_borrowed`] 的产物):归零时只回收
/// 盒子内存,把内含对象原样忘掉——其所有权仍在借用方手里。
unsafe extern "C" fn refbox_release_borrowed<T: Any + Send>(ctx: *mut std::ffi::c_void) {
unsafe {
let rb = ctx as *mut RefBox<T>;
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
// 部分 move:把 value 移出临时 Box,Box 析构只释放分配;
// value 用 forget 放弃析构(double-free 防线)。
std::mem::forget((Box::from_raw(rb)).value);
}
}
}
/// 为 `T` 制作拥有型句柄(计数 1)。分配失败返回空句柄并销毁对象。
///
/// 注:Rust 默认分配失败(OOM)直接 abort,不会走到"返回空句柄"
/// 路径;此处语义保留给未来接入自定义分配器的场景。
pub fn make_owned<T: Any + Send>(value: T) -> CHandle {
let rb = Box::into_raw(Box::new(RefBox {
refs: AtomicU32::new(1),
value,
}));
CHandle {
ctx: rb as *mut std::ffi::c_void,
addref: Some(refbox_addref::<T>),
release: Some(refbox_release_owned::<T>),
abi_version: OAKPLUGIN_ABI_VERSION,
}
}
/// 为已有对象制作借用句柄(计数归零只释放盒子)。`ptr` 必须在本
/// 句柄被释放前保持有效。
///
/// 语义:按位拷贝("借用拷贝",如纹理句柄的快照);被借用对象
/// 的析构完全由调用方负责,盒子从不碰它。拷贝即快照——借出后
/// 修改 `*ptr` 不会反映到句柄内。
///
/// # Safety
/// 调用方保证 `ptr` 的生命周期覆盖所有派生句柄,且其值在借用期内
/// 不被 move/析构。
pub unsafe fn make_borrowed<T: Any + Send>(ptr: *mut T) -> CHandle {
if ptr.is_null() {
return CHandle::null();
}
let rb = Box::into_raw(Box::new(RefBox {
refs: AtomicU32::new(1),
value: unsafe { std::ptr::read(ptr) },
}));
CHandle {
ctx: rb as *mut std::ffi::c_void,
addref: Some(refbox_addref::<T>),
release: Some(refbox_release_borrowed::<T>),
abi_version: OAKPLUGIN_ABI_VERSION,
}
}
/// 取回盒子内对象的不可变引用;空句柄返回 `None`。
///
/// # Safety
/// 调用方必须保证 `T` 与创建句柄时的类型一致。
pub unsafe fn get<T: Any>(h: &CHandle) -> Option<&T> {
if h.is_null() {
return None;
}
unsafe { Some(&(*(h.ctx as *const RefBox<T>)).value) }
}
/// FFI 兜底:捕获 panic,把 `Result<i32>` 映射为对外错误码
/// [`crate::error`])。所有返回 i32 的导出函数必须经它。
///
/// panic 路径返回 `OAKPLUGIN_E_FAILED`panic 详情暂不落日志
/// message 桥接入后补 TODO)。
pub fn guard<F>(f: F) -> i32
where
F: FnOnce() -> crate::error::Result<()>,
{
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(())) => crate::error::OAKPLUGIN_OK,
Ok(Err(e)) => e.code(),
Err(_) => OAKPLUGIN_E_FAILED,
}
}
/// 指针/句柄返回值版本的 [`guard`]panic 或 Err 时返回空句柄
/// (指针类返回 NULL)。
pub fn guard_handle<F>(f: F) -> CHandle
where
F: FnOnce() -> crate::error::Result<CHandle>,
{
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(h)) => h,
Ok(Err(_)) | Err(_) => CHandle::null(),
}
}
/// 无返回值版本:panic 被吞并记录日志(经 bridge 的日志回调)。
pub fn guard_void<F>(f: F)
where
F: FnOnce(),
{
let _ = catch_unwind(AssertUnwindSafe(f));
}
/// 句柄身份注册表:`usize` 身份 ↔ 弱引用。供 param↔node 等需要
/// "按身份找回对象"的桥使用(替代 M9 C++ 版的
/// `oaknode_node_identity()` 注册表)。
pub struct Registry<T: Any + Send> {
map: Mutex<HashMap<usize, Weak<RefBox<T>>>>,
}
impl<T: Any + Send> Registry<T> {
/// 空注册表。
pub fn new() -> Self {
Self {
map: Mutex::new(HashMap::new()),
}
}
/// 登记对象,返回其身份(地址语义,进程内唯一)。
pub fn register(&self, obj: &Arc<RefBox<T>>) -> usize {
// Arc 分配地址即身份:同一 RefBox 恒稳定,进程内唯一。
let id = Arc::as_ptr(obj) as *const () as usize;
lock(&self.map).insert(id, Arc::downgrade(obj));
id
}
/// 按身份取对象;对象已销毁或身份未知返回 `None`。
pub fn lookup(&self, id: usize) -> Option<Arc<RefBox<T>>> {
lock(&self.map).get(&id).and_then(|w| w.upgrade())
}
/// 摘除身份(对象销毁路径调用)。未知身份是 no-op。
pub fn unregister(&self, id: usize) {
lock(&self.map).remove(&id);
}
}
/// 取锁。毒锁(本 crate 代码持锁时 panic)时接管内部状态继续——
/// 一次 panic 不级联成后续所有 FFI 调用失败。
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(|e| e.into_inner())
}
File diff suppressed because it is too large Load Diff
+256
View File
@@ -0,0 +1,256 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! OFX image: an OFX view of a frame buffer (CPU path).
//!
//! Counterpart of the C++ `OliveImage`. Pixel memory is owned by this
//! crate (this was historically a hotspot of memory bugs: ownership
//! must be single, and lifetime is guaranteed by this type).
//! GL texture views are deferred to phase 2 (`// [P2]`).
//!
//! Property writes mirror the C++ `Image::allocate`
//! (image.cpp:132-172) and the HostSupport image property table
//! (HS: ofxhClip.cpp:458-472): Data/RowBytes/Bounds/
//! RegionOfDefinition/Components/PixelDepth/UniqueIdentifier.
//! This struct does not distinguish bounds from ROD (unified), and
//! does not model field/renderScale/pixelAspect/premultiplication
//! (`// [P2]`).
use std::ffi::{c_void, CString};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::instance::OfxRectD;
use crate::property::{PropertySet, Value};
// ---- OFX property names (macro strings of ofxCore.h / ofxImageEffect.h) ----
/// kOfxImagePropData (ofxCore.h:1275): pixel data pointer.
pub(crate) const K_IMAGE_PROP_DATA: &str = "OfxImagePropData";
/// kOfxImagePropRowBytes (ofxCore.h:1322): row byte count.
pub(crate) const K_IMAGE_PROP_ROW_BYTES: &str = "OfxImagePropRowBytes";
/// kOfxImagePropBounds (ofxCore.h:1291): pixel coordinates, Int x 4.
pub(crate) const K_IMAGE_PROP_BOUNDS: &str = "OfxImagePropBounds";
/// kOfxImagePropRegionOfDefinition (ofxCore.h:1307): pixel coordinates, Int x 4.
pub(crate) const K_IMAGE_PROP_ROD: &str = "OfxImagePropRegionOfDefinition";
/// kOfxImageEffectPropComponents (ofxImageEffect.h:915).
pub(crate) const K_IMAGE_EFFECT_PROP_COMPONENTS: &str = "OfxImageEffectPropComponents";
/// kOfxImageEffectPropPixelDepth (ofxImageEffect.h:901).
pub(crate) const K_IMAGE_EFFECT_PROP_PIXEL_DEPTH: &str = "OfxImageEffectPropPixelDepth";
/// kOfxImagePropUniqueIdentifier (ofxCore.h:927): host-assigned unique id.
pub(crate) const K_IMAGE_PROP_UNIQUE_ID: &str = "OfxImagePropUniqueIdentifier";
/// Process-wide monotonically increasing id (zero-dependency
/// replacement for HostSupport's UUID generation).
static NEXT_IMAGE_ID: AtomicU64 = AtomicU64::new(0);
/// Pixel bit depth (OFX kOfxBitDepth*; the full pipeline only uses
/// Float, the rest are kept for compatibility).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BitDepth {
/// 8-bit integer (compat).
Byte,
/// 16-bit integer (compat).
Short,
/// 16-bit half float (compat).
Half,
/// 32-bit float (main path).
Float,
}
impl BitDepth {
/// Bytes per component.
pub(crate) fn bytes_per_component(self) -> usize {
match self {
BitDepth::Byte => 1,
BitDepth::Short | BitDepth::Half => 2,
BitDepth::Float => 4,
}
}
/// OFX bit depth string (kOfxBitDepth*, ofxCore.h:866-880).
pub(crate) fn to_ofx(self) -> &'static str {
match self {
BitDepth::Byte => "OfxBitDepthByte",
BitDepth::Short => "OfxBitDepthShort",
BitDepth::Half => "OfxBitDepthHalf",
BitDepth::Float => "OfxBitDepthFloat",
}
}
}
/// Component layout (OFX kOfxImageComponent*).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Components {
/// RGBA.
Rgba,
/// RGB.
Rgb,
/// Single-channel Alpha.
Alpha,
}
impl Components {
/// Channel count.
pub(crate) fn channel_count(self) -> usize {
match self {
Components::Rgba => 4,
Components::Rgb => 3,
Components::Alpha => 1,
}
}
/// OFX component string (kOfxImageComponent*, ofxImageEffect.h:46-55).
pub(crate) fn to_ofx(self) -> &'static str {
match self {
Components::Rgba => "OfxImageComponentRGBA",
Components::Rgb => "OfxImageComponentRGB",
Components::Alpha => "OfxImageComponentAlpha",
}
}
}
/// A single frame. `data` is row-major; the row stride may be padded
/// for alignment; bounds are in pixel coordinates.
/// `#[repr(C)]` with `props` at offset 0 (handle convention,
/// see [`crate::suites::tag`]).
#[repr(C)]
pub struct Image {
/// Image-level properties (bounds, row bytes, depth, components,
/// unique identifier).
pub props: PropertySet,
/// Pixel buffer (length = row_bytes * height).
data: Vec<u8>,
/// Bit depth.
depth: BitDepth,
/// Components.
components: Components,
/// Pixel bounds.
bounds: OfxRectD,
/// Row byte count.
row_bytes: usize,
}
impl Image {
/// Allocate by format (uninitialized pixels).
///
/// Property writes mirror the C++ `Image::allocate`
/// (image.cpp:156-172): Data/RowBytes/Bounds/RegionOfDefinition/
/// Components/PixelDepth; UniqueIdentifier is also written per the
/// image property table (HS: ofxhClip.cpp:470).
/// ROD is unified with bounds (this struct does not distinguish
/// the two; both are pixel coordinates). A zero-size or inverted
/// rectangle yields an empty buffer (mirrors the
/// `buffer_size < 0 -> 0` guard in image.cpp:147-149).
pub fn allocate(depth: BitDepth, components: Components, bounds: OfxRectD) -> Self {
let (w, h) = {
let width = (bounds.x2 - bounds.x1).round();
let height = (bounds.y2 - bounds.y1).round();
if width > 0.0 && height > 0.0 {
(width as usize, height as usize)
} else {
(0, 0)
}
};
let row_bytes = w * components.channel_count() * depth.bytes_per_component();
let mut img = Self {
props: PropertySet::new(),
data: vec![0u8; row_bytes * h],
depth,
components,
bounds,
row_bytes,
};
// `data` is a heap buffer; moving/borrowing the vec does not move
// the buffer, and it is never resized after allocation, so the
// pointer stays valid for the whole Image lifetime (ownership
// discipline: this is the only place that holds the buffer).
img.props.define(
K_IMAGE_PROP_DATA,
vec![Value::Pointer(img.data.as_mut_ptr() as *mut c_void)],
);
img.props.define(
K_IMAGE_PROP_ROW_BYTES,
vec![Value::Int(row_bytes as i32)],
);
let b = |v: f64| Value::Int(v.round() as i32);
img.props.define(
K_IMAGE_PROP_BOUNDS,
vec![b(bounds.x1), b(bounds.y1), b(bounds.x2), b(bounds.y2)],
);
img.props.define(
K_IMAGE_PROP_ROD,
vec![b(bounds.x1), b(bounds.y1), b(bounds.x2), b(bounds.y2)],
);
img.props.define(
K_IMAGE_EFFECT_PROP_COMPONENTS,
vec![Value::String(CString::new(components.to_ofx()).unwrap())],
);
img.props.define(
K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
vec![Value::String(CString::new(depth.to_ofx()).unwrap())],
);
img.props.define(
K_IMAGE_PROP_UNIQUE_ID,
vec![Value::String(unique_identifier())],
);
img
}
/// Mutable pixel slice (for writing plugin output). The length is
/// consistent with the format by type construction.
pub fn pixels_mut(&mut self) -> &mut [u8] {
&mut self.data
}
/// Read-only pixel slice.
pub fn pixels(&self) -> &[u8] {
&self.data
}
/// Pixel bounds (canonical coordinates; the Image keeps bounds and
/// ROD unified).
pub fn bounds(&self) -> OfxRectD {
self.bounds
}
/// Bit depth.
pub fn depth(&self) -> BitDepth {
self.depth
}
/// Components.
pub fn components(&self) -> Components {
self.components
}
/// Row byte count.
pub fn row_bytes(&self) -> usize {
self.row_bytes
}
}
/// Unique identifier string (monotonically increasing per process,
/// hexadecimal; corresponds to the `uniqueIdentifier` parameter of
/// HS: ofxhClip.cpp:537).
pub(crate) fn unique_identifier() -> CString {
let n = NEXT_IMAGE_ID.fetch_add(1, Ordering::Relaxed);
// Hexadecimal ASCII, no NUL; unwrap cannot fail.
CString::new(format!("{:x}", n)).unwrap()
}
+968
View File
@@ -0,0 +1,968 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! 插件实例:action 调用面。
//!
//! 对应 C++ 的 `OlivePluginInstance`。语义参照:
//! HS: ofxhImageEffect.cpp `Instance`action 调用序列与参数集组装)。
use std::ffi::CString;
use std::sync::Arc;
use crate::clip::ClipInstance;
use crate::host::Plugin;
use crate::param::ParamSetInstance;
use crate::property::PropertySet;
/// paramEditBegin/End 的编辑事务状态(undo 分组)。对应 C++
/// oliveplugininstance.cpp 的 `edit_depth_`/`edit_command_`
/// 事务内的多次参数回写合并为一条 multi 命令,editEnd 时整体
/// redo 后释放。
#[repr(C)]
pub struct EditTransaction {
/// 嵌套深度(editBegin/End 必须配对)。
depth: i32,
/// 事务累积的 multi 命令(空句柄 = 尚无子命令)。
multi: crate::bridge::undo::CommandHandle,
/// 事务内回写次数(标签计数)。
param_count: i32,
/// 第一条回写的标签。
first_label: String,
}
impl EditTransaction {
/// 空事务(无嵌套、无累积命令)。
pub fn new() -> Self {
Self {
depth: 0,
multi: crate::bridge::undo::CommandHandle::null(),
param_count: 0,
first_label: String::new(),
}
}
}
/// 时间范围(OFX 规范双精度秒)。
#[derive(Clone, Copy, Debug, Default)]
pub struct OfxRangeD {
/// 起始(含)。
pub min: f64,
/// 结束(含)。
pub max: f64,
}
/// 矩形(规范坐标,double)。
#[derive(Clone, Copy, Debug, Default)]
pub struct OfxRectD {
/// 左。
pub x1: f64,
/// 上。
pub y1: f64,
/// 右。
pub x2: f64,
/// 下。
pub y2: f64,
}
/// 像素比/场序等渲染参量(render action 的 in_args 子集)。
#[derive(Clone, Copy, Debug)]
pub struct RenderScale {
/// x 方向渲染比例。
pub x: f64,
/// y 方向渲染比例。
pub y: f64,
}
/// 插件实例。`Arc<RefBox<Instance>>` 管理生命周期;身份注册见
/// [`crate::handle::Registry`]param 桥按身份反查)。
///
/// `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`]
/// 实例期 effect/param-set handle 即 `&props`)。
#[repr(C)]
pub struct Instance {
/// 实例级属性集(偏移 0,句柄约定)。
pub props: PropertySet,
/// 所属插件。
pub plugin: Arc<Plugin>,
/// 上下文(kOfxImageEffectContext*)。
pub context: String,
/// 参数实例集(createInstance 后由 describe 产物实例化)。
pub params: ParamSetInstance,
/// clip 实例集(含 "Output"Box 保证句柄地址稳定)。
pub clips: Vec<Box<ClipInstance>>,
/// 绑定的 oaknode 节点身份(param 桥写入;0 = 未绑定)。
pub node_identity: std::sync::atomic::AtomicUsize,
/// destroyInstance action 是否已通知(析构幂等门;Drop 驱动;
/// 公开:宿主 shutdown 与测试都要置位)。
pub destroyed: std::sync::atomic::AtomicBool,
/// beginSequenceRender 的时间域(timeline getTimeBounds 用;
/// endSequenceRender 清除)。
pub sequence_range: std::sync::Mutex<Option<OfxRangeD>>,
/// facade 注册的进度回调(oakplugin_instance_set_progress_cb)。
pub progress_cb: std::sync::Mutex<Option<(crate::progress::ProgressFn, usize)>>,
/// facade 取消标记(oakplugin_instance_cancel)。
pub cancel: std::sync::atomic::AtomicBool,
/// paramEditBegin/End 的编辑事务(undo 分组)。
pub edit: std::sync::Mutex<EditTransaction>,
/// 实例级渲染串行化(对应 C++ `OlivePluginInstance::mutex`
/// pluginrenderer.cpp:1436-1444 的 instance_lock——渲染路径非
/// 线程安全,并发 render 必须互斥)。
pub render_lock: std::sync::Mutex<()>,
}
/// 实例销毁路径:先通知 destroyInstance action,再摘除身份登记。
/// RefBox 归零时 Drop 触发——action 通知必须在对象析构前发出。)
impl Drop for Instance {
fn drop(&mut self) {
if !self.destroyed.swap(true, std::sync::atomic::Ordering::Relaxed) {
self.notify_destroy();
}
crate::suites::param::unregister_params_of(&self.props as *const _ as usize);
crate::host::instance_registry().unregister(
&self.props as *const crate::property::PropertySet as usize,
);
}
}
impl Instance {
/// 绑定 oaknode 节点(C++ `set_node_handle` 的 Rust 侧;装配期由
/// facade/测试调用)。`identity` 为 [`crate::host::instance_registry`]
/// 无关的 oaknode 节点身份(`oaknode_node_identity` 的地址语义);
/// 0 解除绑定。
pub fn bind_node(&self, identity: usize) {
self.node_identity
.store(identity, std::sync::atomic::Ordering::Relaxed);
}
/// paramEditBegin:进入编辑事务(可嵌套;首次进入重置事务状态)。
pub fn edit_begin(&self) {
let mut e = self.edit.lock().unwrap_or_else(|e| e.into_inner());
e.depth += 1;
if e.depth == 1 {
e.multi = crate::bridge::undo::CommandHandle::null();
e.param_count = 0;
e.first_label.clear();
}
}
/// paramEditEnd:退出编辑事务;最外层结束时把累积的 multi 命令
/// redo 生效并释放(本 crate 无 undo 栈——命令在 oakundo 侧
/// 提交,见 C++ `submit_undo_command` 的 fallback 分支)。
pub fn edit_end(&self) {
let mut e = self.edit.lock().unwrap_or_else(|e| e.into_inner());
if e.depth > 0 {
e.depth -= 1;
}
if e.depth == 0 && !e.multi.is_null() {
unsafe { crate::bridge::undo::command_redo_now(e.multi) };
unsafe { crate::bridge::undo::command_free(&mut e.multi) };
e.param_count = 0;
e.first_label.clear();
}
}
/// 是否处于编辑事务内(param 桥回写据此决定打包成 multi)。
pub fn in_edit(&self) -> bool {
self.edit
.lock()
.unwrap_or_else(|e| e.into_inner())
.depth
> 0
}
/// 事务感知的 undo 提交(param 桥回写用;对应 C++
/// oliveplugininstance.cpp:390-414 `submit_undo_command`):
/// 编辑事务内并入 multi(子命令立即 redo 生效,值即时可见),
/// 否则单命令 redo 后释放。
pub(crate) fn submit_undo_command(
&self,
mut cmd: crate::bridge::undo::CommandHandle,
label: &str,
) {
if cmd.is_null() {
return;
}
if self.in_edit() {
let mut e = self.edit.lock().unwrap_or_else(|e| e.into_inner());
if e.multi.is_null() {
e.multi = unsafe { crate::bridge::undo::command_init_multi() };
}
e.param_count += 1;
if e.first_label.is_empty() {
e.first_label = label.to_string();
}
unsafe { crate::bridge::undo::command_redo_now(cmd) };
unsafe { crate::bridge::undo::command_multi_add_child(e.multi, cmd) };
} else {
unsafe { crate::bridge::undo::command_redo_now(cmd) };
unsafe { crate::bridge::undo::command_free(&mut cmd) };
}
}
/// getClipPreferences action,返回协商结果(输出 clip 的分量/
/// 位深/像素比/field 与帧率)。协商顺序逐行对照
/// HS: ofxhImageEffect.cpp `Instance::getClipPreferences`——
/// 当年调试重灾区,实现时必须附行号注释。
///
/// 协商流程(HS: ofxhImageEffect.cpp:1686-1740):
/// 1. out args 预置 clipPrefsStuffsframeRate=1、premult、
/// fieldOrder、continuousSamples、frameVaryingHS:1697-1710);
/// 2. 预置 per-clip 的 "OfxImageClipPropComponents_<name>" 等
/// (默认 RGBA/Float/PAR=1HS:1717-1727);
/// 3. 调 actionout args 即协商产物,HS:1708-1718);
/// 4. 回灌:per-clip 分量/位深/像素比写回 clip 实例属性,
/// 输出帧率/fielding/premult 记入实例(HS:1721-1740)。
pub fn get_clip_preferences(&self) -> crate::error::Result<ClipPreferences> {
use crate::property::Value;
use crate::host::{
ACTION_GET_CLIP_PREFERENCES, CLIP_PREF_COMPONENTS, CLIP_PREF_DEPTH, CLIP_PREF_PAR,
PROP_CONTINUOUS_SAMPLES, PROP_FIELD_ORDER, PROP_FRAME_RATE, PROP_FRAME_VARYING,
PROP_PREMULT,
};
// 1. clipPrefsStuffsHS:1697-1706 的默认值)。
let out = PropertySet::new();
out.set_one(PROP_FRAME_RATE, Value::Double(1.0));
out.set_one(PROP_PREMULT, Value::String(CString::new("").unwrap()));
out.set_one(PROP_FIELD_ORDER, Value::String(CString::new("").unwrap()));
out.set_one(PROP_CONTINUOUS_SAMPLES, Value::Int(0));
out.set_one(PROP_FRAME_VARYING, Value::Int(0));
// 2. per-clip 预置(HS:1717-1727phase 1 全链路 F32+RGBA)。
for clip in &self.clips {
out.set_one(&crate::host::clip_pref_prop(CLIP_PREF_COMPONENTS, &clip.name), Value::String(CString::new("OfxImageComponentRGBA").unwrap()));
out.set_one(&crate::host::clip_pref_prop(CLIP_PREF_DEPTH, &clip.name), Value::String(CString::new("OfxBitDepthFloat").unwrap()));
out.set_one(&crate::host::clip_pref_prop(CLIP_PREF_PAR, &clip.name), Value::Double(1.0));
}
// 3. action。
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let empty = PropertySet::new();
let stat = unsafe {
self.plugin
.call_action(ACTION_GET_CLIP_PREFERENCES, inst_handle, &empty, &out)
};
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
return Err(crate::error::Error::Failed(format!(
"getClipPreferences 失败:{stat}"
)));
}
// 4. 回灌(HS:1721-1740:每 clip 的协商结果写回实例属性)。
let mut output_components = "OfxImageComponentRGBA".to_string();
let mut output_bit_depth = "OfxBitDepthFloat".to_string();
let mut output_par = 1.0;
for clip in &self.clips {
let comp_name = crate::host::clip_pref_prop(CLIP_PREF_COMPONENTS, &clip.name);
let depth_name = crate::host::clip_pref_prop(CLIP_PREF_DEPTH, &clip.name);
let par_name = crate::host::clip_pref_prop(CLIP_PREF_PAR, &clip.name);
let comps = out
.get(&comp_name, 0)
.map(|v| match v {
Value::String(s) => s.to_string_lossy().into_owned(),
_ => "OfxImageComponentRGBA".to_string(),
})
.unwrap_or_else(|| "OfxImageComponentRGBA".to_string());
let depth = out
.get(&depth_name, 0)
.map(|v| match v {
Value::String(s) => s.to_string_lossy().into_owned(),
_ => "OfxBitDepthFloat".to_string(),
})
.unwrap_or_else(|| "OfxBitDepthFloat".to_string());
let par = out
.get(&par_name, 0)
.and_then(|v| match v {
Value::Double(d) => Some(d),
_ => None,
})
.unwrap_or(1.0);
clip.props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS,
Value::String(CString::new(comps.clone()).unwrap()),
);
clip.props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
Value::String(CString::new(depth.clone()).unwrap()),
);
clip.props.set_one(
"OfxImagePropPixelAspectRatio",
Value::Double(par),
);
if clip.name == "Output" {
output_components = comps;
output_bit_depth = depth;
output_par = par;
}
}
let frame_rate = out
.get(PROP_FRAME_RATE, 0)
.and_then(|v| match v {
Value::Double(d) => Some(d),
_ => None,
})
.unwrap_or(24.0);
let field = out
.get(PROP_FIELD_ORDER, 0)
.map(|v| match v {
Value::String(s) => s.to_string_lossy().into_owned(),
_ => String::new(),
})
.unwrap_or_default();
Ok(ClipPreferences {
output_components,
output_bit_depth,
pixel_aspect_ratio: output_par,
frame_rate,
field,
})
}
/// getRegionOfDefinition actionHS: ofxhImageEffect.cpp:1087-1130
/// in args = time + renderScaleout args = RegionOfDefinition)。
pub fn get_region_of_definition(
&self,
time: f64,
scale: RenderScale,
) -> crate::error::Result<OfxRectD> {
use crate::property::Value;
use crate::host::{ACTION_GET_ROD, PROP_RENDER_SCALE, PROP_ROD, PROP_TIME};
let in_args = PropertySet::new();
in_args.set_one(PROP_TIME, Value::Double(time));
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(scale.x), Value::Double(scale.y)]);
// out args 预定义(HS 行为:宿主先建属性表,插件只管写——
// 缺失属性上插件的 propSet 会失败被忽略)。
let out = PropertySet::new();
out.define(
PROP_ROD,
vec![
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
],
);
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let stat = unsafe {
self.plugin
.call_action(ACTION_GET_ROD, inst_handle, &in_args, &out)
};
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
return Err(crate::error::Error::Failed(format!("getRoD 失败:{stat}")));
}
read_rect(&out, PROP_ROD).ok_or(crate::error::Error::Failed("getRoD 无输出".into()))
}
/// getRegionsOfInterest action:输入 clip 的 RoI 写入 out_args
/// 属性集,返回值即各 clip 的 RoI 列表(与 `clips` 顺序一致)。
///
/// 约定名 "OfxImageEffectPropRegionOfInterest_<clipname>"HS:
/// ofxhImageEffect.cpp getRegionsOfInterest 的 per-clip 前缀)。
pub fn get_regions_of_interest(
&self,
time: f64,
scale: RenderScale,
region: OfxRectD,
) -> crate::error::Result<Vec<OfxRectD>> {
use crate::property::Value;
use crate::host::{ACTION_GET_ROI, PROP_RENDER_SCALE, PROP_ROI, PROP_TIME};
let in_args = PropertySet::new();
in_args.set_one(PROP_TIME, Value::Double(time));
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(scale.x), Value::Double(scale.y)]);
in_args.define(
PROP_ROI,
vec![
Value::Double(region.x1),
Value::Double(region.y1),
Value::Double(region.x2),
Value::Double(region.y2),
],
);
// out args 预定义 per-clip ROI 属性(HS 约定名前缀)。
let out = PropertySet::new();
for clip in &self.clips {
let name = format!("OfxImageEffectPropRegionOfInterest_{}", clip.name);
out.define(
&name,
vec![
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
],
);
}
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let stat = unsafe {
self.plugin
.call_action(ACTION_GET_ROI, inst_handle, &in_args, &out)
};
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
return Err(crate::error::Error::Failed(format!("getRoI 失败:{stat}")));
}
let mut rois = Vec::with_capacity(self.clips.len());
for clip in &self.clips {
let name = format!("OfxImageEffectPropRegionOfInterest_{}", clip.name);
let rect = read_rect(&out, &name)
// 未设置 → 默认整个 region(OFX 语义:插件可不写)。
.unwrap_or(region);
rois.push(rect);
}
Ok(rois)
}
/// isIdentity action:返回 Some((time, input_clip_name)) 表示本帧
/// 直接透传该输入 clipNone 表示需要真正 render。
///
/// 参照 HS: ofxhImageEffect.cpp:1378-1450in args = time + scale +
/// renderWindow + fieldToRenderout args = kOfxImageEffectPropIsIdentity
/// (输入 clip 名)+ kOfxPropTime(透传时间,可改)。
pub fn is_identity(&self, time: f64) -> crate::error::Result<Option<(f64, String)>> {
use crate::property::Value;
use crate::host::{
ACTION_IS_IDENTITY, PROP_FIELD_TO_RENDER, PROP_IS_IDENTITY, PROP_RENDER_SCALE,
PROP_RENDER_WINDOW, PROP_TIME,
};
let in_args = PropertySet::new();
in_args.set_one(PROP_TIME, Value::Double(time));
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(1.0), Value::Double(1.0)]);
in_args.define(
PROP_RENDER_WINDOW,
vec![
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
],
);
in_args.set_one(PROP_FIELD_TO_RENDER, Value::String(CString::new("OfxImageFieldBoth").unwrap()));
// out args 预定义:IsIdentityString+ TimeDouble)。
let out = PropertySet::new();
out.set_one(PROP_IS_IDENTITY, Value::String(CString::new("").unwrap()));
out.set_one(PROP_TIME, Value::Double(time));
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let stat = unsafe {
self.plugin
.call_action(ACTION_IS_IDENTITY, inst_handle, &in_args, &out)
};
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
return Err(crate::error::Error::Failed(format!("isIdentity 失败:{stat}")));
}
let identity = out.get(PROP_IS_IDENTITY, 0);
match identity {
Some(Value::String(s)) if !s.is_empty() => {
let clip_name = s.to_string_lossy().into_owned();
let t = out
.get(PROP_TIME, 0)
.and_then(|v| match v {
Value::Double(d) => Some(d),
_ => None,
})
.unwrap_or(time);
Ok(Some((t, clip_name)))
}
_ => Ok(None),
}
}
/// render action 的 in args 组装(CPU/GL 共用;GL 模式加
/// kOfxImageEffectPropOpenGLEnabled=1ofxGPURender.h:117)。
fn render_in_args(time: f64, scale: RenderScale, window: OfxRectD, gl_enabled: bool) -> PropertySet {
use crate::property::Value;
use crate::host::{
PROP_FIELD_TO_RENDER, PROP_INTERACTIVE_RENDER, PROP_NO_SPATIAL_AWARENESS,
PROP_RENDER_QUALITY_DRAFT, PROP_RENDER_SCALE, PROP_RENDER_WINDOW,
PROP_SEQUENTIAL_RENDER, PROP_TIME,
};
let in_args = PropertySet::new();
in_args.set_one(PROP_TIME, Value::Double(time));
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(scale.x), Value::Double(scale.y)]);
in_args.define(
PROP_RENDER_WINDOW,
vec![
Value::Double(window.x1),
Value::Double(window.y1),
Value::Double(window.x2),
Value::Double(window.y2),
],
);
in_args.set_one(PROP_FIELD_TO_RENDER, Value::String(CString::new("OfxImageFieldBoth").unwrap()));
in_args.set_one(PROP_SEQUENTIAL_RENDER, Value::Int(0));
in_args.set_one(PROP_INTERACTIVE_RENDER, Value::Int(0));
in_args.set_one(PROP_RENDER_QUALITY_DRAFT, Value::Int(0));
in_args.set_one(PROP_NO_SPATIAL_AWARENESS, Value::Int(0));
if gl_enabled {
in_args.set_one(crate::host::PROP_GL_ENABLED, Value::Int(1));
}
in_args
}
/// render actionCPU 路径)。`output` 为已按协商格式分配的输出
/// 图像(Arc:插件经 clipGetImage(Output) 取用,render 驱动与
/// suite 各自持强引用);输入 clip 的图像由调用方经
/// [`ClipInstance::fetch_image`] 备好。GL 路径见
/// [`Instance::render_gl`]。
///
/// 声明原为 `&mut Image`:输出图像需与 suite 共享强引用(TLS +
/// LIVE_IMAGES 表),&mut 无法表达——改为 `Arc<Image>`。
pub fn render(
&self,
time: f64,
scale: RenderScale,
window: OfxRectD,
output: std::sync::Arc<crate::image::Image>,
) -> crate::error::Result<()> {
use crate::host::ACTION_RENDER;
let in_args = Self::render_in_args(time, scale, window, false);
// facade 取消标记:render 入口即短路(多帧循环的帧间取消)。
if self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
return Err(crate::error::Error::Failed("已取消".into()));
}
// 渲染上下文(TLS):timeline/progress/clipGetImage 读取。
let range = self
.sequence_range
.lock()
.unwrap_or_else(|e| e.into_inner())
.unwrap_or(OfxRangeD { min: 0.0, max: 0.0 });
crate::suites::set_render_ctx(Some(crate::suites::RenderCtx {
time,
scale,
range,
}));
crate::suites::set_current_output(Some(output.clone()));
// 进度报告器(facade 回调 → Progress suite)。
if let Some((cb, userdata)) = self
.progress_cb
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
{
crate::suites::progress::set_current(Some(unsafe {
crate::progress::ProgressReporter::new(cb, userdata as *mut std::ffi::c_void)
}));
}
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let out = PropertySet::new();
let stat = unsafe {
self.plugin
.call_action(ACTION_RENDER, inst_handle, &in_args, &out)
};
// 清理 TLS(action 返回后渲染上下文必须消失)。
crate::suites::set_current_output(None);
crate::suites::set_render_ctx(None);
crate::suites::progress::set_current(None);
if stat != crate::suites::status::OK {
return Err(crate::error::Error::Failed(format!("render 失败:{stat}")));
}
Ok(())
}
/// GL render actionM11 §4ofxGPURender.h 的 Render 动作 GL
/// 模式)。与 [`Instance::render`]CPU)并存。
///
/// 前置契约(render 驱动遵守):调用方已把 `renderer` 的 GL
/// 上下文置为 currentofxGPURender.h "OpenGL Current Context"
/// 宿主只在 Render/Begin/EndSequenceRender/Attach/Detach 期间
/// 要求上下文 current——本实现的约定是 oakrender 的 PluginJob
/// 路径在进入前做好),且 `output_texture` 已附着为渲染器输出
/// 目标(等价 C++ `PluginRenderer::attach_output_texture`)。
///
/// action 序列:kOfxActionOpenGLContextAttached → renderin args
/// 带 kOfxImageEffectPropOpenGLEnabled=1)→
/// kOfxActionOpenGLContextDetachedofxGPURender.h:345-371
/// attach/detach 必须配对)。渲染结果留在 GL 输出纹理上(插件
/// 直接画进附着目标),宿主不做 CPU 回读;GL 模式下
/// clipGetImage(Output) 不可用(插件按规范走 OpenGL suite——
/// ofxGPURender.h "the effect SHOULD access all its images through
/// the OpenGL suite")。render 返回前对未释放的输入 GL 纹理做
/// 兜底清理([`crate::suites::gl_render::purge_leftovers`])。
pub fn render_gl(
&self,
time: f64,
scale: RenderScale,
window: OfxRectD,
renderer: crate::bridge::render::RendererHandle,
output_texture: crate::bridge::render::TextureHandle,
) -> crate::error::Result<()> {
use crate::host::ACTION_RENDER;
let in_args = Self::render_in_args(time, scale, window, true);
if self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
return Err(crate::error::Error::Failed("已取消".into()));
}
let range = self
.sequence_range
.lock()
.unwrap_or_else(|e| e.into_inner())
.unwrap_or(OfxRangeD { min: 0.0, max: 0.0 });
crate::suites::set_render_ctx(Some(crate::suites::RenderCtx {
time,
scale,
range,
}));
// GL 纹理位深协商(插件 kOfxOpenGLPropPixelDepth → 管线 F32
// 调用方已按协商门(render 驱动)决定走 GL)。
let gl_pixel_depth =
crate::suites::gl_render::pick_gl_pixel_depth(&self.plugin.descriptor.props)
.unwrap_or("OfxBitDepthFloat");
crate::suites::set_gl_ctx(Some(crate::suites::GlCtx {
renderer,
output_texture,
gl_pixel_depth,
}));
// GL 模式无 CPU 输出图像(current_output 保持 None)。
if let Some((cb, userdata)) = self
.progress_cb
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
{
crate::suites::progress::set_current(Some(unsafe {
crate::progress::ProgressReporter::new(cb, userdata as *mut std::ffi::c_void)
}));
}
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let empty = PropertySet::new();
// attach(失败仍继续——插件可忽略;规范允许 ReplyDefault)。
let stat = unsafe {
self.plugin
.call_action(crate::host::ACTION_GL_CONTEXT_ATTACHED, inst_handle, &empty, &empty)
};
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
crate::suites::set_gl_ctx(None);
crate::suites::set_render_ctx(None);
crate::suites::progress::set_current(None);
return Err(crate::error::Error::Failed(format!(
"OpenGLContextAttached 失败:{stat}"
)));
}
let out = PropertySet::new();
let stat = unsafe {
self.plugin
.call_action(ACTION_RENDER, inst_handle, &in_args, &out)
};
// detach(必须与 attach 配对;ofxGPURender.h:345-346)。
unsafe {
self.plugin
.call_action(crate::host::ACTION_GL_CONTEXT_DETACHED, inst_handle, &empty, &empty)
};
// 兜底:插件遗漏 clipFreeTexture 的输入纹理在此释放。
crate::suites::gl_render::purge_leftovers();
crate::suites::set_gl_ctx(None);
crate::suites::set_render_ctx(None);
crate::suites::progress::set_current(None);
if stat != crate::suites::status::OK {
return Err(crate::error::Error::Failed(format!("GL render 失败:{stat}")));
}
Ok(())
}
/// GetOutputColourspace actionM11 §4ofxColour.h:243-283):
/// 宿主给插件一份偏好色彩空间列表,插件回写输出 clip 的色彩
/// 空间(可为 "OfxColourspace_<clip>" 交叉引用)。
///
/// 返回解析后的输出色彩空间:交叉引用解析为所引 clip 的实际
/// 色彩空间(ofxColour.h:142-141 的跨 clip 引用约定);
/// REPLY_DEFAULT → 第一个输入 clip 的色彩空间(ofxColour.h:278-279)。
/// 调用方(render 驱动)随后把结果写回输出 clip(
/// [`Instance::set_output_colourspace`])。
pub fn get_output_colourspace(&self, preferred: &[String]) -> crate::error::Result<String> {
use crate::property::Value;
use crate::host::{ACTION_GET_OUTPUT_COLOURSPACE, PROP_CLIP_COLOURSPACE, PROP_CLIP_PREFERRED_COLOURSPACES};
let in_args = PropertySet::new();
let values: Vec<Value> = preferred
.iter()
.map(|s| Value::String(CString::new(s.as_str()).unwrap()))
.collect();
in_args.define(PROP_CLIP_PREFERRED_COLOURSPACES, values);
let out = PropertySet::new();
out.set_one(PROP_CLIP_COLOURSPACE, Value::String(CString::new("").unwrap()));
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let stat = unsafe {
self.plugin
.call_action(ACTION_GET_OUTPUT_COLOURSPACE, inst_handle, &in_args, &out)
};
if stat == crate::suites::status::OK {
let value = out
.get(PROP_CLIP_COLOURSPACE, 0)
.map(|v| match v {
Value::String(s) => s.to_string_lossy().into_owned(),
_ => String::new(),
})
.unwrap_or_default();
if value.is_empty() {
return Err(crate::error::Error::Failed(
"GetOutputColourspace 未回写色彩空间".into(),
));
}
Ok(self.resolve_colourspace(value))
} else if stat == crate::suites::status::REPLY_DEFAULT {
// 插件未实现 → 用第一个输入 clip 的色彩空间(规范默认)。
let first_input = self
.clips
.iter()
.find(|c| c.name != "Output")
.map(|c| {
c.props
.get(PROP_CLIP_COLOURSPACE, 0)
.map(|v| match v {
Value::String(s) => s.to_string_lossy().into_owned(),
_ => String::new(),
})
.unwrap_or_default()
})
.unwrap_or_default();
Ok(first_input)
} else {
Err(crate::error::Error::Failed(format!(
"GetOutputColourspace 失败:{stat}"
)))
}
}
/// 把输出 clip 的色彩空间写回(GetOutputColourspace 后的落点;
/// ofxColour.h:262-264 "the host must set kOfxImageClipPropColourspace
/// on the instance's output clip to the value from outArgs")。
pub fn set_output_colourspace(&self, colourspace: &str) {
use crate::property::Value;
if let Some(output) = self.clips.iter().find(|c| c.name == "Output") {
output.props.set_one(
crate::host::PROP_CLIP_COLOURSPACE,
Value::String(CString::new(colourspace).unwrap()),
);
}
}
/// 解析 "OfxColourspace_<clip>" 交叉引用为所引 clip 的实际色彩
/// 空间;非交叉引用原样返回。
pub fn resolve_colourspace(&self, value: String) -> String {
const PREFIX: &str = "OfxColourspace_";
if let Some(clip_name) = value.strip_prefix(PREFIX) {
self.clips
.iter()
.find(|c| c.name == clip_name)
.map(|c| {
c.props
.get(crate::host::PROP_CLIP_COLOURSPACE, 0)
.map(|v| match v {
crate::property::Value::String(s) => s.to_string_lossy().into_owned(),
_ => value.clone(),
})
.unwrap_or(value.clone())
})
.unwrap_or(value)
} else {
value
}
}
/// begin/endSequenceRender 配对(帧序列渲染的前后括号;
/// HS: ofxhImageEffect.cpp:473-513 的 in argsframeRange +
/// frameStep + renderScale + sequentialRenderStatus)。GL 模式
/// 变体 [`Instance::begin_sequence_render_gl`] 额外带
/// kOfxImageEffectPropOpenGLEnabledofxGPURender.h:117)。
pub fn begin_sequence_render(&self, range: OfxRangeD) -> crate::error::Result<()> {
self.begin_sequence_inner(range, false)
}
/// GL 模式的 beginSequenceRenderin args 带 OpenGLEnabled=1)。
pub fn begin_sequence_render_gl(&self, range: OfxRangeD) -> crate::error::Result<()> {
self.begin_sequence_inner(range, true)
}
fn begin_sequence_inner(&self, range: OfxRangeD, gl_enabled: bool) -> crate::error::Result<()> {
use crate::host::ACTION_BEGIN_SEQUENCE;
let in_args = Self::sequence_in_args(range, gl_enabled);
self.sequence_range
.lock()
.unwrap_or_else(|e| e.into_inner())
.replace(range);
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let out = PropertySet::new();
let stat = unsafe {
self.plugin
.call_action(ACTION_BEGIN_SEQUENCE, inst_handle, &in_args, &out)
};
if stat != crate::suites::status::OK {
return Err(crate::error::Error::Failed(format!(
"beginSequenceRender 失败:{stat}"
)));
}
Ok(())
}
/// 序列 action 的 in args 组装(begin/end 共用;GL 模式加
/// OpenGLEnabled=1)。
fn sequence_in_args(range: OfxRangeD, gl_enabled: bool) -> PropertySet {
use crate::property::Value;
use crate::host::{PROP_FRAME_RANGE, PROP_FRAME_STEP, PROP_RENDER_SCALE, PROP_SEQUENTIAL_RENDER};
let in_args = PropertySet::new();
in_args.define(PROP_FRAME_RANGE, vec![Value::Double(range.min), Value::Double(range.max)]);
in_args.set_one(PROP_FRAME_STEP, Value::Double(1.0));
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(1.0), Value::Double(1.0)]);
in_args.set_one(PROP_SEQUENTIAL_RENDER, Value::Int(1));
if gl_enabled {
in_args.set_one(crate::host::PROP_GL_ENABLED, Value::Int(1));
}
in_args
}
/// 见 [`Instance::begin_sequence_render`]。
pub fn end_sequence_render(&self, range: OfxRangeD) -> crate::error::Result<()> {
self.end_sequence_inner(range, false)
}
/// GL 模式的 endSequenceRender。
pub fn end_sequence_render_gl(&self, range: OfxRangeD) -> crate::error::Result<()> {
self.end_sequence_inner(range, true)
}
fn end_sequence_inner(&self, range: OfxRangeD, gl_enabled: bool) -> crate::error::Result<()> {
use crate::host::ACTION_END_SEQUENCE;
let in_args = Self::sequence_in_args(range, gl_enabled);
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let out = PropertySet::new();
let stat = unsafe {
self.plugin
.call_action(ACTION_END_SEQUENCE, inst_handle, &in_args, &out)
};
self.sequence_range
.lock()
.unwrap_or_else(|e| e.into_inner())
.take();
if stat != crate::suites::status::OK {
return Err(crate::error::Error::Failed(format!(
"endSequenceRender 失败:{stat}"
)));
}
Ok(())
}
/// 销毁(destroyInstance action)。析构由 RefBox 驱动;
/// 此处只做 action 通知,幂等([`Instance::drop`] 的
/// `destroyed` 门保证只发一次)。
pub(crate) fn notify_destroy(&self) {
use crate::host::ACTION_DESTROY_INSTANCE;
let inst_handle = crate::suites::tag::make(
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
let empty = PropertySet::new();
// 通知失败只记日志(销毁路径不可回滚)。
let stat = unsafe {
self.plugin
.call_action(ACTION_DESTROY_INSTANCE, inst_handle, &empty, &empty)
};
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
eprintln!("destroyInstance 通知失败:{stat}");
}
}
}
/// 从属性集读矩形(Double×4)。
fn read_rect(props: &PropertySet, name: &str) -> Option<OfxRectD> {
use crate::property::Value;
let x1 = match props.get(name, 0)? {
Value::Double(d) => d,
_ => return None,
};
let y1 = match props.get(name, 1)? {
Value::Double(d) => d,
_ => return None,
};
let x2 = match props.get(name, 2)? {
Value::Double(d) => d,
_ => return None,
};
let y2 = match props.get(name, 3)? {
Value::Double(d) => d,
_ => return None,
};
Some(OfxRectD { x1, y1, x2, y2 })
}
/// getClipPreferences 的协商结果。
#[derive(Clone, Debug)]
pub struct ClipPreferences {
/// 输出分量(kOfxImageComponentRGBA 等)。
pub output_components: String,
/// 输出位深(kOfxBitDepthFloat 等;全链路 F32 下恒为 float)。
pub output_bit_depth: String,
/// 像素比。
pub pixel_aspect_ratio: f64,
/// 帧率。
pub frame_rate: f64,
/// field 处理模式(kOfxImageField*)。
pub field: String,
}
+63
View File
@@ -0,0 +1,63 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! # oakplugin — Oak 的插件模块(自研 OFX 宿主)
//!
//! 本 crate 是 oakplugin 模块的全部实现(M11):
//!
//! - **OFX 宿主**:扫描 bundle、加载插件、实现八张 suite
//! property/memory/image_effect/param/message/progress/timeline/
//! multithread),驱动 describe/createInstance/render 等 action。
//! - **桥**:把 OFX 实例的参数接到 oaknode[`bridge::node`])、把
//! clip 的输入输出接到 oakrender 纹理([`bridge::render`])、把
//! 参数修改包成 undo 命令([`bridge::undo`])。
//! - **C ABI 出口**[`ffi`]):逐字实现 `include/plugin/*.h`。
//!
//! ## FFI 纪律(全 crate 最高优先级约定)
//!
//! 1. 每个 `extern "C"` 导出函数体必须包
//! [`handle::guard`]/[`handle::guard_ptr`]catch_unwind + 错误码
//! 映射)。panic 越过 FFI 边界是 release 阻断级缺陷。
//! 2. 句柄一律 [`handle::RefBox`]`ctx` 是不透明指针,含义只在本
//! crate 内解释。
//! 3. 插件可在任意自起线程回调 suitemultithread suite 存活期
//! 内);一切共享状态走 `Mutex`,句柄注册表见 [`handle::Registry`]。
//! 4. OFX 语义以 openfx HostSupport 为参照系;协商与时序实现点必须
//! 注释对应 HostSupport 文件与行号(格式:`// HS: ofxhImageEffect.cpp:2776`)。
//!
//! ## 第 1 期范围
//!
//! filter/generator/transition 上下文;CPU 渲染路径。GL 纹理 suite
//! 与 ofxColour 为第 2 期([`clip`]/[`image`] 内以 `// [P2]` 标记
//! 预留点)。
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
pub mod bridge;
pub mod clip;
pub mod descriptor;
pub mod error;
pub mod ffi;
pub mod handle;
pub mod host;
pub mod image;
pub mod instance;
pub mod param;
pub mod progress;
pub mod property;
pub mod render_driver;
pub mod suites;
+812
View File
@@ -0,0 +1,812 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! 参数体系:12 种参数实例 + param ↔ oaknode 桥。
//!
//! 对应 C++ 的 `ParamInstance`/`OliveParamInstance`。桥的语义
//! (M9 已定):节点输入值变化 → 写回 OFX 参数;OFX 参数被插件
//! 改动 → 经 oaknode C ABI 回写节点(undoable 经
//! [`crate::bridge::undo`])。
//!
//! 句柄约定([`crate::suites::tag`]):`ParamDef`/`ParamInstance`
//! `#[repr(C)]` 且 `props` 在偏移 0;元素装箱(`Vec<Box<..>>`)保证
//! Vec 重分配不移动对象、句柄不悬垂。
//!
//! `// TODO(bridge)``set_from_node`/`notify_instance_changed` 依赖
//! bridge::node 的 `Value` 布局(声明尚未冻结),保留 todo!()。
//! **已落地(M11 第 1 期)**Value 布局随 [`crate::bridge::node`]
//! 冻结(`include/node/node.h` 的 `oaknode_value` POD),两处 todo
//! 已实现。
use std::ffi::CString;
use crate::property::{PropertySet, Value};
// ---- OFX 参数类型字符串(ofxParam.h----
/// kOfxParamTypeInteger。
pub const TYPE_INTEGER: &str = "OfxParamTypeInteger";
/// kOfxParamTypeInteger2D。
pub const TYPE_INTEGER2D: &str = "OfxParamTypeInteger2D";
/// kOfxParamTypeInteger3D。
pub const TYPE_INTEGER3D: &str = "OfxParamTypeInteger3D";
/// kOfxParamTypeDouble。
pub const TYPE_DOUBLE: &str = "OfxParamTypeDouble";
/// kOfxParamTypeDouble2D。
pub const TYPE_DOUBLE2D: &str = "OfxParamTypeDouble2D";
/// kOfxParamTypeDouble3D。
pub const TYPE_DOUBLE3D: &str = "OfxParamTypeDouble3D";
/// kOfxParamTypeBoolean。
pub const TYPE_BOOLEAN: &str = "OfxParamTypeBoolean";
/// kOfxParamTypeChoice。
pub const TYPE_CHOICE: &str = "OfxParamTypeChoice";
/// kOfxParamTypeString。
pub const TYPE_STRING: &str = "OfxParamTypeString";
/// kOfxParamTypeStrChoice。
pub const TYPE_STRCHOICE: &str = "OfxParamTypeStrChoice";
/// kOfxParamTypeRGB。
pub const TYPE_RGB: &str = "OfxParamTypeRGB";
/// kOfxParamTypeRGBA。
pub const TYPE_RGBA: &str = "OfxParamTypeRGBA";
/// kOfxParamTypeBytes。
pub const TYPE_BYTES: &str = "OfxParamTypeBytes";
/// kOfxParamTypeCustom。
pub const TYPE_CUSTOM: &str = "OfxParamTypeCustom";
/// kOfxParamTypePushButton。
pub const TYPE_PUSHBUTTON: &str = "OfxParamTypePushButton";
/// kOfxParamTypeGroup。
pub const TYPE_GROUP: &str = "OfxParamTypeGroup";
/// kOfxParamTypePage。
pub const TYPE_PAGE: &str = "OfxParamTypePage";
/// kOfxParamTypeParametric(第 1 期不支持,paramDefine 拒绝)。
pub const TYPE_PARAMETRIC: &str = "OfxParamTypeParametric";
// ---- 属性名(ofxParam.h / ofxCore.h----
/// kOfxParamPropSecret。
pub(crate) const P_SECRET: &str = "OfxParamPropSecret";
/// kOfxParamPropHint。
pub(crate) const P_HINT: &str = "OfxParamPropHint";
/// kOfxParamPropScriptName。
pub(crate) const P_SCRIPT_NAME: &str = "OfxParamPropScriptName";
/// kOfxParamPropParent。
pub(crate) const P_PARENT: &str = "OfxParamPropParent";
/// kOfxParamPropEnabled。
pub(crate) const P_ENABLED: &str = "OfxParamPropEnabled";
/// kOfxParamPropDataPtr。
pub(crate) const P_DATA_PTR: &str = "OfxParamPropDataPtr";
/// kOfxParamPropType。
pub(crate) const P_TYPE: &str = "OfxParamPropType";
/// kOfxParamPropIsAnimating。
pub(crate) const P_IS_ANIMATING: &str = "OfxParamPropIsAnimating";
/// kOfxParamPropIsAutoKeying。
pub(crate) const P_IS_AUTO_KEYING: &str = "OfxParamPropIsAutoKeying";
/// kOfxParamPropPersistant。
pub(crate) const P_PERSISTANT: &str = "OfxParamPropPersistant";
/// kOfxParamPropEvaluateOnChange。
pub(crate) const P_EVALUATE_ON_CHANGE: &str = "OfxParamPropEvaluateOnChange";
/// kOfxParamPropCanUndo。
pub(crate) const P_CAN_UNDO: &str = "OfxParamPropCanUndo";
/// kOfxParamPropCacheInvalidation。
pub(crate) const P_CACHE_INVALIDATION: &str = "OfxParamPropCacheInvalidation";
/// kOfxParamInvalidateValueChangeP_CACHE_INVALIDATION 的默认值)。
pub(crate) const V_INVALIDATE_VALUE_CHANGE: &str = "OfxParamInvalidateValueChange";
/// kOfxParamPropAnimates。
pub(crate) const P_ANIMATES: &str = "OfxParamPropAnimates";
/// kOfxParamPropDefault。
pub(crate) const P_DEFAULT: &str = "OfxParamPropDefault";
/// kOfxParamPropDisplayMin。
pub(crate) const P_DISPLAY_MIN: &str = "OfxParamPropDisplayMin";
/// kOfxParamPropDisplayMax。
pub(crate) const P_DISPLAY_MAX: &str = "OfxParamPropDisplayMax";
/// kOfxParamPropMin。
pub(crate) const P_MIN: &str = "OfxParamPropMin";
/// kOfxParamPropMax。
pub(crate) const P_MAX: &str = "OfxParamPropMax";
/// kOfxParamPropIncrement。
pub(crate) const P_INCREMENT: &str = "OfxParamPropIncrement";
/// kOfxParamPropDigits。
pub(crate) const P_DIGITS: &str = "OfxParamPropDigits";
/// kOfxParamPropDoubleType。
pub(crate) const P_DOUBLE_TYPE: &str = "OfxParamPropDoubleType";
/// kOfxParamDoubleTypePlain。
pub(crate) const V_DOUBLE_TYPE_PLAIN: &str = "OfxParamDoubleTypePlain";
/// kOfxParamPropDefaultCoordinateSystem。
pub(crate) const P_DEFAULT_COORD_SYS: &str = "OfxParamPropDefaultCoordinateSystem";
/// kOfxParamCoordinatesCanonical。
pub(crate) const V_COORD_CANONICAL: &str = "OfxParamCoordinatesCanonical";
/// kOfxParamPropShowTimeMarker。
pub(crate) const P_SHOW_TIME_MARKER: &str = "OfxParamPropShowTimeMarker";
/// kOfxParamPropDimensionLabel。
pub(crate) const P_DIMENSION_LABEL: &str = "OfxParamPropDimensionLabel";
/// kOfxParamPropStringMode。
pub(crate) const P_STRING_MODE: &str = "OfxParamPropStringMode";
/// kOfxParamStringIsSingleLine。
pub(crate) const V_STRING_SINGLE_LINE: &str = "OfxParamStringIsSingleLine";
/// kOfxParamPropStringFilePathExists。
pub(crate) const P_STRING_FILE_EXISTS: &str = "OfxParamPropStringFilePathExists";
/// kOfxParamPropChoiceOption。
pub(crate) const P_CHOICE_OPTION: &str = "OfxParamPropChoiceOption";
/// kOfxParamPropCustomInterpCallbackV1。
pub(crate) const P_CUSTOM_INTERP: &str = "OfxParamPropCustomCallbackV1";
/// kOfxParamPropPageChild。
pub(crate) const P_PAGE_CHILD: &str = "OfxParamPropPageChild";
/// kOfxParamPropGroupOpen。
pub(crate) const P_GROUP_OPEN: &str = "OfxParamPropGroupOpen";
/// kOfxParamPropInteractV1。
pub(crate) const P_INTERACT_V1: &str = "OfxParamPropInteractV1";
/// kOfxParamPropInteractSize。
pub(crate) const P_INTERACT_SIZE: &str = "OfxParamPropInteractSize";
/// kOfxParamPropInteractSizeAspect。
pub(crate) const P_INTERACT_ASPECT: &str = "OfxParamPropInteractSizeAspect";
/// kOfxParamPropInteractMinimumSize。
pub(crate) const P_INTERACT_MIN_SIZE: &str = "OfxParamPropInteractMinimumSize";
/// kOfxParamPropInteractPreferedSize。
pub(crate) const P_INTERACT_PREF_SIZE: &str = "OfxParamPropInteractPreferedSize";
/// kOfxPropType。
pub(crate) const PROP_TYPE: &str = "OfxPropType";
/// kOfxPropName。
pub(crate) const PROP_NAME: &str = "OfxPropName";
/// kOfxPropLabel。
pub(crate) const PROP_LABEL: &str = "OfxPropLabel";
/// kOfxPropShortLabel。
pub(crate) const PROP_SHORT_LABEL: &str = "OfxPropShortLabel";
/// kOfxPropLongLabel。
pub(crate) const PROP_LONG_LABEL: &str = "OfxPropLongLabel";
/// kOfxPropIcon。
pub(crate) const PROP_ICON: &str = "OfxPropIcon";
/// kOfxTypeParameter。
pub(crate) const TYPE_PARAMETER: &str = "OfxTypeParameter";
fn cs(s: &str) -> CString {
// 静态 ASCII 常量,无内嵌 NUL。
CString::new(s).unwrap()
}
/// 参数值类别(param shim 的变长参数分发;与
/// cbits/ofx_param_shim.c 的 KIND_* 枚举逐字对应)。
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ParamKind {
/// Integer。
Int = 1,
/// Integer2D。
Int2 = 2,
/// Integer3D。
Int3 = 3,
/// Double。
Double = 4,
/// Double2D。
Double2 = 5,
/// Double3D。
Double3 = 6,
/// Boolean。
Bool = 7,
/// Choice。
Choice = 8,
/// RGB。
Rgb = 9,
/// RGBA。
Rgba = 10,
/// String。
Str = 11,
/// StrChoice。
StrChoice = 12,
}
/// OFX 类型字符串 → 值类别 + 维度(HS: ofxhParam.cpp `findType`)。
/// 无值类(PushButton/Group/Page/Unknown)返回 None。
pub(crate) fn kind_of_type(ofx_type: &str) -> Option<(ParamKind, usize)> {
match ofx_type {
TYPE_INTEGER => Some((ParamKind::Int, 1)),
TYPE_INTEGER2D => Some((ParamKind::Int2, 2)),
TYPE_INTEGER3D => Some((ParamKind::Int3, 3)),
TYPE_DOUBLE => Some((ParamKind::Double, 1)),
TYPE_DOUBLE2D => Some((ParamKind::Double2, 2)),
TYPE_DOUBLE3D => Some((ParamKind::Double3, 3)),
TYPE_BOOLEAN => Some((ParamKind::Bool, 1)),
TYPE_CHOICE => Some((ParamKind::Choice, 1)),
TYPE_STRING => Some((ParamKind::Str, 1)),
TYPE_STRCHOICE => Some((ParamKind::StrChoice, 1)),
TYPE_RGB => Some((ParamKind::Rgb, 3)),
TYPE_RGBA => Some((ParamKind::Rgba, 4)),
_ => None,
}
}
/// 数值类参数(Min/Max/DisplayMin/Max 表格适用;HS
/// `addNumericParamProps` 的 isDoubleParam || isIntParam ||
/// isColourParam)。
fn is_numeric_type(ofx_type: &str) -> bool {
matches!(
ofx_type,
TYPE_INTEGER
| TYPE_INTEGER2D
| TYPE_INTEGER3D
| TYPE_DOUBLE
| TYPE_DOUBLE2D
| TYPE_DOUBLE3D
| TYPE_RGB
| TYPE_RGBA
)
}
/// 该类型的默认 ParamValuedescribe 期基线;HS 的属性默认在 props
/// 的 kOfxParamPropDefault,值语义等价)。
fn type_default(ofx_type: &str) -> ParamValue {
match kind_of_type(ofx_type) {
Some((ParamKind::Int, d)) | Some((ParamKind::Int2, d)) | Some((ParamKind::Int3, d)) => {
ParamValue::Int([0; 3], d)
}
Some((ParamKind::Double, d))
| Some((ParamKind::Double2, d))
| Some((ParamKind::Double3, d)) => ParamValue::Double([0.0; 3], d),
Some((ParamKind::Rgb, d)) | Some((ParamKind::Rgba, d)) => ParamValue::Color([0.0; 4], d),
Some((ParamKind::Bool, _)) => ParamValue::Bool(false),
Some((ParamKind::Choice, _)) => ParamValue::Choice(0),
Some((ParamKind::Str, _)) => ParamValue::String(cs("")),
Some((ParamKind::StrChoice, _)) => ParamValue::StrChoice(cs("")),
None => match ofx_type {
TYPE_BYTES | TYPE_CUSTOM => ParamValue::Bytes(Vec::new()),
TYPE_PUSHBUTTON => ParamValue::PushButton,
TYPE_GROUP | TYPE_PAGE => ParamValue::Container,
_ => ParamValue::Container, // 未知类型占位(paramDefine 已拒绝)
},
}
}
/// 数值属性表的值(Min/Max/Display 族;HS ofxhParam.cpp:420-449)。
/// `is_max` 选 ±f64::MAX / ±i32::MAXcolour 参数恒 0/1。
fn numeric_value(ofx_type: &str, kind: ParamKind, dim: usize, is_max: bool) -> Vec<Value> {
if ofx_type == TYPE_RGB || ofx_type == TYPE_RGBA {
// colourdisplay 范围 0..1min/max 同。
let v = Value::Double(if is_max { 1.0 } else { 0.0 });
return vec![v; dim];
}
match kind {
ParamKind::Double | ParamKind::Double2 | ParamKind::Double3 => {
let v = Value::Double(if is_max { f64::MAX } else { f64::MIN });
vec![v; dim]
}
_ => {
let v = Value::Int(if is_max { i32::MAX } else { i32::MIN });
vec![v; dim]
}
}
}
/// 参数值(与 OFX 参数类型一一对应)。
#[derive(Clone, Debug, PartialEq)]
pub enum ParamValue {
/// kOfxParamTypeInteger / Integer2D / Integer3D。
Int([i32; 3], usize),
/// kOfxParamTypeDouble / Double2D / Double3D。
Double([f64; 3], usize),
/// kOfxParamTypeBoolean。
Bool(bool),
/// kOfxParamTypeChoice(选项索引)。
Choice(i32),
/// kOfxParamTypeString。
String(std::ffi::CString),
/// kOfxParamTypeRGB / RGBA。
Color([f64; 4], usize),
/// kOfxParamTypeStrChoice。
StrChoice(std::ffi::CString),
/// kOfxParamTypeBytes / Custom(不透明字节串)。
Bytes(Vec<u8>),
/// kOfxParamTypePushButton(无值,仅触发)。
PushButton,
/// kOfxParamTypeGroup / Page(容器,无值)。
Container,
}
/// 参数定义(describe 产物,见 [`crate::descriptor::EffectDescriptor`])。
/// `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`])。
#[derive(Clone)]
#[repr(C)]
pub struct ParamDef {
/// 定义属性(label/hint/parent/coordinate-system/secret/
/// display min-max/choice 选项与排序等;describe 期预置
/// HostSupport 同款表格)。
pub props: PropertySet,
/// 参数名(OFX 标识)。
pub name: String,
/// OFX 类型字符串(kOfxParamType*)。
pub ofx_type: String,
/// 默认值(describe 后读取)。
pub default: ParamValue,
}
impl ParamDef {
/// 按 OFX 类型构建定义(镜像 HS ofxhParam.cpp:224-352
/// universalProps + addStandardParamProps 的分类型属性表)。
pub(crate) fn new(name: &str, ofx_type: &str) -> Self {
let mut props = PropertySet::new();
let uname = name.to_string();
let utype = ofx_type.to_string();
// universalPropsHS ofxhParam.cpp:229-246)。
props.set_one(PROP_TYPE, Value::String(cs(TYPE_PARAMETER)));
props.set_one(P_SECRET, Value::Int(0));
props.set_one(P_HINT, Value::String(cs("")));
props.set_one(P_SCRIPT_NAME, Value::String(cs(&uname)));
props.set_one(P_PARENT, Value::String(cs("")));
props.set_one(P_ENABLED, Value::Int(1));
props.set_one(P_DATA_PTR, Value::Pointer(std::ptr::null_mut()));
props.set_one(P_TYPE, Value::String(cs(&utype)));
props.set_one(PROP_NAME, Value::String(cs(&uname)));
props.set_one(PROP_LABEL, Value::String(cs(&uname)));
props.set_one(PROP_SHORT_LABEL, Value::String(cs(&uname)));
props.set_one(PROP_LONG_LABEL, Value::String(cs(&uname)));
props.define(PROP_ICON, vec![Value::String(cs("")), Value::String(cs(""))]);
// 值类参数(HS addValueParamPropsofxhParam.cpp:352-380)。
if let Some((kind, dim)) = kind_of_type(ofx_type) {
props.set_one(P_IS_ANIMATING, Value::Int(0));
props.set_one(P_IS_AUTO_KEYING, Value::Int(0));
props.set_one(P_PERSISTANT, Value::Int(1));
props.set_one(P_EVALUATE_ON_CHANGE, Value::Int(1));
props.set_one(P_CAN_UNDO, Value::Int(1));
props.set_one(P_CACHE_INVALIDATION, Value::String(cs(V_INVALIDATE_VALUE_CHANGE)));
// 可动画性(HS ofxhParam.cpp:367-372custom/string/
// boolean/choice 默认不可动画)。
let animates = ofx_type != TYPE_CUSTOM
&& ofx_type != TYPE_STRING
&& ofx_type != TYPE_BOOLEAN
&& ofx_type != TYPE_CHOICE;
props.set_one(P_ANIMATES, Value::Int(animates as i32));
props.define(P_DEFAULT, default_values(kind, dim));
// 数值类(HS addNumericParamPropsofxhParam.cpp:391-455)。
if is_numeric_type(ofx_type) {
props.define(P_DISPLAY_MIN, numeric_value(ofx_type, kind, dim, false));
props.define(P_DISPLAY_MAX, numeric_value(ofx_type, kind, dim, true));
props.define(P_MIN, numeric_value(ofx_type, kind, dim, false));
props.define(P_MAX, numeric_value(ofx_type, kind, dim, true));
if matches!(kind, ParamKind::Double | ParamKind::Double2 | ParamKind::Double3) {
props.set_one(P_INCREMENT, Value::Double(1.0));
props.set_one(P_DIGITS, Value::Int(2));
}
if ofx_type == TYPE_DOUBLE || ofx_type == TYPE_DOUBLE2D || ofx_type == TYPE_DOUBLE3D {
props.set_one(P_DOUBLE_TYPE, Value::String(cs(V_DOUBLE_TYPE_PLAIN)));
props.set_one(P_DEFAULT_COORD_SYS, Value::String(cs(V_COORD_CANONICAL)));
if dim == 1 {
props.set_one(P_SHOW_TIME_MARKER, Value::Int(0));
}
}
if dim == 2 || dim == 3 {
let labels: Vec<Value> = ["x", "y", "z"][..dim]
.iter()
.map(|l| Value::String(cs(l)))
.collect();
props.define(P_DIMENSION_LABEL, labels);
}
}
}
// 分类型附加(HS addStandardParamPropsofxhParam.cpp:248-301)。
match ofx_type {
TYPE_STRING => {
props.set_one(P_STRING_MODE, Value::String(cs(V_STRING_SINGLE_LINE)));
props.set_one(P_STRING_FILE_EXISTS, Value::Int(1));
}
TYPE_CHOICE => {
// 维度 0:选项数由插件 SetN 决定。
props.define(P_CHOICE_OPTION, vec![]);
}
TYPE_CUSTOM => {
props.set_one(P_CUSTOM_INTERP, Value::Pointer(std::ptr::null_mut()));
}
TYPE_PAGE => {
props.define(P_PAGE_CHILD, vec![]);
}
TYPE_GROUP => {
props.set_one(P_GROUP_OPEN, Value::Int(1));
}
_ => {}
}
// 交互属性(HS addInteractParamPropsofxhParam.cpp:305-317
// group/page 除外)。
if ofx_type != TYPE_GROUP && ofx_type != TYPE_PAGE {
props.set_one(P_INTERACT_V1, Value::Pointer(std::ptr::null_mut()));
props.define(P_INTERACT_SIZE, vec![Value::Double(0.0), Value::Double(0.0)]);
props.set_one(P_INTERACT_ASPECT, Value::Double(1.0));
props.define(
P_INTERACT_MIN_SIZE,
vec![Value::Double(10.0), Value::Double(10.0)],
);
props.define(
P_INTERACT_PREF_SIZE,
vec![Value::Int(10), Value::Int(10)],
);
}
Self {
props,
name: uname,
ofx_type: utype,
default: type_default(ofx_type),
}
}
/// 值类别(变长参数分发用;无值类返回 None)。
pub(crate) fn kind(&self) -> Option<ParamKind> {
kind_of_type(&self.ofx_type).map(|(k, _)| k)
}
}
/// kOfxParamPropDefault 的属性值(HS ofxhParam.cpp:376-377)。
fn default_values(kind: ParamKind, dim: usize) -> Vec<Value> {
match kind {
ParamKind::Int | ParamKind::Int2 | ParamKind::Int3 | ParamKind::Bool | ParamKind::Choice => {
vec![Value::Int(0); dim.max(1)]
}
ParamKind::Double
| ParamKind::Double2
| ParamKind::Double3
| ParamKind::Rgb
| ParamKind::Rgba => vec![Value::Double(0.0); dim.max(1)],
ParamKind::Str | ParamKind::StrChoice => vec![Value::String(cs(""))],
}
}
/// 参数实例(createInstance 后由 [`ParamDef`] 实例化)。
/// `#[repr(C)]` + props 在偏移 0(句柄约定)。
#[repr(C)]
pub struct ParamInstance {
/// 实例级属性(当前值镜像等)。
pub props: PropertySet,
/// 对应定义。
pub def: ParamDef,
/// 当前值(节点桥与 OFX 侧的同步点)。
value: std::sync::Mutex<ParamValue>,
}
impl ParamInstance {
/// 从定义实例化(createInstance 路径调用)。实例 props 是定义
/// props 的深拷贝(HS: SetInstance::makeParam 的 descriptor 属性
/// 复制语义——插件在实例期读 label/min/max 等)。
pub fn from_def(def: ParamDef) -> Self {
let value = def.default.clone();
let props = def.props.clone();
Self {
props,
def,
value: std::sync::Mutex::new(value),
}
}
/// 读当前值。
pub fn get(&self) -> ParamValue {
self.value.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
/// 写当前值(OFX 语义:paramSetValue action 语义,不打节点桥)。
pub fn set_ofx(&self, value: ParamValue) {
*self.value.lock().unwrap_or_else(|e| e.into_inner()) = value;
}
/// 字符串值的内驻指针(param suite 的 get_string 用;指向实例
/// 存储,下次 set 前有效——克隆的 CString 指针会悬垂,不能给)。
pub(crate) fn string_ptr(&self) -> Option<*const std::ffi::c_char> {
let v = self.value.lock().unwrap_or_else(|e| e.into_inner());
match &*v {
ParamValue::String(s) | ParamValue::StrChoice(s) => Some(s.as_ptr()),
_ => None,
}
}
/// 从 oaknode 输入值写入(节点→插件方向)。类型映射表逐行对照
/// C++ 的 `valueconvert`/`paraminstance.h``node_get` 的镜像):
/// INT→Integer、FLOAT→Double、BOOL→Boolean、COMBO→Choice、
/// COLOR→RGB/RGBA、VEC2/VEC3→Double(2D/3D)/Integer(2D/3D)。
/// 维度不齐按 OFX 语义截断/补零(缺失元素按 0)。字符串族
/// OAKNODE_VALUE_STRING)的 POD 不携带数据——此路径不改值
/// (走 facade 的字符串 API,见 `include/plugin/instance.h`)。
/// 类型不匹配 → 忽略(保持现值;C++ `node_get` 失败时参数不回写)。
pub fn set_from_node(&self, node_value: &crate::bridge::node::Value) {
use crate::bridge::node::node_value_type as T;
let v = node_value;
let mapped: Option<ParamValue> = match self.def.ofx_type.as_str() {
TYPE_DOUBLE => (v.r#type == T::FLOAT).then(|| ParamValue::Double([v.f[0], 0.0, 0.0], 1)),
TYPE_DOUBLE2D => {
(v.r#type == T::VEC2).then(|| ParamValue::Double([v.f[0], v.f[1], 0.0], 2))
}
TYPE_DOUBLE3D => (v.r#type == T::VEC3)
.then(|| ParamValue::Double([v.f[0], v.f[1], v.f[2]], 3)),
TYPE_INTEGER => (v.r#type == T::INT).then(|| ParamValue::Int([v.num as i32, 0, 0], 1)),
TYPE_INTEGER2D | TYPE_INTEGER3D => {
let dim = if self.def.ofx_type == TYPE_INTEGER2D { 2 } else { 3 };
Some(ParamValue::Int(
[v.f[0] as i32, v.f[1] as i32, v.f[2] as i32],
dim,
))
}
TYPE_BOOLEAN => (v.r#type == T::BOOL).then(|| ParamValue::Bool(v.num != 0)),
TYPE_CHOICE => (v.r#type == T::COMBO).then(|| ParamValue::Choice(v.num as i32)),
TYPE_RGB => (v.r#type == T::COLOR)
.then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], 0.0], 3)),
TYPE_RGBA => (v.r#type == T::COLOR)
.then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], v.f[3]], 4)),
_ => None, // 字符串/无值类:POD 无数据,不改值
};
if let Some(pv) = mapped {
self.set_ofx(pv);
}
}
}
/// ParamValue → [`crate::bridge::node::Value`](插件→节点方向;
/// 字符串族经 [`crate::bridge::node::set_input_string_undoable`])。
/// 镜像 C++ `paraminstance.h` 的 `value_int`/`value_double`/
/// `value_vec`/`value_color` 构造:RGB 颜色补 alpha=1C++
/// `RGBInstance::set` 的 `value_color(r,g,b,1.0)`)。无值类与 Bytes
/// 无节点对应 → None。
pub(crate) fn to_node_value(v: &ParamValue) -> Option<crate::bridge::node::Value> {
use crate::bridge::node::node_value_type as T;
let mut out = crate::bridge::node::Value::default();
match v {
ParamValue::Double(d, 1) => {
out.r#type = T::FLOAT;
out.f = [d[0], 0.0, 0.0, 0.0];
}
ParamValue::Double(d, 2) => {
out.r#type = T::VEC2;
out.f = [d[0], d[1], 0.0, 0.0];
}
ParamValue::Double(d, 3) => {
out.r#type = T::VEC3;
out.f = [d[0], d[1], d[2], 0.0];
}
ParamValue::Double(d, _) => {
out.r#type = T::FLOAT;
out.f = [d[0], 0.0, 0.0, 0.0];
}
ParamValue::Int(a, 1) => {
out.r#type = T::INT;
out.num = a[0] as i64;
}
ParamValue::Int(a, 2) => {
out.r#type = T::VEC2;
out.f = [a[0] as f64, a[1] as f64, 0.0, 0.0];
}
ParamValue::Int(a, 3) => {
out.r#type = T::VEC3;
out.f = [a[0] as f64, a[1] as f64, a[2] as f64, 0.0];
}
ParamValue::Bool(b) => {
out.r#type = T::BOOL;
out.num = *b as i64;
}
ParamValue::Choice(c) => {
out.r#type = T::COMBO;
out.num = *c as i64;
}
ParamValue::Color(c, 3) => {
out.r#type = T::COLOR;
out.f = [c[0], c[1], c[2], 1.0];
}
ParamValue::Color(c, 4) => {
out.r#type = T::COLOR;
out.f = *c;
}
_ => return None,
}
Some(out)
}
/// 参数实例集(实例级)。
pub struct ParamSetInstance {
/// 全部参数(定义顺序稳定;Box 保证句柄地址稳定)。
pub params: Vec<Box<ParamInstance>>,
}
impl ParamSetInstance {
/// 按名查找。
pub fn find(&self, name: &str) -> Option<&ParamInstance> {
self.params.iter().find(|p| p.def.name == name).map(|b| b.as_ref())
}
/// 快照(内省 C ABI 与快照测试用)。
///
/// 声明原为 `Vec<(&str, &ParamValue)>`:值在 Mutex 内,无法返回
/// 活引用——改为拥有型 `(String, ParamValue)`(内省语义不变)。
pub fn snapshot(&self) -> Vec<(String, ParamValue)> {
self.params
.iter()
.map(|p| (p.def.name.clone(), p.get()))
.collect()
}
}
/// 插件 → 节点方向的回写入口(instanceChanged action 触发)。
/// 经 [`crate::bridge::node`] 定位绑定节点(身份注册表),再经
/// [`crate::bridge::undo`] 包成 undoable 修改。未绑定节点时 no-op。
///
/// 语义对照 C++ `paraminstance.h` 的 `set()` 路径(`detail::node_set`/
/// `node_set_at`):
/// - 只回写 [`ChangeReason::PluginEdited`](插件自改)。UserEdited/
/// TimeChanged 是宿主侧变更,值已由 [`ParamInstance::set_from_node`]
/// 同步,不重复写回;
/// - 字符串族经 `oaknode_node_set_input_string_undoable`
/// (POD 不携带字符串数据);
/// - 无值类(PushButton/Group/Page)与 Bytes 无节点对应 → no-op
/// - 编辑事务内([`crate::instance::Instance::in_edit`])并入 multi
/// 命令,否则单命令立即 redo 生效(C++ `submit_undo_command`)。
pub(crate) fn notify_instance_changed(
instance: &crate::instance::Instance,
param_name: &str,
reason: ChangeReason,
) {
use crate::bridge::{node, undo};
use std::sync::atomic::Ordering;
if !matches!(reason, ChangeReason::PluginEdited) {
return;
}
// 未绑定节点 → no-opC++!node_.ctx 时 set 只写本地值)。
let node_id = instance.node_identity.load(Ordering::Relaxed);
if node_id == 0 {
return;
}
// 身份查无(注册表无此项 / 桥符号缺失)→ no-op。
let node_handle = unsafe { node::node_from_identity(node_id) };
if node_handle.is_null() {
return;
}
let Some(param) = instance.params.find(param_name) else {
return;
};
let value = param.get();
let label = format!("Change {param_name}");
let Some(cname) = CString::new(param_name).ok() else {
return;
};
match to_node_value(&value) {
Some(nv) => {
let mut cmd = undo::CommandHandle::null();
let r =
unsafe { node::set_input_undoable(node_handle, cname.as_ptr(), &nv, &mut cmd) };
if r == 0 && !cmd.is_null() {
instance.submit_undo_command(cmd, &label);
}
}
None => match &value {
ParamValue::String(s) | ParamValue::StrChoice(s) => {
let Some(cval) = CString::new(s.to_bytes()).ok() else {
return;
};
let mut cmd = undo::CommandHandle::null();
let r = unsafe {
node::set_input_string_undoable(
node_handle,
cname.as_ptr(),
cval.as_ptr(),
&mut cmd,
)
};
if r == 0 && !cmd.is_null() {
instance.submit_undo_command(cmd, &label);
}
}
_ => {} // 无值类 / Bytes:无节点对应
},
}
}
/// instanceChanged 的原因(OFX kOfxChange*)。
#[derive(Clone, Copy, Debug)]
pub enum ChangeReason {
/// 用户编辑。
UserEdited,
/// 插件自改。
PluginEdited,
/// 时间变化。
TimeChanged,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::node::node_value_type as T;
fn param(ofx_type: &str) -> ParamInstance {
ParamInstance::from_def(ParamDef::new("p", ofx_type))
}
/// ParamValue → oaknode Value 的类型映射表(镜像 C++
/// paraminstance.h 的 value_* 构造)。
#[test]
fn to_node_value_mapping() {
use crate::bridge::node::Value;
let c = |t: i32| Value {
r#type: t,
num: 0,
den: 0,
f: [0.0; 4],
};
// Double 族。
let v = to_node_value(&ParamValue::Double([1.5, 0.0, 0.0], 1)).unwrap();
assert_eq!(v, Value::float(1.5));
let v = to_node_value(&ParamValue::Double([1.0, 2.0, 0.0], 2)).unwrap();
assert_eq!(v, Value::vec(&[1.0, 2.0]));
let v = to_node_value(&ParamValue::Double([1.0, 2.0, 3.0], 3)).unwrap();
assert_eq!(v, Value::vec(&[1.0, 2.0, 3.0]));
// Int 族(2D/3D 升格为 VEC2/VEC3 浮点载荷)。
let v = to_node_value(&ParamValue::Int([7, 0, 0], 1)).unwrap();
assert_eq!(v, Value::int(7));
let v = to_node_value(&ParamValue::Int([1, 2, 0], 2)).unwrap();
assert_eq!(v, Value::vec(&[1.0, 2.0]));
// Bool / Choice。
assert_eq!(to_node_value(&ParamValue::Bool(true)).unwrap(), Value::bool_(true));
assert_eq!(to_node_value(&ParamValue::Choice(3)).unwrap(), Value::combo(3));
// ColorRGB 补 alpha=1C++ oracle),RGBA 全透传。
let v = to_node_value(&ParamValue::Color([0.1, 0.2, 0.3, 0.0], 3)).unwrap();
assert_eq!(v, Value::color(0.1, 0.2, 0.3, 1.0));
let v = to_node_value(&ParamValue::Color([0.1, 0.2, 0.3, 0.9], 4)).unwrap();
assert_eq!(v, Value::color(0.1, 0.2, 0.3, 0.9));
// 无值类 / Bytes → None(无节点对应)。
assert_eq!(to_node_value(&ParamValue::PushButton), None);
assert_eq!(to_node_value(&ParamValue::Container), None);
assert_eq!(to_node_value(&ParamValue::Bytes(vec![1])), None);
// 字符串族走专用桥(POD 路径 None)。
assert_eq!(to_node_value(&ParamValue::String(cs(""))), None);
let _ = c(T::NONE); // 哨兵:类型常量可达
}
/// type_default 的无值类/字节族基线(paramDefine 拒绝前的兜底)。
#[test]
fn type_default_edge_cases() {
assert!(matches!(
type_default(TYPE_BYTES),
ParamValue::Bytes(_)
));
assert!(matches!(type_default(TYPE_CUSTOM), ParamValue::Bytes(_)));
assert!(matches!(type_default(TYPE_PUSHBUTTON), ParamValue::PushButton));
assert!(matches!(type_default(TYPE_GROUP), ParamValue::Container));
assert!(matches!(type_default(TYPE_PAGE), ParamValue::Container));
// 未知类型占位(paramDefine 已拒绝,此处兜底)。
assert!(matches!(type_default("OfxParamTypeBogus"), ParamValue::Container));
// 数值族的默认 0。
assert!(matches!(type_default(TYPE_INTEGER), ParamValue::Int([0, 0, 0], 1)));
assert!(matches!(
type_default(TYPE_DOUBLE2D),
ParamValue::Double([0.0, 0.0, 0.0], 2)
));
assert!(matches!(
type_default(TYPE_RGBA),
ParamValue::Color([0.0, 0.0, 0.0, 0.0], 4)
));
}
/// set_from_node 的维度截断/补零与字符串静默路径。
#[test]
fn set_from_node_dimension_rules() {
// Integer2D 缺第三维补零;浮点截断。
let p = param(TYPE_INTEGER2D);
p.set_from_node(&crate::bridge::node::Value::vec(&[1.9, 2.9]));
assert_eq!(p.get(), ParamValue::Int([1, 2, 0], 2));
// StrChoice 参数遇 STRING 类型:POD 无数据 → 不改值。
let p = param(TYPE_STRCHOICE);
p.set_from_node(&crate::bridge::node::Value::string());
assert!(matches!(p.get(), ParamValue::StrChoice(_)));
// Bytes/Custom 参数:任意节点值都不改(无映射)。
let p = param(TYPE_CUSTOM);
p.set_from_node(&crate::bridge::node::Value::float(3.0));
assert!(matches!(p.get(), ParamValue::Bytes(_)));
}
}
+86
View File
@@ -0,0 +1,86 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! 进度上报(Progress suite 的宿主侧)。
//!
//! 对应 C++ 的 `PluginProgressReporter`。进度/取消经 facade 注册的
//! 回调出 crateM9 的 facade 回调模式,不设全局状态)。
//! 取消是粘滞的:一旦回调答 false,本报告器的 [`is_cancelled`]
//! image effect suite 的 abort 查询)持续为真。
use std::ffi::c_int;
use std::sync::atomic::{AtomicBool, Ordering};
/// 进度回调:签名与 `include/plugin/instance.h` 的
/// `oakplugin_progress_fn` 逐字一致——`(progress, userdata)`
/// 返回非 0 表示应中止(骨架声明为 `(userdata, progress) -> bool`
/// 与头文件不符,以此为准)。
pub type ProgressFn = unsafe extern "C" fn(progress: f64, userdata: *mut std::ffi::c_void) -> c_int;
/// 进度上报器。render 路径持有一份,Progress suite 的
/// progressStart/Update/End 转发到这里。
pub struct ProgressReporter {
callback: Option<ProgressFn>,
userdata: usize,
/// 取消标志(粘滞;update 返回 false 时置位)。
cancelled: AtomicBool,
}
impl ProgressReporter {
/// 无回调(渲染静默进行)。
pub fn silent() -> Self {
Self {
callback: None,
userdata: 0,
cancelled: AtomicBool::new(false),
}
}
/// 带回调。
///
/// # Safety
/// `userdata` 的生命周期由注册方保证。
pub unsafe fn new(callback: ProgressFn, userdata: *mut std::ffi::c_void) -> Self {
Self {
callback: Some(callback),
// usize 存(裸指针破坏 Send 推导;值语义不变)。
userdata: userdata as usize,
cancelled: AtomicBool::new(false),
}
}
/// 报告进度;返回 false 表示应取消(映射
/// kOfxStatReplyNo/action 失败由调用点决定)。
pub fn update(&self, progress: f64) -> bool {
match self.callback {
Some(cb) => {
// 头文件契约:非 0 = 中止。
let abort = unsafe { cb(progress, self.userdata as *mut std::ffi::c_void) } != 0;
if abort {
self.cancelled.store(true, Ordering::Relaxed);
}
!abort
}
None => true,
}
}
/// 本次渲染是否已被取消(image effect suite 的 abort 透传)。
pub(crate) fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Relaxed)
}
}
+172
View File
@@ -0,0 +1,172 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! OFX 属性集(Property Suite 的宿主侧存储)。
//!
//! 每个 OFX 对象(host/plugin/descriptor/instance/clip/image/param
//! 都挂一个 [`PropertySet`]。属性是多维数组,按类型存取。
//! 参照:HS: ofxhProperty.cppGetSuite 的读写语义:越界返回
//! kOfxStatErrBadIndex,类型不符返回 kOfxStatErrBadHandle)。
use std::sync::Mutex;
use crate::error::{Error, Result};
/// 属性值(单元素)。字符串以 OFX 的 `char*` 语义持有(host 拥有,
/// 插件借用)。
#[derive(Clone, Debug)]
pub enum Value {
/// 32 位整型。
Int(i32),
/// 双精度。
Double(f64),
/// UTF-8 字符串(OFX 侧为 NUL 结尾 char*)。
String(std::ffi::CString),
/// 不透明指针(如 OfxImageEffectHandle 互指)。
Pointer(*mut std::ffi::c_void),
}
// 裸指针默认禁 Send/Sync;但属性集语义把指针当作**不透明令牌**:
// 跨线程只搬运/比较值,解引用永远是 suite 层的 unsafe 责任(OFX
// multithread suite 本就要求插件线程可读写宿主图像缓冲——指针的
// 跨线程传递是规范语义,而非逃逸)。Mutex 包裹后属性集整体可共享。
// 与 Rust 安全模型不冲突:安全代码只能拿到 `&Value`,无法解引用指针。
unsafe impl Send for Value {}
unsafe impl Sync for Value {}
/// 一个属性:名字 + 多维值数组。
#[derive(Clone, Debug)]
pub struct Property {
/// OFX 属性名(kOfxProp*;拥有型——协商期的动态名
/// "OfxImageClipPropComponents_<clip>" 需要,`&'static str`
/// 无法表达)。
pub name: String,
/// 元素数组;维度 = len。
pub values: Vec<Value>,
}
/// 属性集。线程安全(内部 Mutex);OFX 对象的 `*Handle` 即指向它的
/// 包装。
pub struct PropertySet {
props: Mutex<Vec<Property>>,
}
/// 深拷贝(持锁克隆内部数组;createInstance 时描述符属性 → 实例
/// 属性需要)。
impl Clone for PropertySet {
fn clone(&self) -> Self {
let props = lock(&self.props);
Self {
props: Mutex::new(props.clone()),
}
}
}
impl PropertySet {
/// 空集。
pub fn new() -> Self {
Self {
props: Mutex::new(Vec::new()),
}
}
/// 定义(或整体替换)一个属性。已存在同名属性时替换其值数组。
///
/// 对应 C++ `Set::addProperty` 的替换语义(HS: ofxhPropertySuite.cpp:462
/// 与 `PropertyTemplate::setValueN` 的整体写数组语义;属性不存在时
/// 新建(维度和值都取自 `values`)。
pub fn define(&self, name: &str, values: Vec<Value>) {
let mut props = lock(&self.props);
if let Some(p) = props.iter_mut().find(|p| p.name == name) {
p.values = values;
} else {
props.push(Property {
name: name.to_string(),
values,
});
}
}
/// 单元素便捷定义。
pub fn set_one(&self, name: &str, value: Value) {
self.define(name, vec![value]);
}
/// 读取第 `index` 个元素;属性不存在或越界返回 `None`。
///
/// 越界对应 C++ 的 kOfxStatErrBadIndex
/// HS: ofxhPropertySuite.cpp:257 `getValueRaw`),此处以 Option 表达。
pub fn get(&self, name: &str, index: usize) -> Option<Value> {
let props = lock(&self.props);
props
.iter()
.find(|p| p.name == name)
.and_then(|p| p.values.get(index).cloned())
}
/// 覆盖第 `index` 个元素的值(不改变维度);失败返回
/// [`crate::error::Error::NotFound`]。
///
/// 维度固定是刻意为之:与 C++ `setValue` 的自动扩容不同,这里
/// 扩容只能经 [`PropertySet::define`] 显式进行(维度语义由定义方
/// 掌控,避免插件意外撑大数组)。
pub fn set_at(&self, name: &str, index: usize, value: Value) -> Result<()> {
let mut props = lock(&self.props);
let p = props
.iter_mut()
.find(|p| p.name == name)
.ok_or(Error::NotFound)?;
let slot = p.values.get_mut(index).ok_or(Error::NotFound)?;
*slot = value;
Ok(())
}
/// 维度(属性不存在为 0)。
pub fn dimension(&self, name: &str) -> usize {
lock(&self.props)
.iter()
.find(|p| p.name == name)
.map_or(0, |p| p.values.len())
}
/// 删除属性;不存在为 no-op。
pub fn remove(&self, name: &str) {
lock(&self.props).retain(|p| p.name != name);
}
/// 遍历快照(dump/快照测试用)。
pub fn snapshot(&self) -> Vec<Property> {
lock(&self.props).clone()
}
/// suite 层专用:持锁访问原始存储。
///
/// 属性 suite 需要"先类型检查再按索引读写、返回内驻字符串指针"
/// 等跨多次读写的一致性语义(逐条对照 HS: ofxhProperty.cpp),
/// 公开 API 无法在不重复加锁的前提下表达;此入口把 `Vec<Property>`
/// 在单一临界区内交给 suite 实现。仅本 crate 可见。
pub(crate) fn with_locked<R>(&self, f: impl FnOnce(&mut Vec<Property>) -> R) -> R {
let mut props = lock(&self.props);
f(&mut props)
}
}
/// 取锁。毒锁(本 crate 代码在持锁时 panic)时接管其内部状态继续
/// 使用——属性集的数据本身总是完好的,宁可继续也不让一次 panic
/// 级联成后续所有 FFI 调用失败。
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(|e| e.into_inner())
}
+435
View File
@@ -0,0 +1,435 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! render 驱动:`src/render/src/plugin/pluginrenderer.cpp`
//! `PluginRenderer::render_plugin`1857 行 C++ 的一部分)的渲染流程
//! 语义收编(M11 §4)。
//!
//! 目标:oakrender 的 PluginJob 退化为一次 C ABI 调用(
//! [`crate::ffi::oakplugin_instance_render_job`]),本模块承载全部
//! OFX 宿主渲染流程。逐段对照的 C++ 行号已注释。
//!
//! ## 流程(render_frame,对应 render_plugin
//!
//! 1. 实例锁(pluginrenderer.cpp:1436-1444+ 取消检查;
//! 2. use_opengl 决策(pluginrenderer.cpp:1446-1457):插件描述符
//! kOfxImageEffectPropOpenGLRenderSupported ∈ {true, needed} 且
//! 渲染器是 OpenGL 且目标纹理有 GL id
//! 3. 输入纹理收集(pluginrenderer.cpp:1518-1552):effect_input_id
//! 匹配 → job.src;否则按 clip 名的纹理表;SimpleSource 回退;
//! 4. getClipPreferencespluginrenderer.cpp:1554-1594);
//! 5. RoI/RoD 设定(pluginrenderer.cpp:1596-1607):region_of_interest
//! = 目标尺寸(规范坐标);输出 clip 的 RoD 与输出纹理挂接;
//! 6. 输入 clip 的 RoD 与格式(pluginrenderer.cpp:1627-1665Phase 2
//! 全链路 F32 → 无转换);
//! 7. getRegionOfInterestpluginrenderer.cpp:1666-1680):RoI 为
//! 建议值,任何失败按默认整帧继续(C++ 对 BadHandle 即如此;
//! Phase 2 其余状态同样继续——RoI 仅影响上游渲染范围,本管线
//! 输入由 oakrender 整帧提供);
//! 8. 输出 clip 格式(pluginrenderer.cpp:1686-1697);
//! 9. **isIdentity 短路**ofxRendering "Identity Effects"):
//! isIdentity 命中 → 直接把所引输入 clip 在透传时间的帧拷入
//! 输出(不调 render action);
//! 10. 参数覆盖(pluginrenderer.cpp:132-290 apply_param_overrides);
//! 11. render actionCPU 路径经 [`crate::instance::Instance::render`]
//! (输出装配:图像 → 目标纹理帧,行跨度感知);GL 路径经
//! [`crate::instance::Instance::render_gl`](插件直接画进已附着
//! 的输出纹理,无 CPU 回读)。
//!
//! ## begin/end 序列括号
//!
//! ofxRendering 文档:"All calls to the render action are bracketed by
//! a pair of begin/end sequence render actions"。oakrender 对同一实例
//! 的一批帧先 [`begin_sequence`] 后 [`end_sequence`],中间逐帧
//! [`render_frame`]。
use crate::bridge::render::{self, FrameHandle, RendererHandle, TextureHandle};
use crate::image::Image;
use crate::instance::{Instance, OfxRangeD, OfxRectD, RenderScale};
use crate::property::Value;
/// 一帧渲染任务的输入(oakrender PluginJob 的 C ABI 载体;
/// [`crate::ffi::OakPluginJob`] 的 Rust 侧视图)。
pub struct RenderJob {
/// 帧时间(秒)。
pub time: f64,
/// 目标纹理(输出;oakrender 侧创建并经句柄传入)。
pub dst: TextureHandle,
/// 主输入纹理(effect_input_id / SimpleSource;可空)。
pub src: TextureHandle,
/// effect 输入 clip 名(job.src 的落点;C++ `node->get_effect_input_id()`)。
pub effect_input_id: Option<String>,
/// 其余输入 clip 的纹理表(clip 名 → 纹理)。
pub inputs: Vec<(String, TextureHandle)>,
/// 参数覆盖(参数名 → oaknode_value POD;对应 NodeValueRow)。
pub values: Vec<(String, crate::ffi::OakNodeValue)>,
/// GL 渲染器(None → CPU 路径;Some → 视 use_opengl 决策)。
pub renderer: Option<RendererHandle>,
/// 渲染前是否清空目标(信息性——C++ render_plugin 亦不处理,
/// 由上层渲染器负责;见插件渲染器注释)。
pub clear_destination: bool,
/// 交互式渲染标记(信息性;Phase 2 的 render in args 恒 0,见
/// [`crate::instance::Instance::render`])。
pub interactive: bool,
}
impl Default for RenderJob {
fn default() -> Self {
Self {
time: 0.0,
dst: TextureHandle::null(),
src: TextureHandle::null(),
effect_input_id: None,
inputs: Vec::new(),
values: Vec::new(),
renderer: None,
clear_destination: false,
interactive: false,
}
}
}
/// beginSequenceRender 括号(ofxRendering"bracketed by a pair of
/// begin/end sequence render actions"ofxGPURender.h 的 GL 模式下该
/// action 的 in args 也带 kOfxImageEffectPropOpenGLEnabled)。
/// `gl` 非空时按 GL 模式调用。
pub fn begin_sequence(
inst: &Instance,
range: OfxRangeD,
gl: Option<RendererHandle>,
) -> crate::error::Result<()> {
if gl.is_some() {
inst.begin_sequence_render_gl(range)
} else {
inst.begin_sequence_render(range)
}
}
/// endSequenceRender 括号(与 [`begin_sequence`] 配对)。
pub fn end_sequence(
inst: &Instance,
range: OfxRangeD,
gl: Option<RendererHandle>,
) -> crate::error::Result<()> {
if gl.is_some() {
inst.end_sequence_render_gl(range)
} else {
inst.end_sequence_render(range)
}
}
/// 渲染一帧(`render_plugin` 的 Rust 移植;逐段行号对照见模块文档)。
///
/// 返回各输入 clip 的 RoIclip 名 → 矩形;Phase 2 供测试断言,
/// 宿主不据此裁剪输入——输入由 oakrender 整帧提供,与 C++ 渲染器
/// 行为一致)。
pub fn render_frame(
inst: &Instance,
job: &RenderJob,
) -> crate::error::Result<Vec<(String, OfxRectD)>> {
use crate::error::Error;
// 1. 实例锁(pluginrenderer.cpp:1436-1444OlivePluginInstance 非
// 线程安全,并发 setInputTexture/renderAction 会毁内部状态)。
let _lock = inst.render_lock.lock().unwrap_or_else(|e| e.into_inner());
// 取消检查必须在任何插件调用之前(shutdown 后入口已卸载)。
if inst.cancel.load(std::sync::atomic::Ordering::Relaxed) {
return Err(Error::Failed("已取消".into()));
}
// 2. use_openglpluginrenderer.cpp:1446-1457):插件声明 GL 支持
// 且渲染器是 OpenGL 且目标纹理有 GL id 且像素深度协商可行
// (管线 F32 满足插件 kOfxOpenGLPropPixelDepth 声明)。
let use_opengl = match job.renderer {
Some(r) if unsafe { render::renderer_is_open_gl(r) } == 1 => {
let plugin_gl = plugin_supports_opengl(inst);
let depth_ok =
crate::suites::gl_render::pick_gl_pixel_depth(&inst.plugin.descriptor.props).is_some();
let dst_id = unsafe { render::texture_id(job.dst) };
plugin_gl && depth_ok && dst_id != 0
}
_ => false,
};
// 目标帧与参数(F32 校验;输出装配的依据)。
let (dst_frame, dst_params, w, h) = read_dst(job.dst)?;
let mut dst_frame = dst_frame;
let par = pixel_aspect(&dst_params);
// 规范坐标的 RoI/RoDpluginrenderer.cpp:1595-1603x2 = 宽 × PAR)。
let region_of_interest = OfxRectD {
x1: 0.0,
y1: 0.0,
x2: w * par,
y2: h,
};
// 3. 输入纹理收集(pluginrenderer.cpp:1518-1552)。
for clip in &inst.clips {
if clip.name == "Output" {
continue;
}
let tex = pick_input(&clip.name, job);
if usable(tex) {
clip.set_input_texture(tex, job.time);
}
}
// 4. getClipPreferencespluginrenderer.cpp:1554-1594)。
let prefs = inst.get_clip_preferences()?;
let components = match prefs.output_components.as_str() {
"OfxImageComponentRGBA" => crate::image::Components::Rgba,
"OfxImageComponentRGB" => crate::image::Components::Rgb,
"OfxImageComponentAlpha" => crate::image::Components::Alpha,
_ => return Err(Error::Failed("协商分量未知".into())),
};
if prefs.output_bit_depth != "OfxBitDepthFloat" {
return Err(Error::Failed("Phase 2 仅支持 F32 输出".into()));
}
// 5. 输出 clipRoD + 输出纹理挂接(pluginrenderer.cpp:1603-1606)。
let output_clip = inst
.clips
.iter()
.find(|c| c.name == "Output")
.ok_or_else(|| Error::Failed("实例无 Output clip".into()))?;
output_clip.set_region_of_definition(region_of_interest, job.time);
output_clip.set_output_texture(job.dst, job.time);
// 6. 输入 clipRoD 与格式(pluginrenderer.cpp:1627-1665Phase 2
// 全链路 F32 → 格式选择恒等,无转换路径)。
for clip in &inst.clips {
if clip.name == "Output" {
continue;
}
if usable(pick_input(&clip.name, job)) {
clip.set_region_of_definition(region_of_interest, job.time);
clip.set_video_params(render::PIXEL_FORMAT_F32, 4);
}
}
// 7. getRegionOfInterestpluginrenderer.cpp:1666-1680)。RoI 为
// 建议值:失败按默认整帧继续(C++ 对 BadHandle 如此;本管线
// 输入整帧提供,RoI 只作记录)。
let rois = inst
.get_regions_of_interest(job.time, RenderScale { x: 1.0, y: 1.0 }, region_of_interest)
.unwrap_or_else(|_| {
inst.clips
.iter()
.map(|_| region_of_interest)
.collect()
});
// 8. 输出 clip 格式(pluginrenderer.cpp:1686-1697)。
output_clip.set_video_params(render::PIXEL_FORMAT_F32, components.channel_count() as i32);
// 渲染窗口(像素坐标;pluginrenderer.cpp:1699-1704)。
let render_window = OfxRectD { x1: 0.0, y1: 0.0, x2: w, y2: h };
// 9. isIdentity 短路(ofxRendering "Identity Effects"):插件声明
// 本帧等价于某输入 clip → 直接透传该 clip 在透传时间的帧。
if let Some((t, clip_name)) = inst.is_identity(job.time)? {
passthrough(inst, &clip_name, t, job.dst)?;
unsafe { render::frame_free(&mut (dst_frame)) };
return Ok(zip_rois(inst, &rois));
}
// 10. 参数覆盖(pluginrenderer.cpp:1729-1731 + 132-290)。
apply_param_overrides(inst, &job.values);
// 11. render action。
if !use_opengl {
let output = std::sync::Arc::new(Image::allocate(
crate::image::BitDepth::Float,
components,
OfxRectD { x1: 0.0, y1: 0.0, x2: w, y2: h },
));
inst.render(job.time, RenderScale { x: 1.0, y: 1.0 }, render_window, output.clone())?;
// 输出装配(pluginrenderer.cpp:1762-1834 的 CPU 路径)。
write_output_frame(job.dst, &output)?;
} else {
// GL 路径:插件直接画进已附着的输出纹理(
// pluginrenderer.cpp:1784-1834 的 GL 分支);无 CPU 回读。
inst.render_gl(job.time, RenderScale { x: 1.0, y: 1.0 }, render_window, job.renderer.unwrap(), job.dst)?;
}
unsafe { render::frame_free(&mut (dst_frame)) };
Ok(zip_rois(inst, &rois))
}
/// 把输入 clip 名与 RoI 列表配对(与 `clips` 顺序一致)。
fn zip_rois(inst: &Instance, rois: &[OfxRectD]) -> Vec<(String, OfxRectD)> {
inst.clips
.iter()
.enumerate()
.filter(|(_, c)| c.name != "Output")
.map(|(i, c)| (c.name.clone(), rois.get(i).copied().unwrap_or_default()))
.collect()
}
/// 读目标纹理的帧与参数(F32 校验)。返回 (帧句柄, 参数, 宽, 高)。
fn read_dst(dst: TextureHandle) -> crate::error::Result<(FrameHandle, render::VideoParams, f64, f64)> {
use crate::error::Error;
let mut frame = FrameHandle::null();
if unsafe { render::texture_get_frame(dst, &mut frame) } != 0 || frame.is_null() {
return Err(Error::Failed("输出纹理无 CPU 帧".into()));
}
let mut params = render::VideoParams::default();
if unsafe { render::frame_get_params(frame, &mut params) } != 0 {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Failed("输出帧无参数".into()));
}
if params.format != render::PIXEL_FORMAT_F32 {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Failed(format!(
"输出帧格式 {} 非 F32Phase 2 约束)",
params.format
)));
}
let (w, h) = (params.width as f64, params.height as f64);
if w <= 0.0 || h <= 0.0 {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Invalid);
}
Ok((frame, params, w, h))
}
/// 目标参数的像素比(缺失 1.0)。
fn pixel_aspect(params: &render::VideoParams) -> f64 {
if params.pixel_aspect_den != 0 {
params.pixel_aspect_num as f64 / params.pixel_aspect_den as f64
} else {
1.0
}
}
/// 插件描述符是否声明 GL 渲染支持(ofxGPURender.h:397-408
/// "true"/"needed")。
fn plugin_supports_opengl(inst: &Instance) -> bool {
inst.plugin
.descriptor
.props
.get(crate::host::PROP_GL_RENDER_SUPPORTED, 0)
.map(|v| match v {
Value::String(s) => {
let s = s.to_string_lossy();
s == "true" || s == "needed"
}
_ => false,
})
.unwrap_or(false)
}
/// 输入纹理是否可用(非空且非占位;pluginrenderer.cpp:1504-1513 的
/// is_usable_input——Phase 2 只看非 dummy,帧/Renderer 由 oakrender
/// 保证)。
fn usable(tex: TextureHandle) -> bool {
!tex.is_null() && unsafe { render::texture_is_dummy(tex) } == 0
}
/// 按 C++ pluginrenderer.cpp:1527-1543 的规则选输入纹理。
fn pick_input(clip_name: &str, job: &RenderJob) -> TextureHandle {
if job.effect_input_id.as_deref() == Some(clip_name) && !job.src.is_null() {
return job.src;
}
for (name, tex) in &job.inputs {
if name == clip_name {
return *tex;
}
}
// SimpleSource 回退(pluginrenderer.cpp:1534-1543
// kOfxImageEffectSimpleSourceClipName 取 k_texture_input,再回退
// job.src)。
if clip_name == "Source" && !job.src.is_null() {
return job.src;
}
TextureHandle::null()
}
/// isIdentity 透传:把所引输入 clip 在 `t` 的帧拷入输出(CPU 拷贝;
/// 目标帧 F32 校验)。
fn passthrough(
inst: &Instance,
clip_name: &str,
t: f64,
dst: TextureHandle,
) -> crate::error::Result<()> {
use crate::error::Error;
let clip = inst
.clips
.iter()
.find(|c| c.name == clip_name)
.ok_or_else(|| Error::Failed(format!("isIdentity 引用未知 clip {clip_name}")))?;
let image = clip.fetch_image(t, RenderScale { x: 1.0, y: 1.0 }, None)?;
write_output_frame(dst, &image)
}
/// 参数覆盖(pluginrenderer.cpp:132-290 `apply_param_overrides` 的
/// Rust 移植):把每帧的节点值注入实例参数。字符串族(String/
/// StrChoice)经专用 C ABIset_param_string),不在此表的 oaknode
/// POD 表达范围内 → 跳过(与 C++ 的 k_file/k_text/k_font/k_str_combo
/// 走专用桥一致)。
fn apply_param_overrides(inst: &Instance, values: &[(String, crate::ffi::OakNodeValue)]) {
for (key, v) in values {
let Some(p) = inst.params.find(key) else { continue };
let Some(pv) = crate::ffi::node_value_to_param(v, &p.def.ofx_type) else {
continue;
};
p.set_ofx(pv);
}
}
/// 把 CPU 图像写入目标纹理的帧(行优先、行跨度感知;F32 校验)。
/// Phase 2 输出装配的公共落点(CPU render 路径与 isIdentity 透传
/// 共用)。
pub(crate) fn write_output_frame(dst: TextureHandle, image: &Image) -> crate::error::Result<()> {
use crate::error::Error;
let mut frame = FrameHandle::null();
if unsafe { render::texture_get_frame(dst, &mut frame) } != 0 || frame.is_null() {
return Err(Error::Failed("输出纹理无 CPU 帧".into()));
}
let mut params = render::VideoParams::default();
if unsafe { render::frame_get_params(frame, &mut params) } != 0 {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Failed("输出帧无参数".into()));
}
if params.format != render::PIXEL_FORMAT_F32 {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Failed("输出帧格式非 F32(Phase 2 约束)".into()));
}
let (w, h) = (params.width as usize, params.height as usize);
let tight = w * image.components().channel_count() * 4;
if tight != image.row_bytes() || tight * h != image.pixels().len() {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Failed("图像尺寸与输出帧不一致".into()));
}
let dst_ptr = unsafe { render::frame_data(frame) };
if dst_ptr.is_null() {
unsafe { render::frame_free(&mut frame) };
return Err(Error::Failed("输出帧无数据".into()));
}
let row = unsafe { render::frame_linesize_bytes(frame) } as usize;
let row = if row > 0 { row } else { tight };
let dst_bytes =
unsafe { std::slice::from_raw_parts_mut(dst_ptr as *mut u8, row * h) };
let pixels = image.pixels();
for y in 0..h {
let d = y * row;
let s = y * tight;
dst_bytes[d..d + tight].copy_from_slice(&pixels[s..s + tight]);
}
unsafe { render::frame_free(&mut frame) };
Ok(())
}
+503
View File
@@ -0,0 +1,503 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! OfxImageEffectOpenGLRenderSuiteV1M11 第 2 期):clipLoadTexture /
//! clipFreeTexture / flushResources。
//!
//! 语义对照 ofxGPURender.hvendoredOpenGL Render Suite 一节)与
//! HostSupport 插件侧实现(HS: ofxhImageEffect.cpp:2296-2367):
//!
//! - `clipLoadTexture(clip, time, format, region, *texture)`:把 clip
//! 在 `time` 的图像加载为 GL 纹理。纹理句柄是**属性集**(标签 0),
//! 含 ofxGPURender.h 规定的 12 个属性(OpenGLTextureIndex/
//! OpenGLTextureTarget/PixelDepth/Components/PreMultiplication/
//! RenderScale/PixelAspectRatio/Bounds/RegionOfDefinition/RowBytes/
//! Field/UniqueIdentifier);宿主侧存强引用([`LIVE_TEXTURES`]),
//! clipFreeTexture 摘除即释放(对应 HS 的 get/release 配对)。
//! - Output clip`format` 忽略,宿主返回已附着的输出纹理句柄——
//! 绑定渲染目标的动作(ofxGPURender.h "the host must bind the
//! resulting texture as the current color buffer")由调用方约定:
//! GL render 驱动的 C ABI 契约要求 oakrender 在进入 render_job 前
//! 已把输出纹理附着为渲染器输出目标(等价 C++ 的
//! `PluginRenderer::attach_output_texture`)。clipFreeTexture 对
//! Output 只释放句柄、不删纹理(宿主还要读它)。
//! - 输入纹理经 [`crate::bridge::render`] 在渲染器上创建(CPU 帧 →
//! GL 上传)。纹理格式:全链路 F32 约束下,像素深度按 clip 协商
//! 结果(恒 F32);`format` 参数(kOfxImageEffectGLFormat*)若
//! 请求的分量与协商分量不符,Phase 2 不做转换 → Failed(规范要求
//! "host ensures it gives the requested format"——宁可显式失败也
//! 不静默给错格式)。ofxGPURender.h 注明"宿主无需按 Clip
//! Preferences 把图像重映射到插件请求的位深",插件以纹理句柄的
//! PixelDepth/Components 为准。
//! - `flushResources`:宿主在 render 之间不缓存 GPU 资源(纹理随
//! clipFreeTexture 立即释放)→ 无可释放 → kOfxStatReplyDefault
//! (规范:"nothing the host could do")。
//! - GL 上下文规则(ofxGPURender.h "OpenGL Current Context"):宿主
//! 只在 Render/BeginSequenceRender/EndSequenceRender/Attach/Detach
//! 期间要求上下文 current;本实现的约定是调用方(oakrender
//! PluginJob 路径)在调用前置好上下文,本 suite 经
//! [`crate::suites::gl_ctx`] TLS 取渲染器句柄。
use std::collections::HashMap;
use std::ffi::{c_char, c_double, c_int, c_void, CStr, CString};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Mutex;
use crate::clip::ClipInstance;
use crate::image::Image;
use crate::property::{PropertySet, Value};
use crate::suites::{status, tag};
// ---- GL 常量(ofxGPURender.hGLFormat 字符串为 ofxOpenGLRender.h
// OpenFX 1.4 的规范名,vendored 头文件是 stub 未收录,按规范定义)----
/// kOfxImageEffectPropOpenGLTextureIndexofxGPURender.h:135)。
pub(crate) const GL_TEXTURE_INDEX: &str = "OfxImageEffectPropOpenGLTextureIndex";
/// kOfxImageEffectPropOpenGLTextureTargetofxGPURender.h:152)。
pub(crate) const GL_TEXTURE_TARGET: &str = "OfxImageEffectPropOpenGLTextureTarget";
/// kOfxImageEffectGLFormatRGBAofxOpenGLRender.h 1.4)。
pub(crate) const GL_FORMAT_RGBA: &str = "OfxImageEffectGLFormatRGBA";
/// kOfxImageEffectGLFormatRGB。
pub(crate) const GL_FORMAT_RGB: &str = "OfxImageEffectGLFormatRGB";
/// kOfxImageEffectGLFormatAlpha。
pub(crate) const GL_FORMAT_ALPHA: &str = "OfxImageEffectGLFormatAlpha";
/// kOfxImageEffectGLFormatLuminance。
pub(crate) const GL_FORMAT_LUMINANCE: &str = "OfxImageEffectGLFormatLuminance";
/// kOfxImageEffectGLFormatLuminanceAlpha。
pub(crate) const GL_FORMAT_LUMINANCE_ALPHA: &str = "OfxImageEffectGLFormatLuminanceAlpha";
/// GL_TEXTURE_2D 的 GLenum 值(0x0DE1;纹理句柄的 OpenGLTextureTarget
/// 属性。GL 规范值,非 OFX 宏)。
pub(crate) const GL_TEXTURE_2D: i32 = 0x0DE1;
/// kOfxImageEffectPropPreMultiplication 的非预乘默认值。
pub(crate) const PREMULT_NONE: &str = "OfxImagePreMultipliedNone";
/// GL 像素深度协商(ofxGPURender.h:65-89 kOfxOpenGLPropPixelDepth):
/// 插件描述符声明的 GL 渲染支持位深列表(可选)。
///
/// 返回 Some(实际位深) 表示 GL 模式可行,None 表示管线无法满足
/// 插件声明 → 宿主应回退 CPU 渲染(ofxGPURender.h "the host will
/// try to provide buffers/textures in one of the supported formats"
/// Phase 2 全链路 F32,无法提供其他位深):
/// - 列表缺失/为空 → Some(Float)(宿主自选,规范默认);
/// - 列表含 Float → Some(Float)
/// - 列表存在且不含 Float → None(管线约束;GL 模式不可行)。
pub(crate) fn pick_gl_pixel_depth(plugin_props: &crate::property::PropertySet) -> Option<&'static str> {
use crate::property::Value;
let dim = plugin_props.dimension(crate::host::PROP_GL_PIXEL_DEPTH);
if dim == 0 {
return Some("OfxBitDepthFloat");
}
for i in 0..dim {
if let Some(Value::String(s)) = plugin_props.get(crate::host::PROP_GL_PIXEL_DEPTH, i) {
if s.to_string_lossy() == "OfxBitDepthFloat" {
return Some("OfxBitDepthFloat");
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::property::{PropertySet, Value};
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
/// GL 像素深度协商矩阵(ofxGPURender.h kOfxOpenGLPropPixelDepth):
/// 未声明/含 Float → 管线 F32 可行;声明且不含 Float → None
/// (GL 模式不可行,回退 CPU)。
#[test]
fn pick_gl_pixel_depth_matrix() {
let props = PropertySet::new();
assert_eq!(pick_gl_pixel_depth(&props), Some("OfxBitDepthFloat"));
let props2 = PropertySet::new();
props2.define(
crate::host::PROP_GL_PIXEL_DEPTH,
vec![
Value::String(cs("OfxBitDepthHalf")),
Value::String(cs("OfxBitDepthByte")),
],
);
assert_eq!(pick_gl_pixel_depth(&props2), None);
let props3 = PropertySet::new();
props3.define(
crate::host::PROP_GL_PIXEL_DEPTH,
vec![
Value::String(cs("OfxBitDepthByte")),
Value::String(cs("OfxBitDepthFloat")),
],
);
assert_eq!(pick_gl_pixel_depth(&props3), Some("OfxBitDepthFloat"));
let props4 = PropertySet::new();
props4.define(
crate::host::PROP_GL_PIXEL_DEPTH,
vec![Value::String(cs("OfxBitDepthFloat"))],
);
assert_eq!(pick_gl_pixel_depth(&props4), Some("OfxBitDepthFloat"));
}
}
// ---- 存活纹理表 -----------------------------------------------------------
/// 存活 GL 纹理表:clipLoadTexture 产出(props 地址 → 属性集 +
/// 纹理强引用 + 是否输出 clip);clipFreeTexture 摘除即释放——对应
/// HS 的 get/release 配对(HS: ofxhImageEffect.cpp:2336-2351)。
///
/// 属性集必须**装箱**(Box 稳定堆地址):纹理句柄指向它,函数返回后
/// 必须仍存活;栈上临时变量会悬垂(phase-2 实现初版的 bug)。
static LIVE_TEXTURES: std::sync::LazyLock<
Mutex<HashMap<usize, (Box<PropertySet>, crate::bridge::render::TextureHandle, bool)>>,
> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
/// 登记纹理(clipLoadTexture 内部;`props` 装箱后取地址为句柄基址)。
fn register(props: Box<PropertySet>, texture: crate::bridge::render::TextureHandle, is_output: bool) -> usize {
let addr = &*props as *const PropertySet as usize;
LIVE_TEXTURES
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(addr, (props, texture, is_output));
addr
}
/// 释放全部残留输入纹理(GL render action 返回后的安全网:规范要求
/// 插件在 action 返回前 clipFreeTexture 全部句柄;遗漏的输入纹理在
/// 此释放,输出纹理保留——宿主还要读它)。
pub(crate) fn purge_leftovers() {
let leftovers: Vec<(crate::bridge::render::TextureHandle, bool)> = {
let mut live = LIVE_TEXTURES.lock().unwrap_or_else(|e| e.into_inner());
live.drain().map(|(_k, (_p, t, o))| (t, o)).collect()
};
for (texture, is_output) in leftovers {
if !is_output && !texture.is_null() {
let mut t = texture;
unsafe { crate::bridge::render::texture_free(&mut t) };
}
}
}
/// 公共入口模板:panic 兜底。
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
catch_unwind(AssertUnwindSafe(f))
.map_or_else(|_| status::FAILED, |r| r.map_or_else(|c| c, |()| status::OK))
}
/// clip 句柄解析(实例期)。
fn resolve_clip(handle: *mut c_void) -> Result<&'static ClipInstance, c_int> {
if handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe {
match tag::kind(handle) {
tag::CLIP => Ok(&*(tag::strip(handle) as *const ClipInstance)),
_ => Err(status::ERR_BAD_HANDLE),
}
}
}
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
/// 把矩形写成 Int×4 属性(Bounds/ROD 用;OFX 图像属性是像素坐标)。
fn rect_props(props: &PropertySet, name: &str, rect: crate::instance::OfxRectD) {
let b = |v: f64| Value::Int(v.round() as i32);
props.define(
name,
vec![b(rect.x1), b(rect.y1), b(rect.x2), b(rect.y2)],
);
}
/// 从 clip 属性读字符串(缺失返回默认)。
fn clip_string(clip: &ClipInstance, name: &str, default: &str) -> String {
clip.props
.get(name, 0)
.map(|v| match v {
Value::String(s) => s.to_string_lossy().into_owned(),
_ => default.to_string(),
})
.unwrap_or_else(|| default.to_string())
}
/// 构造纹理属性集(ofxGPURender.h 规定的属性;输入与输出共用,
/// 只是数据来源不同)。
#[allow(clippy::too_many_arguments)]
fn make_texture_props(
texture: crate::bridge::render::TextureHandle,
width: f64,
height: f64,
components: crate::image::Components,
depth: &str,
premult: &str,
par: f64,
scale: crate::instance::RenderScale,
row_bytes: i32,
) -> PropertySet {
let props = PropertySet::new();
props.set_one(
GL_TEXTURE_INDEX,
Value::Int(unsafe { crate::bridge::render::texture_id(texture) }),
);
props.set_one(GL_TEXTURE_TARGET, Value::Int(GL_TEXTURE_2D));
props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
Value::String(cs(depth)),
);
props.set_one(
crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS,
Value::String(cs(components.to_ofx())),
);
props.set_one("OfxImageEffectPropPreMultiplication", Value::String(cs(premult)));
props.define(
crate::host::PROP_RENDER_SCALE,
vec![Value::Double(scale.x), Value::Double(scale.y)],
);
props.set_one("OfxImagePropPixelAspectRatio", Value::Double(par));
let bounds = crate::instance::OfxRectD { x1: 0.0, y1: 0.0, x2: width, y2: height };
rect_props(&props, crate::image::K_IMAGE_PROP_BOUNDS, bounds);
rect_props(&props, crate::image::K_IMAGE_PROP_ROD, bounds);
props.set_one(crate::image::K_IMAGE_PROP_ROW_BYTES, Value::Int(row_bytes));
props.set_one("OfxImagePropField", Value::String(cs("OfxImageFieldNone")));
props.set_one(
crate::image::K_IMAGE_PROP_UNIQUE_ID,
Value::String(crate::image::unique_identifier()),
);
props
}
/// 纹理请求格式 → 期望分量(NULL/未知 → None = 宿主自选)。
fn format_components(format: Option<&str>) -> Option<crate::image::Components> {
match format {
Some(GL_FORMAT_RGBA) | None => Some(crate::image::Components::Rgba),
Some(GL_FORMAT_RGB) => Some(crate::image::Components::Rgb),
Some(GL_FORMAT_ALPHA) => Some(crate::image::Components::Alpha),
// Luminance 族:Phase 2 不建模(无对应 Components),宿主
// 按 RGBA 上传并如实上报——格式核对走 None 分支。
Some(GL_FORMAT_LUMINANCE) | Some(GL_FORMAT_LUMINANCE_ALPHA) => None,
Some(_) => None,
}
}
/// clipLoadTexture:把 clip 在 `time` 的图像加载为 GL 纹理。
///
/// `format` 为请求的纹理格式(kOfxImageEffectGLFormat*NULL = 宿主
/// 按插件的 kOfxOpenGLPropPixelDepth 决定——Phase 2 全链路 F32)。
/// `region`(规范坐标,可空)会裁剪到 clip 的 RoD——Phase 2 仅支持
/// 整帧(NULL);子区域请求返回 Failed(插件按规范继续,视作黑底)。
unsafe extern "C" fn clip_load_texture(
clip: *mut c_void,
time: c_double,
format: *const c_char,
region: *const c_void,
out: *mut *mut c_void,
) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe { *out = std::ptr::null_mut() };
let c = resolve_clip(clip)?;
// GL 上下文(TLS):仅 GL 渲染期存在(ofxGPURender.h 的
// "OpenGL Current Context" 规则)。
let gl = crate::suites::gl_ctx().ok_or(status::ERR_MISSING_HOST_FEATURE)?;
let scale = crate::suites::render_ctx()
.map(|ctx| ctx.scale)
.unwrap_or(crate::instance::RenderScale { x: 1.0, y: 1.0 });
if c.name == "Output" {
// Output:返回已附着的输出纹理(format 忽略;渲染目标
// 绑定由调用方契约保证——等价 C++ attach_output_texture)。
let tex = gl.output_texture;
if tex.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let (w, h) = texture_size(&tex);
if w <= 0.0 || h <= 0.0 {
return Err(status::ERR_BAD_HANDLE);
}
let props = make_texture_props(
tex,
w,
h,
crate::image::Components::Rgba,
gl.gl_pixel_depth,
&clip_string(c, "OfxImageEffectPropPreMultiplication", PREMULT_NONE),
clip_par(c),
scale,
(w as i32) * 4 * 4,
);
let addr = register(Box::new(props), tex, true);
unsafe { *out = tag::make(addr as *const PropertySet, tag::PROPERTY_SET) };
return Ok(());
}
// 输入 clipPhase 2 仅支持整帧(fetch_image 的 phase-1 约束)。
if !region.is_null() {
return Err(status::FAILED);
}
let format = if format.is_null() {
None
} else {
unsafe { CStr::from_ptr(format) }.to_str().ok()
};
let image = c
.fetch_image(time, scale, None)
.map_err(|_| status::FAILED)?;
let components = image.components();
// 明确请求了不同分量 → Phase 2 不转换 → Failed(规范要求
// 满足请求格式;静默给错格式比显式失败更糟)。
if let Some(expected) = format_components(format) {
if expected != components {
return Err(status::FAILED);
}
}
let premult = clip_string(c, "OfxImageEffectPropPreMultiplication", PREMULT_NONE);
let (w, h) = (image_width(&image), image_height(&image));
if w <= 0.0 || h <= 0.0 {
return Err(status::FAILED);
}
let params = crate::bridge::render::VideoParams {
width: w as i32,
height: h as i32,
format: crate::bridge::render::PIXEL_FORMAT_F32,
..Default::default()
};
let tex = unsafe {
crate::bridge::render::texture_create(
gl.renderer,
&params,
image.pixels().as_ptr() as *const c_void,
image.row_bytes() as i32,
)
};
if tex.is_null() {
return Err(status::ERR_MEMORY);
}
let props = make_texture_props(
tex,
w,
h,
components,
gl.gl_pixel_depth,
&premult,
clip_par(c),
scale,
image.row_bytes() as i32,
);
let addr = register(Box::new(props), tex, false);
unsafe { *out = tag::make(addr as *const PropertySet, tag::PROPERTY_SET) };
Ok(())
})
}
/// 纹理尺寸(经 texture_get_params;失败回退 0,0)。
fn texture_size(tex: &crate::bridge::render::TextureHandle) -> (f64, f64) {
let mut p = crate::bridge::render::VideoParams::default();
if unsafe { crate::bridge::render::texture_get_params(*tex, &mut p) } == 0 && p.width > 0 {
(p.width as f64, p.height as f64)
} else {
(0.0, 0.0)
}
}
fn image_width(image: &Image) -> f64 {
(image.bounds().x2 - image.bounds().x1).round()
}
fn image_height(image: &Image) -> f64 {
(image.bounds().y2 - image.bounds().y1).round()
}
/// clip 的协商像素比。
fn clip_par(c: &ClipInstance) -> f64 {
c.props
.get("OfxImagePropPixelAspectRatio", 0)
.and_then(|v| match v {
Value::Double(d) => Some(d),
_ => None,
})
.unwrap_or(1.0)
}
/// clipFreeTexture:释放纹理句柄(输入 clip 删除 GL 纹理;Output 只
/// 释放句柄不删纹理——宿主还要读它)。
unsafe extern "C" fn clip_free_texture(texture_handle: *mut c_void) -> c_int {
caught(|| {
if texture_handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let addr = tag::strip(texture_handle) as usize;
let entry = {
let mut live = LIVE_TEXTURES.lock().unwrap_or_else(|e| e.into_inner());
live.remove(&addr)
};
match entry {
Some((_props, texture, is_output)) => {
if !is_output && !texture.is_null() {
let mut t = texture;
unsafe { crate::bridge::render::texture_free(&mut t) };
}
Ok(())
}
None => Err(status::ERR_BAD_HANDLE),
}
})
}
/// flushResources:宿主不缓存 GPU 资源 → REPLY_DEFAULT(规范语义
/// "nothing the host could do")。
unsafe extern "C" fn flush_resources() -> c_int {
status::REPLY_DEFAULT
}
/// 函数表布局(与 SDK `OfxImageEffectOpenGLRenderSuiteV1` 逐字段
/// 一致;ofxGPURender.h:181-310)。
#[repr(C)]
pub struct GlRenderSuiteV1 {
/// clipLoadTexture。
pub clip_load_texture: unsafe extern "C" fn(
*mut c_void,
c_double,
*const c_char,
*const c_void,
*mut *mut c_void,
) -> c_int,
/// clipFreeTexture。
pub clip_free_texture: unsafe extern "C" fn(*mut c_void) -> c_int,
/// flushResources。
pub flush_resources: unsafe extern "C" fn() -> c_int,
}
/// 函数表实例。
pub fn suite_v1() -> &'static GlRenderSuiteV1 {
static SUITE: std::sync::OnceLock<GlRenderSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| GlRenderSuiteV1 {
clip_load_texture: clip_load_texture,
clip_free_texture: clip_free_texture,
flush_resources: flush_resources,
})
}
+580
View File
@@ -0,0 +1,580 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! OfxImageEffectSuite v1clip/image 操作。
//!
//! 语义对照 HS: ofxhImageEffect.cpp
//! - clipGetImage 返回图像 **属性集** handleHS:2038 `getPropHandle`),
//! 图像存活由宿主表托管(get → 插入强引用,clipReleaseImage → 摘除
//! 释放;对应 HS 的引用计数配对,HS:2044);
//! - clipGetRegionOfDefinition:读 clip 实例属性里的
//! kOfxImageEffectPropRegionOfDefinition(协商时宿主写入);非法
//! RoD → kOfxStatFailedHS:2143-2147);
//! - abort:实例期 → 当前渲染的进度取消状态(HS:2154-2170
//! HS 默认返回 0,本实现按规范返回 REPLY_YES/REPLY_NO);
//! - imageMemory*:账本同 memory suiteHS 的 lock 是"锁住防重分配"
//! 语义,第 1 期账本不需要 → OK no-op,见 memory.rs 文档)。
//!
//! `// TODO(clip)`clipGetImage 依赖 [`crate::clip::ClipInstance::fetch_image`]
//! bridge::render 帧访问 C ABI 未冻结),代码已齐、运行时待 clip.rs。
use std::collections::HashMap;
use std::ffi::{c_char, c_double, c_int, c_void, CStr};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Mutex;
use crate::clip::ClipInstance;
use crate::descriptor::{ClipDescriptor, EffectDescriptor};
use crate::image::Image;
use crate::instance::{Instance, OfxRectD, RenderScale};
use crate::property::PropertySet;
use crate::suites::{status, tag};
/// 函数表布局(与 SDK `OfxImageEffectSuiteV1` 逐字段一致;常用子集
/// 注释,完整字段以 SDK 为准)。
#[repr(C)]
pub struct ImageEffectSuiteV1 {
/// getPropertySet:取 effect/clip/image 的属性集。
pub get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
/// getParamSet:取实例参数集。
pub get_param_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
/// clipDefinedescribe 期间):定义 clip。
pub clip_define: unsafe extern "C" fn(*mut c_void, *const c_char, *mut *mut c_void) -> c_int,
/// clipGetHandle:按名取 clip。
pub clip_get_handle: unsafe extern "C" fn(*mut c_void, *const c_char, *mut *mut c_void, *mut *mut c_void) -> c_int,
/// clipGetPropertySet
pub clip_get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
/// clipGetImage:取图像(host 侧增加引用,必须与
/// clipReleaseImage 配对)。
pub clip_get_image: unsafe extern "C" fn(*mut c_void, c_double, *const c_void, *mut *mut c_void) -> c_int,
/// clipReleaseImage
pub clip_release_image: unsafe extern "C" fn(*mut c_void) -> c_int,
/// clipGetRegionOfDefinition
pub clip_get_region_of_definition: unsafe extern "C" fn(*mut c_void, c_double, *mut c_void) -> c_int,
/// abort:查询是否应中止(进度取消透传)。
pub abort: unsafe extern "C" fn(*mut c_void) -> c_int,
/// imageMemoryAlloc / imageMemoryFree / imageMemoryLock /
/// imageMemoryUnlock:图像内存管理(账本同 memory suite)。
pub image_memory_alloc: unsafe extern "C" fn(*mut c_void, c_int, *mut *mut c_void) -> c_int,
/// imageMemoryFree
pub image_memory_free: unsafe extern "C" fn(*mut c_void) -> c_int,
/// imageMemoryLock
pub image_memory_lock: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
/// imageMemoryUnlock
pub image_memory_unlock: unsafe extern "C" fn(*mut c_void) -> c_int,
}
/// 存活图像表:clipGetImage 产出(props 地址 → 强引用;唯一持有者,
/// clipReleaseImage 摘除即释放——对应 HS 的 get/release 配对)。
static LIVE_IMAGES: std::sync::LazyLock<Mutex<HashMap<usize, std::sync::Arc<Image>>>> =
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
/// 公共入口模板:panic 兜底。
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
catch_unwind(AssertUnwindSafe(f))
.map_or_else(|_| status::FAILED, |r| r.map_or_else(|c| c, |()| status::OK))
}
/// 属性名(空指针/非 UTF-8 → ErrValue)。
unsafe fn c_name<'a>(name: *const c_char) -> Result<&'a str, c_int> {
if name.is_null() {
return Err(status::ERR_VALUE);
}
unsafe { CStr::from_ptr(name) }
.to_str()
.map_err(|_| status::ERR_VALUE)
}
/// effect 句柄解析(describe 期 → 描述符;实例期 → 实例)。
enum EffectRef<'a> {
Descriptor(&'a mut EffectDescriptor),
Instance(&'a Instance),
}
fn resolve_effect(handle: *mut c_void) -> Result<EffectRef<'static>, c_int> {
if handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
// 两者 props 均在偏移 0(句柄约定)。
unsafe {
match tag::kind(handle) {
tag::DESCRIPTOR => Ok(EffectRef::Descriptor(&mut *(tag::strip(handle) as *mut EffectDescriptor))),
tag::INSTANCE => Ok(EffectRef::Instance(&*(tag::strip(handle) as *const Instance))),
_ => Err(status::ERR_BAD_HANDLE),
}
}
}
/// clip 句柄解析(实例期;describe 期 ClipDescriptor 只在属性
/// suite 里用,不走这里)。
fn resolve_clip(handle: *mut c_void) -> Result<&'static ClipInstance, c_int> {
if handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe {
match tag::kind(handle) {
tag::CLIP => Ok(&*(tag::strip(handle) as *const ClipInstance)),
_ => Err(status::ERR_BAD_HANDLE),
}
}
}
/// getPropertySeteffect 属性集 = handle 本体(props 在偏移 0
/// HS:1886-1898 返回 `getProps().getHandle()`,同址)。
unsafe extern "C" fn get_property_set(effect: *mut c_void, out: *mut *mut c_void) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let _ = resolve_effect(effect)?;
unsafe { *out = effect };
Ok(())
})
}
/// getParamSetparam-set 即 effect 本体(HS:1901-1931 描述符/实例
/// 各自返回其 param set;本设计两者合一)。
unsafe extern "C" fn get_param_set(effect: *mut c_void, out: *mut *mut c_void) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let _ = resolve_effect(effect)?;
unsafe { *out = effect };
Ok(())
})
}
/// clipDefinedescribe 期):定义 clip 并返回其属性集 handle。
/// 重复名 → 整体替换(HS: `defineClip` 的 map 覆盖语义,
/// ofxhImageEffect.cpp:265-271;旧句柄随之失效,与 HS 一致)。
unsafe extern "C" fn clip_define(effect: *mut c_void, name: *const c_char, out: *mut *mut c_void) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe { *out = std::ptr::null_mut() };
let n = unsafe { c_name(name)? };
let desc = match resolve_effect(effect)? {
EffectRef::Descriptor(d) => d,
EffectRef::Instance(_) => return Err(status::ERR_BAD_HANDLE),
};
let clip = ClipDescriptor::new(n);
// 重复定义:替换原 Box(HS map 覆盖语义)。
if let Some(existing) = desc.clips.iter_mut().find(|c| c.name == n) {
*existing = Box::new(clip);
} else {
desc.clips.push(Box::new(clip));
}
// 地址取自已入盒的对象(栈上临时变量在移动后失效)。
let clip = desc.clips.iter().find(|c| c.name == n).expect("just stored");
let addr = &clip.props as *const _ as usize;
unsafe { *out = tag::make(addr as *const PropertySet, tag::CLIP) };
Ok(())
})
}
/// clipGetHandle(实例期):按名取 clip 及其属性集。未找到 →
/// BadHandleHS:2067-2070)。
unsafe extern "C" fn clip_get_handle(
effect: *mut c_void,
name: *const c_char,
clip: *mut *mut c_void,
property_set: *mut *mut c_void,
) -> c_int {
caught(|| {
if clip.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe { *clip = std::ptr::null_mut() };
let n = unsafe { c_name(name)? };
let handle = match resolve_effect(effect)? {
EffectRef::Instance(i) => {
let c = i.clips.iter().find(|c| c.name == n).ok_or(status::ERR_BAD_HANDLE)?;
let addr = &c.props as *const _ as usize;
tag::make(addr as *const PropertySet, tag::CLIP)
}
EffectRef::Descriptor(_) => return Err(status::ERR_BAD_HANDLE),
};
unsafe { *clip = handle };
if !property_set.is_null() {
// props 在偏移 0clip handle 与 props handle 同值。
unsafe { *property_set = handle };
}
Ok(())
})
}
/// clipGetPropertySetclip 属性集 = clip handle 本体。
unsafe extern "C" fn clip_get_property_set(clip: *mut c_void, out: *mut *mut c_void) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let _ = resolve_clip(clip)?;
unsafe { *out = clip };
Ok(())
})
}
/// clipGetImage:抓取输入图像并登记到存活表,返回图像属性集 handle
/// HS:2003-2049`getImage` 失败 → Failed)。
///
/// `// TODO(clip)`fetch_image 待 bridge::render 帧访问 C ABI。
unsafe extern "C" fn clip_get_image(
clip: *mut c_void,
time: c_double,
region: *const c_void,
out: *mut *mut c_void,
) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe { *out = std::ptr::null_mut() };
let c = resolve_clip(clip)?;
// Output clip:返回当前渲染的输出图像(render 驱动经 TLS
// 设置;对应 HS 渲染期输出图像挂在 Output clip 上)。
if c.name == "Output" {
if let Some(image) = crate::suites::current_output() {
let addr = &image.props as *const _ as usize;
LIVE_IMAGES
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(addr, image);
unsafe { *out = tag::make(addr as *const PropertySet, tag::IMAGE) };
return Ok(());
}
return Err(status::FAILED);
}
// 区域(可空):像素坐标的可选 bounds。
let region = if region.is_null() {
None
} else {
Some(unsafe { *(region as *const OfxRectD) })
};
// 渲染比例取自当前渲染上下文(HS 的 clip 在 render 期从
// in_args 拿到 scale;本设计经 TLS,见 suites::RenderCtx)。
let scale = crate::suites::render_ctx()
.map(|ctx| ctx.scale)
.unwrap_or(RenderScale { x: 1.0, y: 1.0 });
let image = c.fetch_image(time, scale, region).map_err(|_| status::FAILED)?;
let image = std::sync::Arc::new(image);
let addr = &image.props as *const _ as usize;
LIVE_IMAGES
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(addr, image);
unsafe { *out = tag::make(addr as *const PropertySet, tag::IMAGE) };
Ok(())
})
}
/// clipReleaseImage:摘除存活表强引用(图像随之释放;HS:2053-2068
/// 的 releaseReference 配对)。
unsafe extern "C" fn clip_release_image(image: *mut c_void) -> c_int {
caught(|| {
if image.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
if tag::kind(image) != tag::IMAGE {
return Err(status::ERR_BAD_HANDLE);
}
let addr = tag::strip(image) as usize;
let mut live = LIVE_IMAGES.lock().unwrap_or_else(|e| e.into_inner());
match live.remove(&addr) {
Some(_) => Ok(()),
None => Err(status::ERR_BAD_HANDLE),
}
})
}
/// clipGetRegionOfDefinition:读 clip 实例属性的协商 RoD;缺失/非法
/// → FailedHS:2111-2150;非法判断 x2<x1 || y2<y1 → Failed)。
unsafe extern "C" fn clip_get_region_of_definition(
clip: *mut c_void,
_time: c_double,
bounds: *mut c_void,
) -> c_int {
caught(|| {
if bounds.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let c = resolve_clip(clip)?;
let rod = read_rod(&c.props).ok_or(status::FAILED)?;
if rod.x2 < rod.x1 || rod.y2 < rod.y1 {
return Err(status::FAILED);
}
unsafe { *(bounds as *mut OfxRectD) = rod };
Ok(())
})
}
/// 从 clip 属性读协商 RoDkOfxImageEffectPropRegionOfDefinition
/// Double×4;协商时宿主写入)。
fn read_rod(props: &PropertySet) -> Option<OfxRectD> {
let v = props.get("OfxImageEffectPropRegionOfDefinition", 0)?;
let Value::Double(x1) = v else { return None };
let Value::Double(y1) = props.get("OfxImageEffectPropRegionOfDefinition", 1)? else {
return None;
};
let Value::Double(x2) = props.get("OfxImageEffectPropRegionOfDefinition", 2)? else {
return None;
};
let Value::Double(y2) = props.get("OfxImageEffectPropRegionOfDefinition", 3)? else {
return None;
};
Some(OfxRectD { x1, y1, x2, y2 })
}
/// abort:实例期 → 当前渲染进度是否已取消(HS:2154-2170 的
/// `instance->abort()`HS 默认 0,本实现按规范返回 REPLY_YES/NO)。
/// 取消状态是成功应答而非错误码,不能走 caught 的 Err 通道。
unsafe extern "C" fn abort(effect: *mut c_void) -> c_int {
catch_unwind(AssertUnwindSafe(|| {
match resolve_effect(effect)? {
EffectRef::Instance(_) => {}
EffectRef::Descriptor(_) => return Err(status::ERR_BAD_HANDLE),
}
Ok(())
}))
.map_or_else(
|_| status::FAILED,
|r| match r {
Ok(()) => {
if crate::suites::progress::is_cancelled() {
status::REPLY_YES
} else {
status::REPLY_NO
}
}
Err(c) => c,
},
)
}
/// imageMemoryAlloc:账本同 memory suiteHS:2174-2212;失败 →
/// ErrMemory)。`handle` 忽略。
unsafe extern "C" fn image_memory_alloc(
_handle: *mut c_void,
byte_size: c_int,
out: *mut *mut c_void,
) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_VALUE);
}
match crate::suites::memory::alloc(byte_size as usize) {
Some(ptr) => {
unsafe { *out = ptr as *mut c_void };
Ok(())
}
None => Err(status::ERR_MEMORY),
}
})
}
/// imageMemoryFree:账本销账;未知指针 → BadHandle(SDK 契约)。
unsafe extern "C" fn image_memory_free(memory: *mut c_void) -> c_int {
caught(|| {
if crate::suites::memory::free(memory as *mut u8) {
Ok(())
} else {
Err(status::ERR_BAD_HANDLE)
}
})
}
/// imageMemoryLock/UnlockHS 语义是"锁住防重分配"ofxhMemory.cpp
/// lock/unlock 计数),第 1 期账本不建模 → OK no-op(文档见
/// memory.rs)。
unsafe extern "C" fn image_memory_lock(memory: *mut c_void, out: *mut *mut c_void) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_VALUE);
}
// 账本里的地址即数据指针(分配即就绪,无 HS 的延迟分配)。
unsafe { *out = memory };
Ok(())
})
}
unsafe extern "C" fn image_memory_unlock(_memory: *mut c_void) -> c_int {
caught(|| Ok(()))
}
use crate::property::Value;
/// 静态函数表实例。
pub fn suite_v1() -> &'static ImageEffectSuiteV1 {
static SUITE: std::sync::OnceLock<ImageEffectSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| ImageEffectSuiteV1 {
get_property_set: get_property_set,
get_param_set: get_param_set,
clip_define: clip_define,
clip_get_handle: clip_get_handle,
clip_get_property_set: clip_get_property_set,
clip_get_image: clip_get_image,
clip_release_image: clip_release_image,
clip_get_region_of_definition: clip_get_region_of_definition,
abort: abort,
image_memory_alloc: image_memory_alloc,
image_memory_free: image_memory_free,
image_memory_lock: image_memory_lock,
image_memory_unlock: image_memory_unlock,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use crate::descriptor::EffectDescriptor;
use crate::property::PropertySet;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
fn descriptor_handle(d: &EffectDescriptor) -> *mut c_void {
tag::make(&d.props as *const PropertySet, tag::DESCRIPTOR)
}
/// describe 期:clipDefine 建 clip、属性 suite 读写其属性、
/// 重复定义整体替换(HS map 覆盖)。
#[test]
fn describe_clip_define_and_props() {
let mut desc = EffectDescriptor::new();
let s = suite_v1();
let h = descriptor_handle(&desc);
let mut clip: *mut c_void = std::ptr::null_mut();
let name = cs("Source");
unsafe {
assert_eq!((s.clip_define)(h, name.as_ptr(), &mut clip), 0);
}
assert_eq!(tag::kind(clip), tag::CLIP);
// 属性 suite 直接读写 clip handleprops 在偏移 0)。
let ps = crate::suites::property::suite_v1();
let label_prop = cs(crate::param::PROP_LABEL);
let optional_prop = cs(crate::descriptor::CLIP_OPTIONAL);
let label = cs("SourceLabel");
unsafe {
assert_eq!((ps.set_string)(clip, label_prop.as_ptr(), 0, label.as_ptr()), 0);
}
let mut out: *mut c_char = std::ptr::null_mut();
unsafe {
assert_eq!((ps.get_string)(clip, label_prop.as_ptr(), 0, &mut out), 0);
assert_eq!(CStr::from_ptr(out).to_bytes(), b"SourceLabel");
// 可选性标记:插件写 Optional=1。
assert_eq!((ps.set_int)(clip, optional_prop.as_ptr(), 0, 1), 0);
}
// getPropertySet / getParamSet:返回 effect 本体 handle。
let mut props: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.get_property_set)(h, &mut props), 0);
assert_eq!(props, h);
assert_eq!((s.get_param_set)(h, &mut props), 0);
assert_eq!(props, h);
}
// 重复 clipDefine:替换(旧句柄失效,HS 一致)。
let mut clip2: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.clip_define)(h, name.as_ptr(), &mut clip2), 0);
}
assert_ne!(clip, clip2);
assert_eq!(desc.clips.len(), 1);
// 空 out / 空 handle → BadHandle。
unsafe {
assert_eq!((s.clip_define)(h, name.as_ptr(), std::ptr::null_mut()), status::ERR_BAD_HANDLE);
assert_eq!((s.clip_define)(std::ptr::null_mut(), name.as_ptr(), &mut clip2), status::ERR_BAD_HANDLE);
}
}
/// abortdescribe 期 → BadHandle;实例期 → REPLY_NO(未取消)。
#[test]
fn abort_requires_instance() {
let desc = EffectDescriptor::new();
let s = suite_v1();
let dh = descriptor_handle(&desc);
unsafe {
assert_eq!((s.abort)(dh), status::ERR_BAD_HANDLE);
assert_eq!((s.abort)(std::ptr::null_mut()), status::ERR_BAD_HANDLE);
}
// 实例期:未取消 → REPLY_NO。
let inst = std::sync::Arc::new(crate::instance::Instance {
props: PropertySet::new(),
plugin: std::sync::Arc::new(crate::host::Plugin {
identifier: "t".into(),
version: (1, 0),
bundle_path: std::path::PathBuf::new(),
contexts: vec![],
descriptor: EffectDescriptor::new(),
lib: std::ptr::null_mut(),
entry: dummy_entry,
ofx_plugin: std::ptr::null_mut(),
}),
context: "OfxImageEffectContextFilter".into(),
params: crate::param::ParamSetInstance { params: vec![] },
clips: vec![],
node_identity: std::sync::atomic::AtomicUsize::new(0),
destroyed: std::sync::atomic::AtomicBool::new(false),
sequence_range: std::sync::Mutex::new(None),
progress_cb: std::sync::Mutex::new(None),
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
});
let ih = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
unsafe {
assert_eq!((s.abort)(ih), status::REPLY_NO);
}
}
unsafe extern "C" fn dummy_entry(
_: *const c_char,
_: *const c_void,
_: *mut c_void,
_: *mut c_void,
) -> c_int {
status::OK
}
/// imageMemoryAlloc/Free 走 memory 账本。
#[test]
fn image_memory_ledger() {
let s = suite_v1();
let mut mem: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.image_memory_alloc)(std::ptr::null_mut(), 1024, &mut mem), 0);
assert!(!mem.is_null());
// lock 返回数据指针(第 1 期 = 句柄本身)。
let mut ptr: *mut c_void = std::ptr::null_mut();
assert_eq!((s.image_memory_lock)(mem, &mut ptr), 0);
assert_eq!(ptr, mem);
assert_eq!((s.image_memory_unlock)(mem), 0);
// 未知指针 free → BadHandle。
assert_eq!((s.image_memory_free)(0xdeadbeef as *mut c_void), status::ERR_BAD_HANDLE);
assert_eq!((s.image_memory_free)(mem), 0);
}
}
}
+152
View File
@@ -0,0 +1,152 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! OfxMemorySuite v1:宿主代分配内存。
//!
//! 语义参照 HS: ofxhHost.cpp:26-42(裸 malloc/free 语义:失败 →
//! kOfxStatErrMemoryfree 恒 OK)——但本 crate 按骨架声明增加
//! **宿主级账本**:分配记录进账本,destroyInstance 时兜底回收
//! (泄漏防线;插件正常应自行 free)。未知指针 free →
//! kOfxStatErrBadHandleSDK ofxMemory.h:51 的契约)。
//! image_effect suite 的 imageMemory* 与此共用账本。
use std::alloc::Layout;
use std::sync::Mutex;
use crate::suites::status;
/// 对齐:malloc 保证 max_align_t(本平台 16 字节);插件会把缓冲当
/// float*/SIMD 用,16 字节是安全下限。
const ALIGN: usize = 16;
/// 一个已分配块:地址 + 布局(回收时按记录布局 dealloc)。
/// 地址以 `usize` 存(不透明令牌,避免裸指针破坏 `static` 的
/// Send/Sync 推导;账本只做 alloc/dealloc 配对,从不解引用)。
struct Block {
ptr: usize,
layout: Layout,
}
/// 宿主级账本(进程单例;memory suite 无宿主指针参数,全局即可)。
static LEDGER: Mutex<Vec<Block>> = Mutex::new(Vec::new());
/// 取锁(毒锁接管:一次持锁 panic 不级联)。
fn lock() -> std::sync::MutexGuard<'static, Vec<Block>> {
LEDGER.lock().unwrap_or_else(|e| e.into_inner())
}
/// 分配并记账。返回裸指针;失败(OOM/布局非法)返回 `None`。
pub(crate) fn alloc(size: usize) -> Option<*mut u8> {
// 零尺寸分配:Layout 要求非零,取 1 字节兜底(free 按同布局回收)。
let layout = Layout::from_size_align(size.max(1), ALIGN).ok()?;
let ptr = unsafe { std::alloc::alloc(layout) };
if ptr.is_null() {
return None;
}
lock().push(Block {
ptr: ptr as usize,
layout,
});
Some(ptr)
}
/// 按指针释放并销账。未知指针返回 `false`(suite 层映射
/// kOfxStatErrBadHandle);`NULL` 按 C 语义 no-op 返回 `true`。
pub(crate) fn free(ptr: *mut u8) -> bool {
if ptr.is_null() {
return true;
}
let mut ledger = lock();
let pos = ledger.iter().position(|b| b.ptr == ptr as usize);
match pos {
Some(i) => {
let b = ledger.remove(i);
unsafe { std::alloc::dealloc(b.ptr as *mut u8, b.layout) };
true
}
None => false,
}
}
/// 兜底回收全部在账块(destroyInstance 的泄漏防线)。返回回收块数
/// (泄漏断言用)。
pub(crate) fn sweep_leaked() -> usize {
let mut ledger = lock();
let n = ledger.len();
for b in ledger.drain(..) {
unsafe { std::alloc::dealloc(b.ptr as *mut u8, b.layout) };
}
n
}
/// 函数表布局(与 SDK `OfxMemorySuiteV1` 一致;`size_t` 在本平台
/// 与 `usize` 同宽,stable Rust 用 `usize` 表达——骨架的 `c_size_t`
/// 是不稳定特性,弃用)。
#[repr(C)]
pub struct MemorySuiteV1 {
/// memoryAlloc:分配 `size` 字节,写 `*out`。
pub alloc: unsafe extern "C" fn(*mut c_void, usize, *mut *mut c_void) -> c_int,
/// memoryFree:释放;NULL 是 no-op。
pub free: unsafe extern "C" fn(*mut c_void) -> c_int,
}
use std::ffi::{c_int, c_void};
/// memoryAlloc 实现:`handle` 忽略(HS 同,ofxhHost.cpp:27)。
///
/// `# Safety``out` 必须是可写指针(插件契约)。
unsafe extern "C" fn memory_alloc(
_handle: *mut c_void,
byte_size: usize,
out: *mut *mut c_void,
) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if out.is_null() {
return status::ERR_VALUE;
}
match alloc(byte_size) {
Some(ptr) => {
unsafe { *out = ptr as *mut c_void };
status::OK
}
// 与 HS 一致:分配失败 → kOfxStatErrMemoryofxhHost.cpp:35)。
None => status::ERR_MEMORY,
}
}))
.unwrap_or(status::FAILED)
}
/// memoryFree 实现:未知指针 → kOfxStatErrBadHandleSDK 契约);
/// NULL 按 C 语义 no-op → OKHS 的 free(NULL) 行为,ofxhHost.cpp:40)。
unsafe extern "C" fn memory_free(data: *mut c_void) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if free(data as *mut u8) {
status::OK
} else {
status::ERR_BAD_HANDLE
}
}))
.unwrap_or(status::FAILED)
}
/// 静态函数表实例。
pub fn suite_v1() -> &'static MemorySuiteV1 {
static SUITE: std::sync::OnceLock<MemorySuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| MemorySuiteV1 {
alloc: memory_alloc,
free: memory_free,
})
}
+295
View File
@@ -0,0 +1,295 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! OfxMessageSuite v1/v2:插件消息 → 宿主日志。
//!
//! stable Rust 无法定义 C-variadic 函数(c_variadic 仍不稳定),
//! v1 的 `...` 与 v2 的 va_list 入口由 C shim 承载
//! cbits/ofx_message_shim.cbuild.rs 经 `cc` 编译,符号随
//! staticlib 进入 liboakplugin):C 侧 vsnprintf 成定长缓冲后转发
//! 到本模块的 `oak_ofx_message_impl`。
//!
//! 消息出口 = facade 注册的 `oakplugin_message_fn`
//! include/plugin/host.h)。注意:骨架 ffi.rs 声明的 `MessageFn`
//! 与头文件不符(头文件是 `(type, message, userdata)`,骨架是
//! `(userdata, level, message)`)——做 ffi.rs 时以头文件为准修正。
//! 本模块按头文件契约建模。
use std::ffi::{c_char, c_int, c_void, CStr};
use crate::suites::status;
/// facade 消息回调(include/plugin/host.h `oakplugin_message_fn`):
/// `(type, message, userdata) -> OAKPLUGIN_MESSAGE_ANSWER_NO/YES`。
pub(crate) type MessageHandler =
unsafe extern "C" fn(*const c_char, *const c_char, *mut c_void) -> c_int;
/// 消息出口注册表(ffi 层经 `oakplugin_host_set_message_handler`
/// 写入,suite 读;userdata 以 usize 存,避免裸指针破坏 static 的
/// Send/Sync 推导)。
static HANDLER: std::sync::Mutex<(Option<MessageHandler>, usize)> =
std::sync::Mutex::new((None, 0));
/// 注册/注销消息出口(ffi 层 `oakplugin_host_set_message_handler`
/// 调用;公开:测试直接注入捕获器)。
pub fn set_handler(f: Option<MessageHandler>, userdata: *mut c_void) {
let mut h = HANDLER.lock().unwrap_or_else(|e| e.into_inner());
*h = (f, userdata as usize);
}
/// 读取当前出口。
fn handler() -> (Option<MessageHandler>, *mut c_void) {
let h = HANDLER.lock().unwrap_or_else(|e| e.into_inner());
(h.0, h.1 as *mut c_void)
}
/// kOfxMessageQuestionofxMessage.h:61):无出口时答"否"。
const K_MESSAGE_QUESTION: &[u8] = b"OfxMessageQuestion";
/// 消息类型是否为 question。
fn is_question(type_: *const c_char) -> bool {
// 空指针已在调用点拦截;此处仍防御(CStr::from_ptr 的调用方义务)。
if type_.is_null() {
return false;
}
unsafe { CStr::from_ptr(type_) }.to_bytes() == K_MESSAGE_QUESTION
}
/// Rust 侧实现(C shim 转发至此;`#[no_mangle]` 保证符号名与 C 侧
/// 引用一致)。语义逐条对照 C++:
/// - type/message 为空 → kOfxStatFailedolivehost.cpp:267vmessage
/// 对 !type || !format 的处理;format 为空的 case 由 shim 以
/// NULL message 转发);
/// - 有出口 → 转发 `(type, message, userdata)`,返回按头文件契约
/// 映射为 REPLY_YES/REPLY_NOolivehost.cpp:277-279 的 handler
/// 路径;非 question 消息插件忽略返回值);
/// - 无出口 → stderr 日志(镜像 olivehost.cpp:282 的 fprintf 默认),
/// question 答"否"olivehost.cpp:283-284)。
///
/// `# Safety``type`/`message` 必须是有效 C 字符串(插件契约,
/// shim 已保证 message 非悬垂——指向其栈缓冲,调用期间有效)。
#[no_mangle]
pub unsafe extern "C" fn oak_ofx_message_impl(
_handle: *mut c_void,
type_: *const c_char,
_id: *const c_char,
message: *const c_char,
) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if type_.is_null() || message.is_null() {
return status::FAILED;
}
let (handler, userdata) = handler();
if let Some(h) = handler {
// 头文件契约:返回值 0/1 即答复 NO/YES。
let answer = unsafe { h(type_, message, userdata) };
return if answer != 0 {
status::REPLY_YES
} else {
status::REPLY_NO
};
}
// Headless 默认:stderrC++ fprintf 的 Rust 等价物)。
eprintln!("OFX message: {}", unsafe { CStr::from_ptr(message) }.to_string_lossy());
if is_question(type_) {
status::REPLY_NO
} else {
status::OK
}
}))
.unwrap_or(status::FAILED)
}
/// 函数表布局(OfxMessageSuiteV1)。
#[repr(C)]
pub struct MessageSuiteV1 {
/// message(变长参数版;入口在 C shim)
pub message: unsafe extern "C" fn(
*mut c_void,
*const c_char,
*const c_char,
*const c_char,
...
) -> c_int,
}
/// 函数表布局(OfxMessageSuiteV2:第 5 参为平台相关 `va_list`)。
///
/// 真实 ABI 是 C 的 va_listApple arm64 上是结构体按值传递);
/// 声明为不透明指针仅作占位——本表只被插件→C shim 调用,Rust 侧
/// 永不直接调用,指针类型不参与任何实际调用。
#[repr(C)]
pub struct MessageSuiteV2 {
/// messageva_list 版;入口在 C shim
pub message: unsafe extern "C" fn(
*mut c_void,
*const c_char,
*const c_char,
*const c_char,
*mut c_void,
) -> c_int,
}
extern "C" {
/// C shim 的 v1 入口(cbits/ofx_message_shim.c)。
fn ofx_message_shim_v1(
handle: *mut c_void,
type_: *const c_char,
id: *const c_char,
format: *const c_char,
...
) -> c_int;
/// C shim 的 v2 入口(va_list 已由 C 侧消费,Rust 只见不透明值)。
fn ofx_message_shim_v2(
handle: *mut c_void,
type_: *const c_char,
id: *const c_char,
format: *const c_char,
args: *mut c_void,
) -> c_int;
}
/// 静态函数表实例(v1)。
pub fn suite_v1() -> &'static MessageSuiteV1 {
static SUITE: std::sync::OnceLock<MessageSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| MessageSuiteV1 {
message: ofx_message_shim_v1,
})
}
/// 静态函数表实例(v2)。
pub fn suite_v2() -> &'static MessageSuiteV2 {
static SUITE: std::sync::OnceLock<MessageSuiteV2> = std::sync::OnceLock::new();
SUITE.get_or_init(|| MessageSuiteV2 {
message: ofx_message_shim_v2,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
/// 测试共享全局 HANDLER,cargo 默认并行执行——串行化契约用例。
static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// 捕获型出口:把 (type, message) 记进 userdata 指向的 Vec。
unsafe extern "C" fn capture(
type_: *const c_char,
message: *const c_char,
userdata: *mut c_void,
) -> c_int {
let v = unsafe { &mut *(userdata as *mut Vec<(String, String)>) };
v.push((
unsafe { CStr::from_ptr(type_) }.to_string_lossy().into_owned(),
unsafe { CStr::from_ptr(message) }.to_string_lossy().into_owned(),
));
1
}
/// v1 入口:`...` 在 C shim 侧 vsnprintf 后转发,格式化正确。
#[test]
fn shim_v1_formats_and_forwards() {
let _g = TEST_LOCK.lock().unwrap();
let mut captured: Vec<(String, String)> = Vec::new();
set_handler(Some(capture), &mut captured as *mut _ as *mut c_void);
let s = suite_v1();
let type_ = CString::new("OfxMessageError").unwrap();
let id = CString::new("test-id").unwrap();
let fmt = CString::new("value=%d name=%s").unwrap();
let name = CString::new("oak").unwrap();
let r = unsafe {
(s.message)(
std::ptr::null_mut(),
type_.as_ptr(),
id.as_ptr(),
fmt.as_ptr(),
42,
name.as_ptr(),
)
};
// 出口答 YES → REPLY_YES。
assert_eq!(r, status::REPLY_YES);
assert_eq!(captured.len(), 1);
assert_eq!(captured[0].0, "OfxMessageError");
assert_eq!(captured[0].1, "value=42 name=oak");
set_handler(None, std::ptr::null_mut());
}
/// v1 入口:NULL format → kOfxStatFailedC++ olivehost.cpp:267)。
#[test]
fn shim_v1_null_format_fails() {
let _g = TEST_LOCK.lock().unwrap();
let s = suite_v1();
let type_ = CString::new("OfxMessageError").unwrap();
let id = CString::new("id").unwrap();
let r = unsafe {
(s.message)(
std::ptr::null_mut(),
type_.as_ptr(),
id.as_ptr(),
std::ptr::null(),
)
};
assert_eq!(r, status::FAILED);
}
/// v2 入口:表字段非空且指向 C shim 符号(va_list 路径无法从
/// Rust 构造 va_list 调用,链的其余部分与 v1 共用 forward)。
#[test]
fn shim_v2_table_entry_resolves() {
let _g = TEST_LOCK.lock().unwrap();
let s = suite_v2();
assert!(!std::ptr::eq(s.message as *const (), ofx_message_shim_v1 as *const ()));
}
/// 无出口(headless 默认):非 question → OKquestion → REPLY_NO。
#[test]
fn no_handler_headless_defaults() {
let _g = TEST_LOCK.lock().unwrap();
set_handler(None, std::ptr::null_mut());
let s = suite_v1();
let type_ = CString::new("OfxMessageError").unwrap();
let id = CString::new("id").unwrap();
let fmt = CString::new("hello %s").unwrap();
let arg = CString::new("x").unwrap();
let r = unsafe {
(s.message)(
std::ptr::null_mut(),
type_.as_ptr(),
id.as_ptr(),
fmt.as_ptr(),
arg.as_ptr(),
)
};
assert_eq!(r, status::OK);
let q = CString::new("OfxMessageQuestion").unwrap();
let r = unsafe {
(s.message)(
std::ptr::null_mut(),
q.as_ptr(),
id.as_ptr(),
fmt.as_ptr(),
arg.as_ptr(),
)
};
assert_eq!(r, status::REPLY_NO);
}
}
+325
View File
@@ -0,0 +1,325 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! suite 层:插件调进来的 C 函数表(unsafe trampoline 集中区)。
//!
//! 每张 suite 是一个 `#[repr(C)]` 的函数表静态实例;插件经
//! [`fetch_suite`] 取得。所有入口的安全模式相同:
//!
//! 1. 解 handle`RefBox`/`PropertySet` 指针),空指针 →
//! kOfxStatErrBadHandle
//! 2. 调 safe Rust 实现;
//! 3. `catch_unwind` 兜底 → kOfxStatFailed。
//!
//! 参照:HS: ofxhImageEffect.cpp:2776fetchSuite 分发表与版本协商)。
pub mod gl_render;
pub mod image_effect;
pub mod memory;
pub mod message;
pub mod multithread;
pub mod param;
pub mod progress;
pub mod property;
pub mod timeline;
/// OFX 状态码(全表与 SDK ofxCore.h:895-953 逐字一致;插件对状态码
/// 做精确比较,任何偏差都是静默兼容性破坏)。
pub mod status {
/// kOfxStatOK:成功。
pub const OK: i32 = 0;
/// kOfxStatFailed:通用失败。
pub const FAILED: i32 = 1;
/// kOfxStatErrFatal:致命错误。
pub const ERR_FATAL: i32 = 2;
/// kOfxStatErrUnknown:未知对象/属性。
pub const ERR_UNKNOWN: i32 = 3;
/// kOfxStatErrMissingHostFeature:宿主缺功能。
pub const ERR_MISSING_HOST_FEATURE: i32 = 4;
/// kOfxStatErrUnsupported:不支持的操作。
pub const ERR_UNSUPPORTED: i32 = 5;
/// kOfxStatErrExists:对象已存在。
pub const ERR_EXISTS: i32 = 6;
/// kOfxStatErrFormat:格式错误。
pub const ERR_FORMAT: i32 = 7;
/// kOfxStatErrMemory:内存不足。
pub const ERR_MEMORY: i32 = 8;
/// kOfxStatErrBadHandle:非法 handle(含 NULL)。
pub const ERR_BAD_HANDLE: i32 = 9;
/// kOfxStatErrBadIndex:索引越界。
pub const ERR_BAD_INDEX: i32 = 10;
/// kOfxStatErrValue:非法值。
pub const ERR_VALUE: i32 = 11;
/// kOfxStatReplyYes。
pub const REPLY_YES: i32 = 12;
/// kOfxStatReplyNo。
pub const REPLY_NO: i32 = 13;
/// kOfxStatReplyDefault。
pub const REPLY_DEFAULT: i32 = 14;
}
/// 句柄约定:发给插件的每个 OFX 句柄 = 对象 `props` 字段地址 | 低
/// 3 位标签。
///
/// 前置条件(本 crate 内部纪律,宿主创建对象时遵守):
/// - 对象 `#[repr(C)]` 且 `props: PropertySet` 在**偏移 0**——句柄的
/// 地址就是属性集地址,与 C++ "基类在偏移 0"HS 里
/// OfxPropertySetHandle 即 Property::Set*)同构,属性 suite 无需
/// 反查即可直接解引用;
/// - 对象堆上稳定:`Arc<RefBox<T>>` / 结构体内的 `Box<T>` 元素
/// (Vec 重分配只移动 Box 指针,不移动负载)。
///
/// 标签位编码:低 3 位(含 Mutex/Arc 的对象对齐 ≥8,低 3 位恒 0)。
pub mod tag {
/// 标签掩码(低 3 位)。
pub const MASK: usize = 0b111;
/// 裸属性集(宿主内部直用 / 测试直传 `&props`)。
pub const PROPERTY_SET: usize = 0;
/// [`crate::descriptor::EffectDescriptor`]describe 期 effect/
/// param-set handle)。
pub const DESCRIPTOR: usize = 1;
/// [`crate::instance::Instance`](实例期 effect/param-set handle)。
pub const INSTANCE: usize = 2;
/// [`crate::param::ParamDef`]describe 期 param handle)。
pub const PARAM_DEF: usize = 3;
/// [`crate::param::ParamInstance`](实例期 param handle)。
pub const PARAM_INSTANCE: usize = 4;
/// [`crate::clip::ClipInstance`](实例期 clip handledescribe 期
/// [`crate::descriptor::ClipDescriptor`] 共用此标签——两类都只在
/// 各自的阶段被使用,属性 suite 不区分)。
pub const CLIP: usize = 5;
/// [`crate::image::Image`]clipGetImage 的产物)。
pub const IMAGE: usize = 6;
/// 打标签(宿主创建对象句柄用;公开供宿主/测试构造句柄)。
pub fn make(
props: *const crate::property::PropertySet,
t: usize,
) -> *mut std::ffi::c_void {
(props as usize | t) as *mut std::ffi::c_void
}
/// 剥标签,取 props 指针(标签 0 的裸指针原样返回)。
pub fn strip(handle: *mut std::ffi::c_void) -> *const crate::property::PropertySet {
((handle as usize) & !MASK) as *const crate::property::PropertySet
}
/// 读标签。
pub fn kind(handle: *mut std::ffi::c_void) -> usize {
(handle as usize) & MASK
}
}
/// 渲染上下文(TLS):render 驱动在调用插件 action 前设置,suite
/// 回调(timeline、clipGetImage 的 scale、进度)从中读取;对应 HS
/// 中实例在渲染期的状态(ofxhImageEffect.cpp Render in_args 的子集)。
#[derive(Clone, Copy)]
pub struct RenderCtx {
/// 当前渲染时间(秒)。
pub time: f64,
/// 渲染比例。
pub scale: crate::instance::RenderScale,
/// 项目时间域(timeline getTimeBounds)。
pub range: crate::instance::OfxRangeD,
}
thread_local! {
static RENDER_CTX: std::cell::RefCell<Option<RenderCtx>> = const { std::cell::RefCell::new(None) };
}
/// 设置/清除渲染上下文(render 驱动调用;action 返回前必须清除;
/// 公开:render 驱动在 host 层,测试也直接注入)。
pub fn set_render_ctx(ctx: Option<RenderCtx>) {
RENDER_CTX.with(|c| *c.borrow_mut() = ctx);
}
/// 读取渲染上下文(无上下文返回 None;无 TLS 时插件时间查询得到
/// 0 的 headless 默认)。
pub(crate) fn render_ctx() -> Option<RenderCtx> {
RENDER_CTX.with(|c| *c.borrow())
}
thread_local! {
/// 当前渲染的输出图像(render 驱动设置;image effect suite 的
/// clipGetImage 对 Output clip 返回它)。
static CURRENT_OUTPUT: std::cell::RefCell<Option<std::sync::Arc<crate::image::Image>>> =
const { std::cell::RefCell::new(None) };
}
/// 设置/清除当前输出图像(render 驱动调用;action 返回前必须清除)。
pub(crate) fn set_current_output(image: Option<std::sync::Arc<crate::image::Image>>) {
CURRENT_OUTPUT.with(|c| *c.borrow_mut() = image);
}
/// 读取当前输出图像。
pub(crate) fn current_output() -> Option<std::sync::Arc<crate::image::Image>> {
CURRENT_OUTPUT.with(|c| c.borrow().clone())
}
/// GL 渲染上下文(TLS):GL render 驱动在调用插件 action 前设置,
/// OpenGLRender suite 的 clipLoadTexture 从中取当前渲染器与输出
/// 纹理(对应 C++ 里实例渲染期的 GL 状态;ofxGPURender.h
/// "OpenGL Current Context" 一节要求宿主在 Render 期间持有 GL
/// 上下文——本设计的约定是调用方(oakrender 的 PluginJob 路径)在
/// 进入 render_job 前已把渲染器上下文置为 current,本表只传递句柄)。
#[derive(Clone, Copy)]
pub struct GlCtx {
/// 当前渲染器(oakrender 句柄)。
pub renderer: crate::bridge::render::RendererHandle,
/// 已附着的输出纹理(渲染目标;GL 模式下插件把结果画进它)。
pub output_texture: crate::bridge::render::TextureHandle,
/// 当前 GL 纹理的实际像素深度(kOfxBitDepth* 静态串;Phase 2
/// 全链路 F32,由 render_gl 按插件 kOfxOpenGLPropPixelDepth 协商
/// 后填入——纹理句柄的 kOfxImageEffectPropPixelDepth 以它为准)。
pub gl_pixel_depth: &'static str,
}
thread_local! {
static GL_CTX: std::cell::RefCell<Option<GlCtx>> = const { std::cell::RefCell::new(None) };
}
/// 设置/清除 GL 渲染上下文(GL render 驱动调用;action 返回前必须
/// 清除;公开:测试直接注入)。
pub fn set_gl_ctx(ctx: Option<GlCtx>) {
GL_CTX.with(|c| *c.borrow_mut() = ctx);
}
/// 读取 GL 渲染上下文(无上下文返回 None——clipLoadTexture 在非
/// GL 渲染期调用时按规范返回 kOfxStatErrMissingHostFeature)。
pub(crate) fn gl_ctx() -> Option<GlCtx> {
GL_CTX.with(|c| *c.borrow())
}
/// 宿主进程身份(fetchSuite 的 version 检查用;= OFX API 1.5)。
pub(crate) const OFX_API_VERSION: i32 = 105;
/// fetchSuite 宿主入口:按名字与版本返回函数表指针;不认识或版本
/// 不符返回 `None`FFI 层转 NULL)。
///
/// 第 1 期注册:OfxPropertySuite v1、OfxMemorySuite v1、
/// OfxImageEffectSuite v1、OfxParameterSuite v1、OfxMessageSuite
/// v1/v2、OfxProgressSuite v1/v2、OfxTimeLineSuite v1、
/// OfxMultiThreadSuite v1。
/// 第 2 期追加:OfxImageEffectOpenGLRenderSuite v1GL 路径);
/// ofxColour 无 suite 表(纯属性 + GetOutputColourspace action)。
pub fn fetch_suite(name: &str, version: i32) -> Option<*const std::ffi::c_void> {
let suite: *const std::ffi::c_void = match (name, version) {
("OfxPropertySuite", 1) => ptr(property::suite_v1()),
("OfxMemorySuite", 1) => ptr(memory::suite_v1()),
("OfxImageEffectSuite", 1) => ptr(image_effect::suite_v1()),
("OfxParameterSuite", 1) => ptr(param::suite_v1()),
("OfxMessageSuite", 1) => ptr(message::suite_v1()),
("OfxMessageSuite", 2) => ptr(message::suite_v2()),
("OfxProgressSuite", 1) => ptr(progress::suite_v1()),
("OfxProgressSuite", 2) => ptr(progress::suite_v2()),
("OfxTimeLineSuite", 1) => ptr(timeline::suite_v1()),
("OfxMultiThreadSuite", 1) => ptr(multithread::suite_v1()),
("OfxImageEffectOpenGLRenderSuite", 1) => ptr(gl_render::suite_v1()),
_ => return None,
};
Some(suite)
}
/// 表引用 → 不透明指针(fetch_suite 的统一出口)。
fn ptr<T>(p: &'static T) -> *const std::ffi::c_void {
p as *const T as *const std::ffi::c_void
}
#[cfg(test)]
mod tests {
use super::*;
/// 八张 suite 的分发表:版本精确匹配、未知版本/名字 → None。
#[test]
fn fetch_suite_dispatch() {
assert!(fetch_suite("OfxPropertySuite", 1).is_some());
assert!(fetch_suite("OfxMemorySuite", 1).is_some());
assert!(fetch_suite("OfxImageEffectSuite", 1).is_some());
assert!(fetch_suite("OfxParameterSuite", 1).is_some());
assert!(fetch_suite("OfxMessageSuite", 1).is_some());
assert!(fetch_suite("OfxMessageSuite", 2).is_some());
assert!(fetch_suite("OfxProgressSuite", 1).is_some());
assert!(fetch_suite("OfxProgressSuite", 2).is_some());
assert!(fetch_suite("OfxTimeLineSuite", 1).is_some());
assert!(fetch_suite("OfxMultiThreadSuite", 1).is_some());
assert!(fetch_suite("OfxImageEffectOpenGLRenderSuite", 1).is_some());
assert!(fetch_suite("OfxPropertySuite", 2).is_none());
assert!(fetch_suite("OfxMessageSuite", 3).is_none());
assert!(fetch_suite("OfxImageEffectOpenGLRenderSuite", 2).is_none());
assert!(fetch_suite("OfxBogusSuite", 1).is_none());
assert!(fetch_suite("", 1).is_none());
}
/// 句柄标签:打标/剥标/读 kind 往返;对齐地址低位为 0。
#[test]
fn handle_tag_roundtrip() {
let set = crate::property::PropertySet::new();
let props = &set as *const crate::property::PropertySet;
assert_eq!(props as usize & tag::MASK, 0, "PropertySet 必须 ≥8 对齐");
for t in [
tag::PROPERTY_SET,
tag::DESCRIPTOR,
tag::INSTANCE,
tag::PARAM_DEF,
tag::PARAM_INSTANCE,
tag::CLIP,
tag::IMAGE,
] {
let h = tag::make(props, t);
assert_eq!(tag::kind(h), t);
assert_eq!(tag::strip(h) as usize, props as usize);
}
}
/// 渲染上下文 TLS:设置/读取/清除。
#[test]
fn render_ctx_tls() {
assert!(render_ctx().is_none());
let ctx = RenderCtx {
time: 1.5,
scale: crate::instance::RenderScale { x: 2.0, y: 2.0 },
range: crate::instance::OfxRangeD { min: 0.0, max: 100.0 },
};
set_render_ctx(Some(ctx));
let got = render_ctx().unwrap();
assert_eq!(got.time, 1.5);
assert_eq!(got.scale.x, 2.0);
assert_eq!(got.range.max, 100.0);
set_render_ctx(None);
assert!(render_ctx().is_none());
}
/// GL 上下文 TLS:设置/读取/清除(clipLoadTexture 的渲染期取值)。
#[test]
fn gl_ctx_tls() {
assert!(gl_ctx().is_none());
let renderer = crate::handle::CHandle::null();
let tex = crate::handle::CHandle::null();
set_gl_ctx(Some(GlCtx {
renderer,
output_texture: tex,
gl_pixel_depth: "OfxBitDepthFloat",
}));
let got = gl_ctx().unwrap();
assert!(got.renderer.is_null());
assert!(got.output_texture.is_null());
assert_eq!(got.gl_pixel_depth, "OfxBitDepthFloat");
set_gl_ctx(None);
assert!(gl_ctx().is_none());
}
}
+217
View File
@@ -0,0 +1,217 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! OfxMultiThreadSuite v1:插件自起线程的登记与索引分配。
//!
//! 纪律:插件线程可在本 suite 存活期回调任意 suite——所有共享状态
//! 必须 MutexREADME §3)。线程经 `std::thread::scope` 启动并同步
//! joinOFX 语义:multiThread 返回时全部线程已完成;
//! HS: ofxhImageEffect.cpp gMultiThreadSuite),故无需常驻线程表。
//! 线程索引经 TLS 分配:宿主线程 [`index`] 为 -1、
//! [`is_spawned`] 为 0。
//!
//! 参照:HS: ofxhImageEffect.cpp gMultiThreadSuite。
use std::ffi::{c_int, c_uint, c_void};
use crate::suites::status;
thread_local! {
/// 本线程的 OFX 线程索引(宿主线程 = None)。
static THREAD_INDEX: std::cell::Cell<Option<c_uint>> = const { std::cell::Cell::new(None) };
}
/// 函数表布局(OfxMultiThreadSuiteV1)。
#[repr(C)]
pub struct MultiThreadSuiteV1 {
/// multiThread:启动 `n_threads` 个线程跑 `func`,线程索引
/// 0..n_threads-1 经 `thread_arg` 传入。
pub multi_thread: unsafe extern "C" fn(
func: unsafe extern "C" fn(c_uint, c_uint, *mut c_void),
n_threads: c_uint,
thread_arg: *mut c_void,
) -> c_int,
/// multiThreadNumCPUs
pub num_cpus: unsafe extern "C" fn(*mut c_int) -> c_int,
/// multiThreadIndex:当前线程的 OFX 线程索引(宿主线程为 -1)。
pub index: unsafe extern "C" fn(*mut c_int) -> c_int,
/// multiThreadIsSpawnedThread:当前线程是否插件线程。
pub is_spawned: unsafe extern "C" fn(*mut c_int) -> c_int,
}
/// 公共入口模板:panic 兜底。
fn caught(f: impl FnOnce() -> c_int) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or(status::FAILED)
}
/// multiThread 实现:同步起 n 个线程(0..n-1),join 后返回。
/// 线程体只做两件事:登记 TLS 索引、调 `func`C 代码,不 panic)。
unsafe extern "C" fn multi_thread(
func: unsafe extern "C" fn(c_uint, c_uint, *mut c_void),
n_threads: c_uint,
thread_arg: *mut c_void,
) -> c_int {
caught(|| {
// 裸指针不可 Send:以 usize 搬运,线程内还原(C 侧本即
// 整数传递语义)。
let arg = thread_arg as usize;
let _ = std::thread::scope(|scope| {
for i in 0..n_threads {
scope.spawn(move || {
THREAD_INDEX.with(|t| t.set(Some(i)));
// func 是插件代码:按 OFX 契约不 panicRust 侧
// 只做 TLS 登记,无 panic 源。
unsafe { (func)(i, n_threads, arg as *mut c_void) };
THREAD_INDEX.with(|t| t.set(None));
});
}
});
status::OK
})
}
unsafe extern "C" fn multi_thread_num_cpus(out: *mut c_int) -> c_int {
caught(|| {
if out.is_null() {
return status::ERR_VALUE;
}
// 物理核数(HS 同:系统 CPU 数)。
let n = std::thread::available_parallelism()
.map(|n| n.get() as c_int)
.unwrap_or(1);
unsafe { *out = n };
status::OK
})
}
unsafe extern "C" fn multi_thread_index(out: *mut c_int) -> c_int {
caught(|| {
if out.is_null() {
return status::ERR_VALUE;
}
// 宿主线程 → -1(HS 约定)。
unsafe { *out = THREAD_INDEX.with(|t| t.get()).map_or(-1, |i| i as c_int) };
status::OK
})
}
unsafe extern "C" fn multi_thread_is_spawned(out: *mut c_int) -> c_int {
caught(|| {
if out.is_null() {
return status::ERR_VALUE;
}
unsafe { *out = THREAD_INDEX.with(|t| t.get().is_some()) as c_int };
status::OK
})
}
/// 静态函数表实例。
pub fn suite_v1() -> &'static MultiThreadSuiteV1 {
static SUITE: std::sync::OnceLock<MultiThreadSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| MultiThreadSuiteV1 {
multi_thread: multi_thread,
num_cpus: multi_thread_num_cpus,
index: multi_thread_index,
is_spawned: multi_thread_is_spawned,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
/// 每个插件线程把 (index, count) 累计进 userdata。
unsafe extern "C" fn worker(index: c_uint, count: c_uint, arg: *mut c_void) {
let state = unsafe { &mut *(arg as *mut (AtomicUsize, AtomicUsize)) };
state.0.fetch_add(index as usize, Ordering::Relaxed);
state.1.fetch_add(count as usize, Ordering::Relaxed);
}
#[test]
fn multi_thread_spawns_and_joins() {
let s = suite_v1();
let state = (AtomicUsize::new(0), AtomicUsize::new(0));
unsafe {
assert_eq!(
(s.multi_thread)(worker, 8, &state as *const _ as *mut c_void),
status::OK
);
}
// 索引 0..7 各一次 → 28count 各 8 → 64。
assert_eq!(state.0.load(Ordering::Relaxed), 28);
assert_eq!(state.1.load(Ordering::Relaxed), 64);
// 0 线程:no-op OK。
unsafe {
assert_eq!(
(s.multi_thread)(worker, 0, &state as *const _ as *mut c_void),
status::OK
);
}
}
/// 插件线程内查询 index/isSpawned 并把结果写回 arg 指向的槽。
/// (闭包捕获 suite 无法转 fn 指针,用具名函数 + 静态槽。)
static SPAWNED_RESULT: std::sync::LazyLock<std::sync::Mutex<Option<(c_int, c_int)>>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(None));
unsafe extern "C" fn query_worker(_index: c_uint, _count: c_uint, _arg: *mut c_void) {
let s = suite_v1();
let mut i = 0;
let mut sp = 1;
unsafe {
let _ = (s.index)(&mut i);
let _ = (s.is_spawned)(&mut sp);
}
*SPAWNED_RESULT.lock().unwrap_or_else(|e| e.into_inner()) = Some((i, sp));
}
#[test]
fn index_and_spawned() {
let s = suite_v1();
let mut i = 0;
let mut spawned = 1;
unsafe {
// 宿主线程:index = -1isSpawned = 0。
assert_eq!((s.index)(&mut i), status::OK);
assert_eq!(i, -1);
assert_eq!((s.is_spawned)(&mut spawned), status::OK);
assert_eq!(spawned, 0);
// 插件线程内:index 正确、isSpawned = 1。
assert_eq!(
(s.multi_thread)(query_worker, 1, std::ptr::null_mut()),
status::OK
);
assert_eq!(
*SPAWNED_RESULT.lock().unwrap_or_else(|e| e.into_inner()),
Some((0, 1))
);
}
}
#[test]
fn num_cpus_positive() {
let s = suite_v1();
let mut n = 0;
unsafe {
assert_eq!((s.num_cpus)(&mut n), status::OK);
}
assert!(n >= 1);
}
}
+983
View File
@@ -0,0 +1,983 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! OfxParameterSuite v1:参数定义与读写。
//!
//! 语义对照 HS: ofxhParam.cpp
//! - paramDefinedescribe 期把参数定义挂到效果描述符(未定义类型 →
//! kOfxStatErrUnsupportedHS:1665-1710parametric 第 1 期不支持);
//! - paramGetHandle:按名查(未找到 → kOfxStatErrUnknownHS:1758);
//! - paramGetValue/paramSetValue 等变长入口在 C shim
//! cbits/ofx_param_shim.c):按 [`crate::param::ParamKind`] 解析
//! va_args 后转发到本模块的 `ofx_param_*_impl`
//! - paramSetValue 成功后触发 instanceChangedkOfxChangePluginEdited
//! HS:1982-1994 的 `paramChangedByPlugin`),经
//! [`crate::param::notify_instance_changed`] 回写绑定节点;
//! - 第 1 期无动画:AtTime == 当前值;keys 恒 0derivative/integral
//! → kOfxStatErrMissingHostFeatureeditBegin/End 为编辑事务括号
//! (undo 分组:事务内回写并入一条 multi 命令,见
//! [`crate::instance::Instance::edit_begin`])。
use std::collections::HashMap;
use std::ffi::{c_char, c_double, c_int, c_void, CStr, CString};
use std::sync::Mutex;
use crate::descriptor::EffectDescriptor;
use crate::instance::Instance;
use crate::param::{ChangeReason, ParamDef, ParamInstance, ParamKind, ParamValue};
use crate::suites::status;
use crate::suites::tag;
/// 函数表布局(与 SDK `OfxParameterSuiteV1` 一致;字段序以
/// SDK 为准)。
#[repr(C)]
pub struct ParameterSuiteV1 {
/// paramDefinedescribe 期间):定义参数。
pub param_define: unsafe extern "C" fn(*mut c_void, *const c_char, *const c_char, *mut *mut c_void) -> c_int,
/// paramGetHandle
pub param_get_handle: unsafe extern "C" fn(*mut c_void, *const c_char, *mut *mut c_void, *mut *mut c_void) -> c_int,
/// paramSetGetPropertySet
pub param_set_get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
/// paramGetPropertySet
pub param_get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
/// paramGetValue(变长出口参数按类型解释;入口在 C shim)
pub param_get_value: unsafe extern "C" fn(*mut c_void, ...) -> c_int,
/// paramGetValueAtTime
pub param_get_value_at_time: unsafe extern "C" fn(*mut c_void, c_double, ...) -> c_int,
/// paramGetDerivative
pub param_get_derivative: unsafe extern "C" fn(*mut c_void, c_double, ...) -> c_int,
/// paramGetIntegral
pub param_get_integral: unsafe extern "C" fn(*mut c_void, c_double, c_double, ...) -> c_int,
/// paramSetValue(触发 instanceChanged
pub param_set_value: unsafe extern "C" fn(*mut c_void, ...) -> c_int,
/// paramSetValueAtTime
pub param_set_value_at_time: unsafe extern "C" fn(*mut c_void, c_double, ...) -> c_int,
/// paramGetNumKeys
pub param_get_num_keys: unsafe extern "C" fn(*mut c_void, *mut c_int) -> c_int,
/// paramGetKeyTime
pub param_get_key_time: unsafe extern "C" fn(*mut c_void, c_int, *mut c_double) -> c_int,
/// paramGetKeyIndex
pub param_get_key_index: unsafe extern "C" fn(*mut c_void, c_double, c_int, *mut c_int) -> c_int,
/// paramDeleteKey
pub param_delete_key: unsafe extern "C" fn(*mut c_void, c_double) -> c_int,
/// paramDeleteAllKeys
pub param_delete_all_keys: unsafe extern "C" fn(*mut c_void) -> c_int,
/// paramCopy
pub param_copy: unsafe extern "C" fn(*mut c_void, *mut c_void, c_double, c_double, *const c_void) -> c_int,
/// paramEditBegin / paramEditEnd(编辑事务括号)
pub param_edit_begin: unsafe extern "C" fn(*mut c_void) -> c_int,
/// paramEditEnd
pub param_edit_end: unsafe extern "C" fn(*mut c_void) -> c_int,
}
// ---- 句柄解析 -----------------------------------------------------------
/// param-set handledescribe 期是效果描述符,实例期是实例
/// getParamSet 返回与 effect 相同的 handle,见 image_effect suite)。
enum ParamSetRef<'a> {
Descriptor(&'a mut EffectDescriptor),
Instance(&'a Instance),
}
/// param handledescribe 期是定义(值=默认值),实例期是实例。
enum ParamRef<'a> {
Def(&'a ParamDef),
Instance(&'a ParamInstance),
}
/// 解析 param-seteffect)句柄。空指针/标签不符 → BadHandle。
fn resolve_param_set(handle: *mut c_void) -> Result<ParamSetRef<'static>, c_int> {
if handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
// 两种对象 props 均在偏移 0(句柄约定)。
unsafe {
match tag::kind(handle) {
tag::DESCRIPTOR => {
Ok(ParamSetRef::Descriptor(&mut *(tag::strip(handle) as *mut EffectDescriptor)))
}
tag::INSTANCE => Ok(ParamSetRef::Instance(&*(tag::strip(handle) as *const Instance))),
_ => Err(status::ERR_BAD_HANDLE),
}
}
}
/// 解析 param 句柄。
fn resolve_param(handle: *mut c_void) -> Result<ParamRef<'static>, c_int> {
if handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe {
match tag::kind(handle) {
tag::PARAM_DEF => Ok(ParamRef::Def(&*(tag::strip(handle) as *const ParamDef))),
tag::PARAM_INSTANCE => {
Ok(ParamRef::Instance(&*(tag::strip(handle) as *const ParamInstance)))
}
_ => Err(status::ERR_BAD_HANDLE),
}
}
}
/// 属性名(空指针/非 UTF-8 → ErrValue;防御性)。
unsafe fn c_name<'a>(name: *const c_char) -> Result<&'a str, c_int> {
if name.is_null() {
return Err(status::ERR_VALUE);
}
unsafe { CStr::from_ptr(name) }
.to_str()
.map_err(|_| status::ERR_VALUE)
}
/// 公共入口模板:panic 兜底。
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f))
.map_or_else(|_| status::FAILED, |r| r.map_or_else(|c| c, |()| status::OK))
}
// ---- 变长参数实现(C shim 转发)----------------------------------------
impl ParamKind {
fn from_i32(v: c_int) -> Option<ParamKind> {
Some(match v {
1 => ParamKind::Int,
2 => ParamKind::Int2,
3 => ParamKind::Int3,
4 => ParamKind::Double,
5 => ParamKind::Double2,
6 => ParamKind::Double3,
7 => ParamKind::Bool,
8 => ParamKind::Choice,
9 => ParamKind::Rgb,
10 => ParamKind::Rgba,
11 => ParamKind::Str,
12 => ParamKind::StrChoice,
_ => return None,
})
}
}
/// 按 kind 把 ParamValue 写进类型化 out(数值类)。
fn write_value(v: &ParamValue, kind: ParamKind, out: *mut c_void) -> Result<(), c_int> {
let err = || Err(status::FAILED);
unsafe {
match kind {
ParamKind::Int => match v {
ParamValue::Int(a, _) => {
*(out as *mut c_int) = a[0];
Ok(())
}
_ => err(),
},
ParamKind::Int2 | ParamKind::Int3 => match v {
ParamValue::Int(a, dim) => {
let n = if kind == ParamKind::Int2 { 2 } else { 3 };
let dst = std::slice::from_raw_parts_mut(out as *mut c_int, n);
for (d, s) in dst.iter_mut().zip(a.iter()).take(n) {
*d = *s;
}
let _ = dim;
Ok(())
}
_ => err(),
},
ParamKind::Double => match v {
ParamValue::Double(d, _) => {
*(out as *mut f64) = d[0];
Ok(())
}
_ => err(),
},
ParamKind::Double2 | ParamKind::Double3 => match v {
ParamValue::Double(d, _) => {
let n = if kind == ParamKind::Double2 { 2 } else { 3 };
let dst = std::slice::from_raw_parts_mut(out as *mut f64, n);
for (dd, s) in dst.iter_mut().zip(d.iter()).take(n) {
*dd = *s;
}
Ok(())
}
_ => err(),
},
ParamKind::Bool => match v {
ParamValue::Bool(b) => {
*(out as *mut c_int) = *b as c_int;
Ok(())
}
_ => err(),
},
ParamKind::Choice => match v {
ParamValue::Choice(c) => {
*(out as *mut c_int) = *c;
Ok(())
}
_ => err(),
},
ParamKind::Rgb | ParamKind::Rgba => match v {
ParamValue::Color(c, _) => {
let n = if kind == ParamKind::Rgb { 3 } else { 4 };
let dst = std::slice::from_raw_parts_mut(out as *mut f64, n);
for (dd, s) in dst.iter_mut().zip(c.iter()).take(n) {
*dd = *s;
}
Ok(())
}
_ => err(),
},
ParamKind::Str | ParamKind::StrChoice => {
// 字符串走 ofx_param_get_string_impl(内驻指针)。
err()
}
}
}
}
/// 按 kind 从类型化 input 构造 ParamValue(数值类)。
fn read_value(kind: ParamKind, input: *const c_void) -> Option<ParamValue> {
unsafe {
match kind {
ParamKind::Int => Some(ParamValue::Int([*(input as *const c_int), 0, 0], 1)),
ParamKind::Int2 => {
let a = *(input as *const [c_int; 2]);
Some(ParamValue::Int([a[0], a[1], 0], 2))
}
ParamKind::Int3 => {
let a = *(input as *const [c_int; 3]);
Some(ParamValue::Int([a[0], a[1], a[2]], 3))
}
ParamKind::Double => Some(ParamValue::Double([*(input as *const f64), 0.0, 0.0], 1)),
ParamKind::Double2 => {
let a = *(input as *const [f64; 2]);
Some(ParamValue::Double([a[0], a[1], 0.0], 2))
}
ParamKind::Double3 => {
let a = *(input as *const [f64; 3]);
Some(ParamValue::Double([a[0], a[1], a[2]], 3))
}
ParamKind::Bool => Some(ParamValue::Bool(*(input as *const c_int) != 0)),
ParamKind::Choice => Some(ParamValue::Choice(*(input as *const c_int))),
ParamKind::Rgb => {
let a = *(input as *const [f64; 3]);
Some(ParamValue::Color([a[0], a[1], a[2], 0.0], 3))
}
ParamKind::Rgba => {
let a = *(input as *const [f64; 4]);
Some(ParamValue::Color([a[0], a[1], a[2], a[3]], 4))
}
ParamKind::Str | ParamKind::StrChoice => None,
}
}
}
/// 取"当前值"(实例期=当前值,describe 期=默认值)。
fn value_of(r: &ParamRef) -> ParamValue {
match r {
ParamRef::Instance(p) => p.get(),
ParamRef::Def(d) => d.default.clone(),
}
}
/// param → 所属 instance 的回写定位表(instanceChanged 用)。
/// `// TODO(instance)`createInstance 后由宿主登记,destroy 时摘除。
static PARAM_OWNER: std::sync::LazyLock<Mutex<HashMap<usize, usize>>> =
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
/// 登记参数 → 实例(实例化路径调用)。
pub(crate) fn register_param_owner(param_props: usize, instance_props: usize) {
PARAM_OWNER
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(param_props, instance_props);
}
/// 摘除某实例的全部参数登记(destroy 路径调用)。
pub(crate) fn unregister_params_of(instance_props: usize) {
PARAM_OWNER
.lock()
.unwrap_or_else(|e| e.into_inner())
.retain(|_, owner| *owner != instance_props);
}
/// paramSetValue 成功后的 instanceChanged 触发(HS:
/// ofxhParam.cpp:1991-1994 `paramChangedByPlugin`)。未登记实例时
/// no-op(未绑定节点的场景本就 no-op)。
fn notify_changed(param_props: usize, name: &str) {
let owner = PARAM_OWNER
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&param_props)
.copied();
if let Some(inst_addr) = owner {
// instance 的 props 在偏移 0(句柄约定)。
let inst = unsafe { &*(inst_addr as *const Instance) };
crate::param::notify_instance_changed(inst, name, ChangeReason::PluginEdited);
}
}
/// 参数类型查询(C shim 调用):未知/非法句柄 → 0(dispatch 失败)。
#[no_mangle]
pub unsafe extern "C" fn ofx_param_kind_of(param: *mut c_void) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
match resolve_param(param) {
Ok(r) => match &r {
ParamRef::Instance(p) => p.def.kind().map_or(0, |k| k as c_int),
ParamRef::Def(d) => d.kind().map_or(0, |k| k as c_int),
},
Err(_) => 0,
}
}))
.unwrap_or(0)
}
/// 数值类 getC shim 转发):`out` 为按 kind 的类型化数组。
#[no_mangle]
pub unsafe extern "C" fn ofx_param_get_impl(
param: *mut c_void,
kind: c_int,
out: *mut c_void,
) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_VALUE);
}
let k = ParamKind::from_i32(kind).ok_or(status::ERR_UNKNOWN)?;
let r = resolve_param(param)?;
write_value(&value_of(&r), k, out)?;
Ok(())
})
}
/// 字符串 get(内驻指针:指向实例存储 / 定义默认值;下次 set 前
/// 有效——与属性 suite 的字符串指针契约一致)。
#[no_mangle]
pub unsafe extern "C" fn ofx_param_get_string_impl(
param: *mut c_void,
out: *mut *mut c_char,
) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_VALUE);
}
let ptr: *const c_char = match resolve_param(param)? {
ParamRef::Instance(p) => p.string_ptr().ok_or(status::FAILED)?,
ParamRef::Def(d) => match &d.default {
ParamValue::String(s) | ParamValue::StrChoice(s) => s.as_ptr(),
_ => return Err(status::FAILED),
},
};
unsafe { *out = ptr as *mut c_char };
Ok(())
})
}
/// 数值类 setC shim 转发):`input` 为按 kind 的类型化数组。
/// describe 期(ParamDef)→ BadHandleHS: paramSetValue 的
/// verifyMagic 对 descriptor 失败)。
#[no_mangle]
pub unsafe extern "C" fn ofx_param_set_impl(
param: *mut c_void,
kind: c_int,
input: *const c_void,
) -> c_int {
caught(|| {
if input.is_null() {
return Err(status::ERR_VALUE);
}
let k = ParamKind::from_i32(kind).ok_or(status::ERR_UNKNOWN)?;
let value = read_value(k, input).ok_or(status::FAILED)?;
let (name, props_addr) = match resolve_param(param)? {
ParamRef::Instance(p) => {
p.set_ofx(value);
(p.def.name.clone(), &p.props as *const _ as usize)
}
ParamRef::Def(_) => return Err(status::ERR_BAD_HANDLE),
};
notify_changed(props_addr, &name);
Ok(())
})
}
/// 字符串 set。
#[no_mangle]
pub unsafe extern "C" fn ofx_param_set_string_impl(
param: *mut c_void,
input: *const c_char,
) -> c_int {
caught(|| {
if input.is_null() {
return Err(status::ERR_VALUE);
}
let s = CString::new(unsafe { CStr::from_ptr(input) }.to_bytes()).unwrap();
let (name, props_addr) = match resolve_param(param)? {
ParamRef::Instance(p) => {
p.set_ofx(ParamValue::String(s));
(p.def.name.clone(), &p.props as *const _ as usize)
}
ParamRef::Def(_) => return Err(status::ERR_BAD_HANDLE),
};
notify_changed(props_addr, &name);
Ok(())
})
}
/// derivative/integral:第 1 期无动画 → MissingHostFeature
/// C shim 不消费 va_args 直接转发)。
#[no_mangle]
pub unsafe extern "C" fn ofx_param_missing_feature_impl(param: *mut c_void) -> c_int {
caught(|| {
// handle 合法性仍检查(空/坏句柄 → BadHandle)。
let _ = resolve_param(param)?;
Err(status::ERR_MISSING_HOST_FEATURE)
})
}
// ---- 非变长入口 ----------------------------------------------------------
/// paramDefinedescribe 期定义参数(未定义类型/parametric →
/// UnsupportedHS:1704;重复名 → HS 允许,原样再建一条——宿主以
/// 首个为准,与 HS 的 map 覆盖语义一致)。
unsafe extern "C" fn param_define(
param_set: *mut c_void,
param_type: *const c_char,
name: *const c_char,
property_set: *mut *mut c_void,
) -> c_int {
caught(|| {
if property_set.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe { *property_set = std::ptr::null_mut() };
let t = unsafe { c_name(param_type)? };
let n = unsafe { c_name(name)? };
// 仅 describe 期(HS: SetDescriptor 专属,实例期 dynamic_cast
// 失败 → BadHandle)。
let desc = match resolve_param_set(param_set)? {
ParamSetRef::Descriptor(d) => d,
ParamSetRef::Instance(_) => return Err(status::ERR_BAD_HANDLE),
};
// 类型合法性(kind_of_type 之外还有无值类与 parametric)。
let valid = crate::param::kind_of_type(t).is_some()
|| matches!(
t,
crate::param::TYPE_BYTES
| crate::param::TYPE_CUSTOM
| crate::param::TYPE_PUSHBUTTON
| crate::param::TYPE_GROUP
| crate::param::TYPE_PAGE
);
if !valid || t == crate::param::TYPE_PARAMETRIC {
return Err(status::ERR_UNSUPPORTED);
}
let def = ParamDef::new(n, t);
desc.params.push(Box::new(def));
// 地址必须取自已入盒的对象(栈上临时变量在 push 后失效,
// 句柄会指向死内存——Box 负载地址在 Vec 重分配时稳定)。
let def = desc.params.last().expect("just pushed");
let addr = &def.props as *const _ as usize;
unsafe { *property_set = tag::make(addr as *const crate::property::PropertySet, tag::PARAM_DEF) };
Ok(())
})
}
/// paramGetHandle:按名查(未找到 → ErrUnknownHS:1758)。
unsafe extern "C" fn param_get_handle(
param_set: *mut c_void,
name: *const c_char,
param: *mut *mut c_void,
property_set: *mut *mut c_void,
) -> c_int {
caught(|| {
if param.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe { *param = std::ptr::null_mut() };
let n = unsafe { c_name(name)? };
let handle = match resolve_param_set(param_set)? {
ParamSetRef::Descriptor(d) => {
let def = d.param(n).ok_or(status::ERR_UNKNOWN)?;
let addr = &def.props as *const _ as usize;
tag::make(addr as *const crate::property::PropertySet, tag::PARAM_DEF)
}
ParamSetRef::Instance(i) => {
let p = i.params.find(n).ok_or(status::ERR_UNKNOWN)?;
let addr = &p.props as *const _ as usize;
tag::make(addr as *const crate::property::PropertySet, tag::PARAM_INSTANCE)
}
};
unsafe { *param = handle };
if !property_set.is_null() {
// props 在偏移 0param handle 与 props handle 同值。
unsafe { *property_set = handle };
}
Ok(())
})
}
/// paramSetGetPropertySetparam-set 的属性集(= effect handle 本体)。
unsafe extern "C" fn param_set_get_property_set(
param_set: *mut c_void,
property_set: *mut *mut c_void,
) -> c_int {
caught(|| {
if property_set.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let _ = resolve_param_set(param_set)?;
unsafe { *property_set = param_set };
Ok(())
})
}
/// paramGetPropertySetparam 的属性集(= param handle 本体)。
unsafe extern "C" fn param_get_property_set(
param: *mut c_void,
property_set: *mut *mut c_void,
) -> c_int {
caught(|| {
if property_set.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
let _ = resolve_param(param)?;
unsafe { *property_set = param };
Ok(())
})
}
/// paramGetNumKeys:第 1 期无动画 → 恒 0。
unsafe extern "C" fn param_get_num_keys(param: *mut c_void, count: *mut c_int) -> c_int {
caught(|| {
let _ = resolve_param(param)?;
if count.is_null() {
return Err(status::ERR_VALUE);
}
unsafe { *count = 0 };
Ok(())
})
}
/// paramGetKeyTime:无关键帧 → BadIndex。
unsafe extern "C" fn param_get_key_time(param: *mut c_void, _index: c_int, _time: *mut c_double) -> c_int {
caught(|| {
let _ = resolve_param(param)?;
Err(status::ERR_BAD_INDEX)
})
}
/// paramGetKeyIndex:无关键帧 → BadIndex。
unsafe extern "C" fn param_get_key_index(
param: *mut c_void,
_time: c_double,
_direction: c_int,
_index: *mut c_int,
) -> c_int {
caught(|| {
let _ = resolve_param(param)?;
Err(status::ERR_BAD_INDEX)
})
}
/// paramDeleteKey / paramDeleteAllKeys:无关键帧 → OK no-op。
unsafe extern "C" fn param_delete_key(param: *mut c_void, _time: c_double) -> c_int {
caught(|| {
let _ = resolve_param(param)?;
Ok(())
})
}
unsafe extern "C" fn param_delete_all_keys(param: *mut c_void) -> c_int {
caught(|| {
let _ = resolve_param(param)?;
Ok(())
})
}
/// paramCopy:第 1 期无关键帧可拷 → OK no-op。
unsafe extern "C" fn param_copy(
dst: *mut c_void,
src: *mut c_void,
_dst_time: c_double,
_src_time: c_double,
_key_range: *const c_void,
) -> c_int {
caught(|| {
let _ = resolve_param(dst)?;
let _ = resolve_param(src)?;
Ok(())
})
}
/// paramEditBegin:进入编辑事务(undo 分组)。解析 param 句柄后经
/// PARAM_OWNER 定位所属实例并递增其事务深度;未登记实例时 no-op
/// (手工构造的实例没有宿主注册,edit 括号退化为无分组)。
unsafe extern "C" fn param_edit_begin(param: *mut c_void) -> c_int {
caught(|| {
let addr = match resolve_param(param)? {
ParamRef::Instance(p) => &p.props as *const _ as usize,
ParamRef::Def(_) => return Err(status::ERR_BAD_HANDLE),
};
if let Some(owner) = PARAM_OWNER
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&addr)
.copied()
{
let inst = unsafe { &*(owner as *const Instance) };
inst.edit_begin();
}
Ok(())
})
}
/// paramEditEnd:退出编辑事务;最外层结束时把累积的 multi 命令
/// redo 生效并释放(见 [`crate::instance::Instance::edit_end`])。
unsafe extern "C" fn param_edit_end(param: *mut c_void) -> c_int {
caught(|| {
let addr = match resolve_param(param)? {
ParamRef::Instance(p) => &p.props as *const _ as usize,
ParamRef::Def(_) => return Err(status::ERR_BAD_HANDLE),
};
if let Some(owner) = PARAM_OWNER
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&addr)
.copied()
{
let inst = unsafe { &*(owner as *const Instance) };
inst.edit_end();
}
Ok(())
})
}
extern "C" {
fn ofx_param_get_value_shim(param: *mut c_void, ...) -> c_int;
fn ofx_param_get_value_at_time_shim(param: *mut c_void, time: c_double, ...) -> c_int;
fn ofx_param_set_value_shim(param: *mut c_void, ...) -> c_int;
fn ofx_param_set_value_at_time_shim(param: *mut c_void, time: c_double, ...) -> c_int;
fn ofx_param_get_derivative_shim(param: *mut c_void, time: c_double, ...) -> c_int;
fn ofx_param_get_integral_shim(
param: *mut c_void,
time1: c_double,
time2: c_double,
...
) -> c_int;
}
/// 静态函数表实例。
pub fn suite_v1() -> &'static ParameterSuiteV1 {
static SUITE: std::sync::OnceLock<ParameterSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| ParameterSuiteV1 {
param_define: param_define,
param_get_handle: param_get_handle,
param_set_get_property_set: param_set_get_property_set,
param_get_property_set: param_get_property_set,
param_get_value: ofx_param_get_value_shim,
param_get_value_at_time: ofx_param_get_value_at_time_shim,
param_get_derivative: ofx_param_get_derivative_shim,
param_get_integral: ofx_param_get_integral_shim,
param_set_value: ofx_param_set_value_shim,
param_set_value_at_time: ofx_param_set_value_at_time_shim,
param_get_num_keys: param_get_num_keys,
param_get_key_time: param_get_key_time,
param_get_key_index: param_get_key_index,
param_delete_key: param_delete_key,
param_delete_all_keys: param_delete_all_keys,
param_copy: param_copy,
param_edit_begin: param_edit_begin,
param_edit_end: param_edit_end,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use std::sync::Arc;
use crate::host::Plugin;
use crate::param::{ParamInstance, ParamSetInstance};
use crate::property::PropertySet;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
fn descriptor_handle(d: &EffectDescriptor) -> *mut c_void {
tag::make(&d.props as *const crate::property::PropertySet, tag::DESCRIPTOR)
}
/// 假插件(host::Plugin 的构造只为拿 Arc 喂给 Instancedescribe
/// 之外的字段不被本测试触碰)。
fn dummy_plugin(descriptor: EffectDescriptor) -> Arc<Plugin> {
unsafe extern "C" fn dummy_entry(
_: *const c_char,
_: *const c_void,
_: *mut c_void,
_: *mut c_void,
) -> c_int {
status::OK
}
Arc::new(Plugin {
identifier: "test.plugin".into(),
version: (1, 0),
bundle_path: std::path::PathBuf::new(),
contexts: vec![],
descriptor,
lib: std::ptr::null_mut(),
entry: dummy_entry,
ofx_plugin: std::ptr::null_mut(),
})
}
fn instance_handle(i: &Instance) -> *mut c_void {
tag::make(&i.props as *const crate::property::PropertySet, tag::INSTANCE)
}
/// 用 paramDefine 建一个实例(describe 产物 → createInstance)。
fn make_instance() -> (Arc<Instance>, *mut c_void) {
let mut desc = EffectDescriptor::new();
let s = suite_v1();
let dhandle = descriptor_handle(&desc);
unsafe {
for (t, n) in [
("OfxParamTypeInteger", "gain"),
("OfxParamTypeDouble", "opacity"),
("OfxParamTypeDouble2D", "pos"),
("OfxParamTypeRGB", "color"),
("OfxParamTypeBoolean", "enabled"),
("OfxParamTypeString", "label"),
] {
let t = cs(t);
let n = cs(n);
let mut ph: *mut c_void = std::ptr::null_mut();
assert_eq!((s.param_define)(dhandle, t.as_ptr(), n.as_ptr(), &mut ph), 0);
}
}
let params = ParamSetInstance {
params: desc
.params
.iter()
.map(|d| Box::new(ParamInstance::from_def((**d).clone())))
.collect(),
};
let plugin = dummy_plugin(desc);
let inst = Arc::new(Instance {
props: PropertySet::new(),
plugin,
context: "OfxImageEffectContextFilter".into(),
params,
clips: vec![],
node_identity: std::sync::atomic::AtomicUsize::new(0),
destroyed: std::sync::atomic::AtomicBool::new(false),
sequence_range: std::sync::Mutex::new(None),
progress_cb: std::sync::Mutex::new(None),
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
});
(inst.clone(), instance_handle(&inst))
}
#[test]
fn describe_define_and_get() {
let mut desc = EffectDescriptor::new();
let s = suite_v1();
let handle = descriptor_handle(&desc);
let t = cs("OfxParamTypeDouble");
let n = cs("opacity");
let mut ph: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.param_define)(handle, t.as_ptr(), n.as_ptr(), &mut ph), 0);
}
assert!(!ph.is_null());
assert_eq!(tag::kind(ph), tag::PARAM_DEF);
// describe 期 paramGetValue:默认值 0.0。
let mut v = 99.0;
unsafe {
assert_eq!((s.param_get_value)(ph, &mut v), 0);
}
assert_eq!(v, 0.0);
// describe 期 set → BadHandleHS: verifyMagic 对 descriptor 失败)。
unsafe {
assert_eq!((s.param_set_value)(ph, 42.0), status::ERR_BAD_HANDLE);
}
// paramGetHandle 按名取(props handle 同值)。
let mut ph2: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!(
(s.param_get_handle)(handle, n.as_ptr(), &mut ph2, std::ptr::null_mut()),
0
);
assert_eq!(ph, ph2);
}
// 未找到 → ErrUnknownHS:1758)。
let nope = cs("nope");
unsafe {
assert_eq!(
(s.param_get_handle)(handle, nope.as_ptr(), &mut ph2, std::ptr::null_mut()),
status::ERR_UNKNOWN
);
}
// 未定义类型 / parametric → ErrUnsupportedHS:1704)。
let bad = cs("OfxParamTypeBogus");
let par = cs("OfxParamTypeParametric");
unsafe {
assert_eq!(
(s.param_define)(handle, bad.as_ptr(), n.as_ptr(), &mut ph2),
status::ERR_UNSUPPORTED
);
assert_eq!(
(s.param_define)(handle, par.as_ptr(), n.as_ptr(), &mut ph2),
status::ERR_UNSUPPORTED
);
}
// 实例期 paramDefine → BadHandle。
let (_inst, ih) = make_instance();
unsafe {
assert_eq!(
(s.param_define)(ih, t.as_ptr(), n.as_ptr(), &mut ph2),
status::ERR_BAD_HANDLE
);
}
}
#[test]
fn instance_get_set_variadic_roundtrip() {
let (_inst, ih) = make_instance();
let s = suite_v1();
// 取各参数 handle。
let mut gain: *mut c_void = std::ptr::null_mut();
let mut opacity: *mut c_void = std::ptr::null_mut();
let mut pos: *mut c_void = std::ptr::null_mut();
let mut color: *mut c_void = std::ptr::null_mut();
let mut enabled: *mut c_void = std::ptr::null_mut();
let mut label: *mut c_void = std::ptr::null_mut();
unsafe {
for (n, out) in [
("gain", &mut gain),
("opacity", &mut opacity),
("pos", &mut pos),
("color", &mut color),
("enabled", &mut enabled),
("label", &mut label),
] {
let n = cs(n);
let r = (s.param_get_handle)(ih, n.as_ptr(), out, std::ptr::null_mut());
assert_eq!(r, 0, "gethandle {} failed: {}", n.to_str().unwrap(), r);
assert_eq!(tag::kind(*out), tag::PARAM_INSTANCE);
}
}
// Integer set/get。
unsafe {
assert_eq!((s.param_set_value)(gain, 7), 0);
}
let mut v = 0;
unsafe {
assert_eq!((s.param_get_value)(gain, &mut v), 0);
}
assert_eq!(v, 7);
// Double2D。
unsafe {
assert_eq!((s.param_set_value)(pos, 1.5, -2.5), 0);
}
let (mut x, mut y) = (0.0, 0.0);
unsafe {
assert_eq!((s.param_get_value)(pos, &mut x, &mut y), 0);
}
assert_eq!((x, y), (1.5, -2.5));
// RGB。
unsafe {
assert_eq!((s.param_set_value)(color, 0.1, 0.2, 0.3), 0);
}
let (mut r, mut g, mut b) = (0.0, 0.0, 0.0);
unsafe {
assert_eq!((s.param_get_value)(color, &mut r, &mut g, &mut b), 0);
}
assert_eq!((r, g, b), (0.1, 0.2, 0.3));
// Booleanint 0/1)。
unsafe {
assert_eq!((s.param_set_value)(enabled, 1), 0);
}
let mut e = 0;
unsafe {
assert_eq!((s.param_get_value)(enabled, &mut e), 0);
}
assert_eq!(e, 1);
// Stringset 后 get 返回内驻指针。
let hello = cs("hello");
unsafe {
assert_eq!((s.param_set_value)(label, hello.as_ptr()), 0);
}
let mut p: *mut c_char = std::ptr::null_mut();
unsafe {
assert_eq!((s.param_get_value)(label, &mut p), 0);
assert_eq!(CStr::from_ptr(p).to_bytes(), b"hello");
}
// AtTime == 当前值(无动画)。
let mut v2 = 0.0;
unsafe {
assert_eq!((s.param_get_value_at_time)(opacity, 12.0, &mut v2), 0);
assert_eq!(v2, 0.0);
assert_eq!((s.param_set_value_at_time)(opacity, 12.0, 0.75), 0);
}
let mut v3 = 0.0;
unsafe {
assert_eq!((s.param_get_value)(opacity, &mut v3), 0);
}
assert_eq!(v3, 0.75);
// keys:恒 0 / BadIndex。
let mut nkeys = -1;
unsafe {
assert_eq!((s.param_get_num_keys)(opacity, &mut nkeys), 0);
assert_eq!(nkeys, 0);
let mut kt = 0.0;
assert_eq!((s.param_get_key_time)(opacity, 0, &mut kt), status::ERR_BAD_INDEX);
}
// derivative/integral → MissingHostFeature。
let mut dv = 0.0;
unsafe {
assert_eq!(
(s.param_get_derivative)(opacity, 1.0, &mut dv),
status::ERR_MISSING_HOST_FEATURE
);
}
// editBegin/End、deleteKey(s)、copyno-op OK。
unsafe {
assert_eq!((s.param_edit_begin)(opacity), 0);
assert_eq!((s.param_edit_end)(opacity), 0);
assert_eq!((s.param_delete_key)(opacity, 1.0), 0);
assert_eq!((s.param_delete_all_keys)(opacity), 0);
assert_eq!((s.param_copy)(opacity, opacity, 1.0, 1.0, std::ptr::null()), 0);
}
}
}
+168
View File
@@ -0,0 +1,168 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! OfxProgressSuite v1/v2:进度 → 当前渲染的
//! [`crate::progress::ProgressReporter`]。
//!
//! 定位方式:TLSrender 驱动在调用插件 action 前
//! [`set_current`]action 返回后清除)——进度只在渲染期内有意义,
//! 不设全局状态(progress.rs 文档)。
//! 语义:update 返回 false(取消)→ kOfxStatReplyNo
//! HS: ofxhImageEffect.cpp gProgressSuite);无报告器(渲染外
//! 调用)→ 静默 OK。start/end 为括号,OK。
use std::ffi::{c_char, c_double, c_int, c_void};
use crate::progress::ProgressReporter;
use crate::suites::status;
thread_local! {
static CURRENT: std::cell::RefCell<Option<ProgressReporter>> = const { std::cell::RefCell::new(None) };
}
/// 设置/清除当前报告器(render 驱动调用;公开:render 驱动在
/// host 层,测试也直接注入)。
pub fn set_current(reporter: Option<ProgressReporter>) {
CURRENT.with(|c| *c.borrow_mut() = reporter);
}
/// 读取当前报告器的取消状态(image effect suite 的 abort 用)。
pub(crate) fn is_cancelled() -> bool {
CURRENT.with(|c| c.borrow().as_ref().is_some_and(|r| r.is_cancelled()))
}
/// 进度更新:取消 → kOfxStatReplyNo;无报告器/未取消 → OK。
fn progress_update(progress: f64) -> c_int {
let cancelled = CURRENT.with(|c| match c.borrow().as_ref() {
Some(r) => !r.update(progress),
None => false,
});
if cancelled {
status::REPLY_NO
} else {
status::OK
}
}
/// 公共入口模板:panic 兜底。
fn caught(f: impl FnOnce() -> c_int) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or(status::FAILED)
}
/// 函数表布局(OfxProgressSuiteV1)。
#[repr(C)]
pub struct ProgressSuiteV1 {
/// progressStart
pub start: unsafe extern "C" fn(*mut c_void, *const c_char) -> c_int,
/// progressUpdate(返回非 kOfxStatOK 即取消)
pub update: unsafe extern "C" fn(*mut c_void, c_double) -> c_int,
/// progressEnd
pub end: unsafe extern "C" fn(*mut c_void) -> c_int,
}
/// 函数表布局(OfxProgressSuiteV2start 带 label + message)。
#[repr(C)]
pub struct ProgressSuiteV2 {
/// progressStart
pub start: unsafe extern "C" fn(*mut c_void, *const c_char, *const c_char) -> c_int,
/// progressUpdate
pub update: unsafe extern "C" fn(*mut c_void, c_double) -> c_int,
/// progressEnd
pub end: unsafe extern "C" fn(*mut c_void) -> c_int,
}
/// progressStart:括号起点,OKlabel/message 留作未来 UI 展示,
/// 第 1 期不建模)。
unsafe extern "C" fn progress_start_v1(_handle: *mut c_void, _label: *const c_char) -> c_int {
caught(|| status::OK)
}
unsafe extern "C" fn progress_update_v1(_handle: *mut c_void, progress: c_double) -> c_int {
caught(|| progress_update(progress))
}
unsafe extern "C" fn progress_end_v1(_handle: *mut c_void) -> c_int {
caught(|| status::OK)
}
unsafe extern "C" fn progress_start_v2(
_handle: *mut c_void,
_label: *const c_char,
_message: *const c_char,
) -> c_int {
caught(|| status::OK)
}
unsafe extern "C" fn progress_update_v2(_handle: *mut c_void, progress: c_double) -> c_int {
caught(|| progress_update(progress))
}
unsafe extern "C" fn progress_end_v2(_handle: *mut c_void) -> c_int {
caught(|| status::OK)
}
/// 静态函数表实例(v1)。
pub fn suite_v1() -> &'static ProgressSuiteV1 {
static SUITE: std::sync::OnceLock<ProgressSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| ProgressSuiteV1 {
start: progress_start_v1,
update: progress_update_v1,
end: progress_end_v1,
})
}
/// 静态函数表实例(v2)。
pub fn suite_v2() -> &'static ProgressSuiteV2 {
static SUITE: std::sync::OnceLock<ProgressSuiteV2> = std::sync::OnceLock::new();
SUITE.get_or_init(|| ProgressSuiteV2 {
start: progress_start_v2,
update: progress_update_v2,
end: progress_end_v2,
})
}
#[cfg(test)]
mod tests {
use super::*;
/// 取消型出口(头文件契约:非 0 = 中止)。
unsafe extern "C" fn cancel_cb(_p: f64, _userdata: *mut c_void) -> c_int {
1
}
/// 静默:update 恒 OK;带取消回调:update → REPLY_NO 且 abort
/// 查询为真(粘滞)。
#[test]
fn update_cancel_and_abort() {
// 无报告器(渲染外):静默 OK。
set_current(None);
let s = suite_v1();
unsafe {
assert_eq!((s.update)(std::ptr::null_mut(), 0.5), status::OK);
assert_eq!((s.start)(std::ptr::null_mut(), std::ptr::null()), status::OK);
assert_eq!((s.end)(std::ptr::null_mut()), status::OK);
}
// 带取消回调:update → REPLY_NOis_cancelled 为真。
set_current(Some(unsafe { ProgressReporter::new(cancel_cb, std::ptr::null_mut()) }));
unsafe {
assert_eq!((s.update)(std::ptr::null_mut(), 0.5), status::REPLY_NO);
}
assert!(is_cancelled());
set_current(None);
assert!(!is_cancelled());
}
}
+619
View File
@@ -0,0 +1,619 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! OfxPropertySuite v1:属性读写。
//!
//! 语义逐条对照 HS: ofxhPropertySuite.cpp
//! - 读未定义属性 / 类型不符 → kOfxStatErrUnknownHS: propGet 的
//! `fetchTypedProperty` 失败路径,ofxhPropertySuite.cpp:787-794);
//! - 越界读写 → kOfxStatErrBadIndexHS: `getValueRaw`/
//! `setValue`ofxhPropertySuite.cpp:257/284);
//! - propGetN 拷贝 min(count, dimension) 个元素,不报错
//! HS: `getValueNRaw`ofxhPropertySuite.cpp:271-280);
//! - propSetN 维度变化时整体替换:本 crate 的属性维度始终跟随数组
//! 长度(HS 对固定维度属性 count > dimension 会返回
//! kOfxStatErrBadIndexofxhPropertySuite.cpp:299——本 crate 不区分
//! 固定/可变维度,如实更宽容;三个系统 bundle 均以正确 count 调用,
//! 无行为分歧);
//! - 只读属性:SDK 无 kOfxStatErrReadOnlyHostSupport 也不在 suite
//! 层拦截写(`_pluginReadOnly` 仅供宿主内部逻辑查询)——本实现
//! 如实不拦截;若日后需要,在 PropertySet 增加只读标记再评。
//! - propResetHostSupport 恢复到 define 时默认值
//! ofxhPropertySuite.cpp:330);本 crate 不保存默认值快照,
//! 第 1 期明确返回 kOfxStatErrUnsupported(真话比静默 no-op 安全)。
use std::ffi::{c_char, c_double, c_int, c_void, CStr, CString};
use crate::property::{Property, PropertySet, Value};
use crate::suites::status;
/// 函数表布局(与 SDK `OfxPropertySuiteV1` 逐字段一致)。
#[repr(C)]
pub struct PropertySuiteV1 {
/// propSetPointer
pub set_pointer: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_void) -> c_int,
/// propSetString
pub set_string: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const c_char) -> c_int,
/// propSetDouble
pub set_double: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, c_double) -> c_int,
/// propSetInt
pub set_int: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, c_int) -> c_int,
/// propSetPointerN
pub set_pointer_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const *mut c_void) -> c_int,
/// propSetStringN
pub set_string_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const *const c_char) -> c_int,
/// propSetDoubleN
pub set_double_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const c_double) -> c_int,
/// propSetIntN
pub set_int_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const c_int) -> c_int,
/// propGetPointer
pub get_pointer: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut *mut c_void) -> c_int,
/// propGetString
pub get_string: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut *mut c_char) -> c_int,
/// propGetDouble
pub get_double: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_double) -> c_int,
/// propGetInt
pub get_int: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_int) -> c_int,
/// propGetPointerN
pub get_pointer_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut *mut c_void) -> c_int,
/// propGetStringN
pub get_string_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut *mut c_char) -> c_int,
/// propGetDoubleN
pub get_double_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_double) -> c_int,
/// propGetIntN
pub get_int_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_int) -> c_int,
/// propReset
pub reset: unsafe extern "C" fn(*mut c_void, *const c_char) -> c_int,
/// propGetDimension
pub get_dimension: unsafe extern "C" fn(*mut c_void, *const c_char, *mut c_int) -> c_int,
}
/// 属性值类别(suite 层的类型检查;对应 HS 的 `Property::TypeEnum`)。
#[derive(Clone, Copy, PartialEq, Eq)]
enum Kind {
Int,
Double,
Str,
Pointer,
}
impl Kind {
fn of(v: &Value) -> Kind {
match v {
Value::Int(_) => Kind::Int,
Value::Double(_) => Kind::Double,
Value::String(_) => Kind::Str,
Value::Pointer(_) => Kind::Pointer,
}
}
}
/// 属性 suite 公共入口模板:解句柄 + panic 兜底。
///
/// 句柄按 [`crate::suites::tag`] 约定:低 3 位是对象种类标签,地址
/// 即对象 props 字段(偏移 0);剥标签后可直接当 PropertySet 用。
/// 裸 PropertySet 指针(标签 0,宿主内部/测试直传)原样通过。
///
/// `# Safety``handle` 必须指向活的 `PropertySet` 或已注册对象
/// (suite 生命周期契约:宿主对象先于 suite 调用创建,后于全部调用
/// 销毁)。
unsafe fn caught(handle: *mut c_void, f: impl FnOnce(&PropertySet) -> Result<(), c_int>) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if handle.is_null() {
return status::ERR_BAD_HANDLE;
}
let set = unsafe { &*crate::suites::tag::strip(handle) };
f(set).map_or_else(|code| code, |()| status::OK)
}))
.unwrap_or(status::FAILED)
}
/// 属性名:空指针 / 非 UTF-8 → kOfxStatErrValue(防御性;HostSupport
/// 直接解引用会崩)。
///
/// `# Safety``name` 必须是有效的 NUL 结尾 C 字符串(插件契约)。
unsafe fn c_name<'a>(name: *const c_char) -> Result<&'a str, c_int> {
if name.is_null() {
return Err(status::ERR_VALUE);
}
unsafe { CStr::from_ptr(name) }
.to_str()
.map_err(|_| status::ERR_VALUE)
}
/// 越界防护:`index` 转 usize(负数 → 越界错误)。
fn idx(index: c_int) -> Result<usize, c_int> {
usize::try_from(index).map_err(|_| status::ERR_BAD_INDEX)
}
/// 先类型后索引的取值(HS 顺序:`fetchTypedProperty` 在
/// `getValueRaw` 之前,ofxhPropertySuite.cpp:787/794/257)。
fn get_value<'a>(props: &'a [Property], name: &str, index: c_int, kind: Kind) -> Result<&'a Value, c_int> {
let p = props.iter().find(|p| p.name == name).ok_or(status::ERR_UNKNOWN)?;
// 首元素代理整条属性的类型(宿主只定义同构数组;HS 是定义期
// 固定类型,等价)。
let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?;
if Kind::of(probe) != kind {
return Err(status::ERR_UNKNOWN);
}
p.values.get(idx(index)?).ok_or(status::ERR_BAD_INDEX)
}
// ---- propSet(单元素)-----------------------------------------------------
/// propSet 通用实现:类型不符/未定义 → Unknown;越界 → BadIndex。
fn set_value(set: &PropertySet, name: &str, index: c_int, value: Value) -> Result<(), c_int> {
set.with_locked(|props| {
let p = props.iter_mut().find(|p| p.name == name).ok_or(status::ERR_UNKNOWN)?;
let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?;
if Kind::of(probe) != Kind::of(&value) {
return Err(status::ERR_UNKNOWN);
}
// HS `setValue` 允许 index == size 时追加(ofxhPropertySuite.cpp:284);
// 本 crate 维度固定语义(扩容只能经 define)——越界一律 BadIndex。
let slot = p.values.get_mut(idx(index)?).ok_or(status::ERR_BAD_INDEX)?;
*slot = value;
Ok(())
})
}
unsafe extern "C" fn prop_set_pointer(
handle: *mut c_void,
name: *const c_char,
index: c_int,
value: *mut c_void,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
set_value(set, name, index, Value::Pointer(value))
})
}
}
unsafe extern "C" fn prop_set_string(
handle: *mut c_void,
name: *const c_char,
index: c_int,
value: *const c_char,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if value.is_null() {
return Err(status::ERR_VALUE);
}
// C 语义:截断到首个 NULCString::new 不会失败
//from_ptr 已保证 NUL 结尾)。
let s = CString::new(CStr::from_ptr(value).to_bytes()).unwrap();
set_value(set, name, index, Value::String(s))
})
}
}
unsafe extern "C" fn prop_set_double(
handle: *mut c_void,
name: *const c_char,
index: c_int,
value: c_double,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
set_value(set, name, index, Value::Double(value))
})
}
}
unsafe extern "C" fn prop_set_int(
handle: *mut c_void,
name: *const c_char,
index: c_int,
value: c_int,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
set_value(set, name, index, Value::Int(value))
})
}
}
// ---- propSetN(批量)------------------------------------------------------
/// 从 C 数组读出 `count` 个元素并整体写入;count != 现有维度时
/// 整体替换(HS `setValueN` 的 resize 语义,ofxhPropertySuite.cpp:299-311)。
fn set_values(set: &PropertySet, name: &str, count: c_int, values: Vec<Value>) -> Result<(), c_int> {
set.with_locked(|props| {
let p = props.iter_mut().find(|p| p.name == name).ok_or(status::ERR_UNKNOWN)?;
// count == 0 时无类型可探(HS 仍会做 fetchTypedProperty)。
if let Some(first) = p.values.first() {
if let Some(v) = values.first() {
if Kind::of(first) != Kind::of(v) {
return Err(status::ERR_UNKNOWN);
}
}
}
let count = idx(count)?;
if count != p.values.len() {
// 维度变化:整体替换(HS 是 resize + 逐位写,等价)。
p.values = values;
} else {
for (i, v) in values.into_iter().enumerate() {
p.values[i] = v;
}
}
Ok(())
})
}
unsafe extern "C" fn prop_set_pointer_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
values: *const *mut c_void,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && values.is_null() {
return Err(status::ERR_VALUE);
}
let vals = (0..idx(count)?)
.map(|i| Value::Pointer(*values.add(i)))
.collect();
set_values(set, name, count, vals)
})
}
}
unsafe extern "C" fn prop_set_string_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
values: *const *const c_char,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && values.is_null() {
return Err(status::ERR_VALUE);
}
let mut vals = Vec::with_capacity(count.max(0) as usize);
for i in 0..idx(count)? {
let v = *values.add(i);
if v.is_null() {
return Err(status::ERR_VALUE);
}
vals.push(Value::String(
CString::new(CStr::from_ptr(v).to_bytes()).unwrap(),
));
}
set_values(set, name, count, vals)
})
}
}
unsafe extern "C" fn prop_set_double_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
values: *const c_double,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && values.is_null() {
return Err(status::ERR_VALUE);
}
let vals = (0..idx(count)?)
.map(|i| Value::Double(*values.add(i)))
.collect();
set_values(set, name, count, vals)
})
}
}
unsafe extern "C" fn prop_set_int_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
values: *const c_int,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && values.is_null() {
return Err(status::ERR_VALUE);
}
let vals = (0..idx(count)?)
.map(|i| Value::Int(*values.add(i)))
.collect();
set_values(set, name, count, vals)
})
}
}
// ---- propGet(单元素)-----------------------------------------------------
/// 非字符串标量读取(克隆值写出;字符串走内驻指针路径)。
fn get_scalar(set: &PropertySet, name: &str, index: c_int, kind: Kind) -> Result<Value, c_int> {
set.with_locked(|props| get_value(props, name, index, kind).cloned())
}
unsafe extern "C" fn prop_get_pointer(
handle: *mut c_void,
name: *const c_char,
index: c_int,
out: *mut *mut c_void,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if out.is_null() {
return Err(status::ERR_VALUE);
}
let v = get_scalar(set, name, index, Kind::Pointer)?;
match v {
Value::Pointer(p) => {
*out = p;
Ok(())
}
_ => Err(status::FAILED),
}
})
}
}
/// propGetString:写**内驻** C 串指针(OFX 契约:指向宿主内部存储,
/// 属性被下次修改前有效;克隆的 CString 指针会悬垂,绝不能给插件)。
fn get_string(set: &PropertySet, name: &str, index: c_int) -> Result<*mut c_char, c_int> {
set.with_locked(|props| {
match get_value(props, name, index, Kind::Str)? {
Value::String(s) => Ok(s.as_ptr() as *mut c_char),
_ => Err(status::FAILED),
}
})
}
unsafe extern "C" fn prop_get_string(
handle: *mut c_void,
name: *const c_char,
index: c_int,
out: *mut *mut c_char,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if out.is_null() {
return Err(status::ERR_VALUE);
}
*out = get_string(set, name, index)?;
Ok(())
})
}
}
unsafe extern "C" fn prop_get_double(
handle: *mut c_void,
name: *const c_char,
index: c_int,
out: *mut c_double,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if out.is_null() {
return Err(status::ERR_VALUE);
}
let v = get_scalar(set, name, index, Kind::Double)?;
match v {
Value::Double(d) => {
*out = d;
Ok(())
}
_ => Err(status::FAILED),
}
})
}
}
unsafe extern "C" fn prop_get_int(
handle: *mut c_void,
name: *const c_char,
index: c_int,
out: *mut c_int,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if out.is_null() {
return Err(status::ERR_VALUE);
}
let v = get_scalar(set, name, index, Kind::Int)?;
match v {
Value::Int(i) => {
*out = i;
Ok(())
}
_ => Err(status::FAILED),
}
})
}
}
// ---- propGetN(批量)------------------------------------------------------
/// 批量读取:拷贝 min(count, dimension) 个(HS `getValueNRaw`
/// ofxhPropertySuite.cpp:271-280,不报错)。
fn get_n(set: &PropertySet, name: &str, count: c_int, kind: Kind, write: impl Fn(&Value, usize)) -> Result<(), c_int> {
set.with_locked(|props| {
let p = props.iter().find(|p| p.name == name).ok_or(status::ERR_UNKNOWN)?;
let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?;
if Kind::of(probe) != kind {
return Err(status::ERR_UNKNOWN);
}
let n = idx(count)?.min(p.values.len());
for (i, v) in p.values.iter().take(n).enumerate() {
write(v, i);
}
Ok(())
})
}
unsafe extern "C" fn prop_get_pointer_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
out: *mut *mut c_void,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && out.is_null() {
return Err(status::ERR_VALUE);
}
get_n(set, name, count, Kind::Pointer, |v, i| match v {
Value::Pointer(p) => *out.add(i) = *p,
_ => {}
})
})
}
}
unsafe extern "C" fn prop_get_string_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
out: *mut *mut c_char,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && out.is_null() {
return Err(status::ERR_VALUE);
}
get_n(set, name, count, Kind::Str, |v, i| match v {
Value::String(s) => *out.add(i) = s.as_ptr() as *mut c_char,
_ => {}
})
})
}
}
unsafe extern "C" fn prop_get_double_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
out: *mut c_double,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && out.is_null() {
return Err(status::ERR_VALUE);
}
get_n(set, name, count, Kind::Double, |v, i| match v {
Value::Double(d) => *out.add(i) = *d,
_ => {}
})
})
}
}
unsafe extern "C" fn prop_get_int_n(
handle: *mut c_void,
name: *const c_char,
count: c_int,
out: *mut c_int,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if count > 0 && out.is_null() {
return Err(status::ERR_VALUE);
}
get_n(set, name, count, Kind::Int, |v, i| match v {
Value::Int(d) => *out.add(i) = *d,
_ => {}
})
})
}
}
// ---- propReset / propGetDimension -----------------------------------------
unsafe extern "C" fn prop_reset(handle: *mut c_void, name: *const c_char) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
// HS `reset()` 恢复到 define 时的默认值
// ofxhPropertySuite.cpp:330);本 crate 不保存默认值快照,
// 第 1 期明确不支持(见模块文档)。
let _ = (set, name);
Err(status::ERR_UNSUPPORTED)
})
}
}
unsafe extern "C" fn prop_get_dimension(
handle: *mut c_void,
name: *const c_char,
out: *mut c_int,
) -> c_int {
unsafe {
caught(handle, |set| {
let name = c_name(name)?;
if out.is_null() {
return Err(status::ERR_VALUE);
}
// HS: fetchProperty 失败(未定义)→ UnknownofxhPropertySuite.cpp:992)。
let dim = set.dimension(name);
if dim == 0 {
return Err(status::ERR_UNKNOWN);
}
*out = dim as c_int;
Ok(())
})
}
}
/// 静态函数表实例(fetch_suite 返回其地址)。
pub fn suite_v1() -> &'static PropertySuiteV1 {
static SUITE: std::sync::OnceLock<PropertySuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| PropertySuiteV1 {
set_pointer: prop_set_pointer,
set_string: prop_set_string,
set_double: prop_set_double,
set_int: prop_set_int,
set_pointer_n: prop_set_pointer_n,
set_string_n: prop_set_string_n,
set_double_n: prop_set_double_n,
set_int_n: prop_set_int_n,
get_pointer: prop_get_pointer,
get_string: prop_get_string,
get_double: prop_get_double,
get_int: prop_get_int,
get_pointer_n: prop_get_pointer_n,
get_string_n: prop_get_string_n,
get_double_n: prop_get_double_n,
get_int_n: prop_get_int_n,
reset: prop_reset,
get_dimension: prop_get_dimension,
})
}
+141
View File
@@ -0,0 +1,141 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! OfxTimeLineSuite v1:时间查询转发到当前渲染上下文
//! [`crate::suites::RenderCtx`]frame range 来自 clip 桥)。
//! 参照 HS: ofxhImageEffect.cpp gTimelineSuite。
//!
//! 无渲染上下文(渲染外调用)→ 时间 0 / 时间域 (0,0) 的 headless
//! 默认;gotoTime 第 1 期无时间线驱动 → OK no-op(渲染时间由驱动
//! 固定,见 [`crate::suites::set_render_ctx`])。
use std::ffi::{c_double, c_int, c_void};
use crate::suites::{render_ctx, status};
/// 函数表布局(OfxTimeLineSuiteV1)。
#[repr(C)]
pub struct TimeLineSuiteV1 {
/// getTime
pub get_time: unsafe extern "C" fn(*mut c_void, *mut c_double) -> c_int,
/// gotoTime
pub goto_time: unsafe extern "C" fn(*mut c_void, c_double) -> c_int,
/// getTimeBounds
pub get_time_bounds: unsafe extern "C" fn(*mut c_void, *mut c_double, *mut c_double) -> c_int,
}
/// 公共入口模板:handle 空检查 + panic 兜底。
unsafe fn caught(handle: *mut c_void, f: impl FnOnce() -> c_int) -> c_int {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if handle.is_null() {
return status::ERR_BAD_HANDLE;
}
f()
}))
.unwrap_or(status::FAILED)
}
unsafe extern "C" fn timeline_get_time(handle: *mut c_void, time: *mut c_double) -> c_int {
unsafe {
caught(handle, || {
if time.is_null() {
return status::ERR_VALUE;
}
// 渲染上下文缺省 → 0(headless 默认)。
*time = render_ctx().map_or(0.0, |c| c.time);
status::OK
})
}
}
/// gotoTime:第 1 期宿主不随插件移动时间线(渲染时间由驱动固定),
/// OK no-op。
unsafe extern "C" fn timeline_goto_time(handle: *mut c_void, _time: c_double) -> c_int {
unsafe { caught(handle, || status::OK) }
}
unsafe extern "C" fn timeline_get_time_bounds(
handle: *mut c_void,
min: *mut c_double,
max: *mut c_double,
) -> c_int {
unsafe {
caught(handle, || {
if min.is_null() || max.is_null() {
return status::ERR_VALUE;
}
let r = render_ctx().map_or((0.0, 0.0), |c| (c.range.min, c.range.max));
*min = r.0;
*max = r.1;
status::OK
})
}
}
/// 静态函数表实例。
pub fn suite_v1() -> &'static TimeLineSuiteV1 {
static SUITE: std::sync::OnceLock<TimeLineSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| TimeLineSuiteV1 {
get_time: timeline_get_time,
goto_time: timeline_goto_time,
get_time_bounds: timeline_get_time_bounds,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instance::{OfxRangeD, RenderScale};
use crate::suites::{RenderCtx, set_render_ctx};
#[test]
fn queries_forward_to_render_ctx() {
let s = suite_v1();
let handle = 0x10usize as *mut c_void;
let mut t = 0.0;
let mut min = 0.0;
let mut max = 0.0;
// 无上下文:0 / (0,0)。
unsafe {
assert_eq!((s.get_time)(handle, &mut t), status::OK);
assert_eq!(t, 0.0);
assert_eq!((s.get_time_bounds)(handle, &mut min, &mut max), status::OK);
assert_eq!((min, max), (0.0, 0.0));
}
// 有上下文:转发。
set_render_ctx(Some(RenderCtx {
time: 42.5,
scale: RenderScale { x: 1.0, y: 1.0 },
range: OfxRangeD { min: 10.0, max: 200.0 },
}));
unsafe {
assert_eq!((s.get_time)(handle, &mut t), status::OK);
assert_eq!(t, 42.5);
assert_eq!((s.get_time_bounds)(handle, &mut min, &mut max), status::OK);
assert_eq!((min, max), (10.0, 200.0));
assert_eq!((s.goto_time)(handle, 99.0), status::OK);
}
set_render_ctx(None);
// 空 handle / 空 out。
unsafe {
assert_eq!((s.get_time)(std::ptr::null_mut(), &mut t), status::ERR_BAD_HANDLE);
assert_eq!((s.get_time)(handle, std::ptr::null_mut()), status::ERR_VALUE);
}
}
}
+535
View File
@@ -0,0 +1,535 @@
// 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 层测试:param↔oaknode、clip↔oakrender、undo 打包。
//!
//! 依赖 liboaknode/liboakrender/liboakundo 的用例(standalone 树
//! 环境,外层 ctest 驱动)在 cargo 内以库内桩替代
//! `--features test-stubs`):桥调用面一致,桩提供节点值/命令/
//! 帧状态访问器(见 `bridge::{node,render,undo}::stub`)。未启用
//! 桩时同用例走 dlsym 缺失的降级路径(可解释错误 / no-op)。
//!
//! 系统级冒烟:[`system_misc_ofx_bundle_smoke`] 在
//! `/Library/OFX/Plugins/Misc.ofx.bundle` 存在时扫描真实 OFX 插件
//! 并 create_instanceabsent 时 skip)。
mod common;
use std::ffi::{c_int, c_void, CString};
use oakplugin::clip::ClipInstance;
use oakplugin::descriptor::ClipDescriptor;
use oakplugin::handle::CHandle;
use oakplugin::image::{BitDepth, Components, Image};
use oakplugin::instance::{Instance, OfxRectD, RenderScale};
use oakplugin::param::ParamInstance;
use oakplugin::property::PropertySet;
use oakplugin::suites::param::suite_v1 as param_suite;
use oakplugin::suites::tag;
const OK: c_int = 0;
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
/// 简易纹理句柄(桩 ctx 约定,见 bridge::render::stubsrc=0xA2、
/// dst=0xA1)。
fn fake_texture(ctx: usize) -> CHandle {
CHandle {
ctx: ctx as *mut c_void,
addref: None,
release: None,
abi_version: 1,
}
}
/// 用 param suite 的 paramDefine 造一个指定 OFX 类型的参数实例
/// describe 产物 → createInstance)。
fn make_param(ofx_type: &str) -> ParamInstance {
let mut desc = oakplugin::descriptor::EffectDescriptor::new();
let s = param_suite();
let dhandle = tag::make(&desc.props as *const PropertySet, tag::DESCRIPTOR);
let t = cs(ofx_type);
let n = cs("p");
let mut ph: *mut c_void = std::ptr::null_mut();
let r = unsafe { (s.param_define)(dhandle, t.as_ptr(), n.as_ptr(), &mut ph) };
assert_eq!(r, OK, "paramDefine {ofx_type} 失败: {r}");
ParamInstance::from_def(*desc.params.pop().expect("刚 define 的参数"))
}
/// 简易 clip 实例。
fn make_clip(name: &str) -> ClipInstance {
ClipInstance::from_descriptor(&ClipDescriptor {
props: PropertySet::new(),
name: name.into(),
})
}
/// 扫描测试插件并创建实例,绑定 `identity`(0 = 不绑定),返回
/// (实例, gain 参数句柄)。插件不可用时 skip 并返回 None。
fn bound_inst(identity: usize) -> Option<(std::sync::Arc<oakplugin::handle::RefBox<Instance>>, *mut c_void)> {
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
return None;
}
let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap());
let dirs = [dir.as_ptr()];
unsafe { oakplugin::ffi::oakplugin_host_scan(dirs.as_ptr(), 1) };
let inst = oakplugin::host::Host::global()
.create_instance(TEST_PLUGIN_ID, None)
.ok()?;
inst.value.bind_node(identity);
let ih = tag::make(&inst.value.props as *const PropertySet, tag::INSTANCE);
let name = cs("gain");
let mut ph: *mut c_void = std::ptr::null_mut();
let s = param_suite();
let r = unsafe { (s.param_get_handle)(ih, name.as_ptr(), &mut ph, std::ptr::null_mut()) };
assert_eq!(r, OK, "paramGetHandle(gain) 失败: {r}");
Some((inst, ph))
}
/// 节点→插件:oaknode 输入值变化经桥写入 OFX 参数(类型映射表
/// 逐行:int/float/bool/color/vec2/vec3/combo/str_combo)。
#[test]
fn node_to_param_type_mapping() {
use oakplugin::bridge::node::{node_value_type as T, Value};
// Integer ← INTnum 载荷)。
let p = make_param(oakplugin::param::TYPE_INTEGER);
let mut v = Value::default();
v.r#type = T::INT;
v.num = 7;
p.set_from_node(&v);
assert_eq!(p.get(), oakplugin::param::ParamValue::Int([7, 0, 0], 1));
// Double ← FLOATf[0] 载荷)。
let p = make_param(oakplugin::param::TYPE_DOUBLE);
p.set_from_node(&Value::float(1.5));
assert_eq!(p.get(), oakplugin::param::ParamValue::Double([1.5, 0.0, 0.0], 1));
// Boolean ← BOOLnum 0/1)。
let p = make_param(oakplugin::param::TYPE_BOOLEAN);
p.set_from_node(&Value::bool_(true));
assert_eq!(p.get(), oakplugin::param::ParamValue::Bool(true));
// Choice ← COMBO。
let p = make_param(oakplugin::param::TYPE_CHOICE);
p.set_from_node(&Value::combo(2));
assert_eq!(p.get(), oakplugin::param::ParamValue::Choice(2));
// RGBA ← COLORr,g,b,a)。
let p = make_param(oakplugin::param::TYPE_RGBA);
p.set_from_node(&Value::color(0.1, 0.2, 0.3, 0.9));
assert_eq!(
p.get(),
oakplugin::param::ParamValue::Color([0.1, 0.2, 0.3, 0.9], 4)
);
// RGB ← COLORalpha 忽略,恒 0 占位)。
let p = make_param(oakplugin::param::TYPE_RGB);
p.set_from_node(&Value::color(0.1, 0.2, 0.3, 0.5));
assert_eq!(p.get(), oakplugin::param::ParamValue::Color([0.1, 0.2, 0.3, 0.0], 3));
// Double2D ← VEC2Double3D ← VEC3。
let p = make_param(oakplugin::param::TYPE_DOUBLE2D);
p.set_from_node(&Value::vec(&[1.0, 2.0]));
assert_eq!(p.get(), oakplugin::param::ParamValue::Double([1.0, 2.0, 0.0], 2));
let p = make_param(oakplugin::param::TYPE_DOUBLE3D);
p.set_from_node(&Value::vec(&[1.0, 2.0, 3.0]));
assert_eq!(p.get(), oakplugin::param::ParamValue::Double([1.0, 2.0, 3.0], 3));
// Integer2D ← VEC2 / Integer3D ← VEC3(浮点截断为 int)。
let p = make_param(oakplugin::param::TYPE_INTEGER2D);
p.set_from_node(&Value::vec(&[1.5, 2.5]));
assert_eq!(p.get(), oakplugin::param::ParamValue::Int([1, 2, 0], 2));
let p = make_param(oakplugin::param::TYPE_INTEGER3D);
p.set_from_node(&Value::vec(&[1.5, 2.5, 3.5]));
assert_eq!(p.get(), oakplugin::param::ParamValue::Int([1, 2, 3], 3));
// String 参数 + STRING 节点类型:POD 不携带数据 → 值不变
// (保持默认空串;字符串值走 facade 的字符串 API)。
let p = make_param(oakplugin::param::TYPE_STRING);
p.set_from_node(&Value::string());
assert!(
matches!(p.get(), oakplugin::param::ParamValue::String(s) if s.to_bytes().is_empty())
);
// 类型不匹配 → 忽略(保持现值)。
let p = make_param(oakplugin::param::TYPE_DOUBLE);
p.set_ofx(oakplugin::param::ParamValue::Double([5.0, 0.0, 0.0], 1));
p.set_from_node(&Value::int(9)); // INT 不是 FLOAT
assert_eq!(p.get(), oakplugin::param::ParamValue::Double([5.0, 0.0, 0.0], 1));
}
/// 插件→节点:paramSetValue(插件自改)经 instanceChanged 桥回写
/// oaknode,且打包为一条 undo 命令(undo/redo 后值正确)。
#[test]
fn param_to_node_undoable_writeback() {
common::with_host(|| {
let Some((inst, gain_h)) = bound_inst(42) else { return };
let s = param_suite();
#[cfg(feature = "test-stubs")]
{
use oakplugin::bridge::{node, undo};
node::stub::reset();
undo::stub::reset();
node::stub::register_node(42);
node::stub::set_input(42, "gain", node::Value::float(0.5));
// 插件自改 → 回写 + 单命令立即 redo。
let r = unsafe { (s.param_set_value)(gain_h, 0.75) };
assert_eq!(r, OK, "paramSetValue 失败: {r}");
let cur = node::stub::input(42, "gain").expect("已回写节点输入");
assert_eq!(cur.value.f[0], 0.75);
// undoable:命令捕获 prev/next 且已应用(redo 语义)。
let rec = undo::stub::last_command().expect("回写产生命令");
assert_eq!(rec.node, 42);
assert_eq!(rec.input, "gain");
assert!(!rec.is_multi);
assert!(rec.applied);
assert_eq!(rec.prev.f[0], 0.5, "prev = 修改前节点值");
assert_eq!(rec.next.f[0], 0.75, "next = 修改后值");
// undo → 恢复旧值;redo → 再应用。
undo::stub::undo(rec.id);
assert_eq!(node::stub::input(42, "gain").unwrap().value.f[0], 0.5);
undo::stub::redo(rec.id);
assert_eq!(node::stub::input(42, "gain").unwrap().value.f[0], 0.75);
// 字符串参数:POD 无数据 → 经字符串桥回写。
node::stub::set_input_string(42, "label", "old");
let name = cs("label");
let mut lh: *mut c_void = std::ptr::null_mut();
let ih = tag::make(&inst.value.props as *const PropertySet, tag::INSTANCE);
assert_eq!(
unsafe { (s.param_get_handle)(ih, name.as_ptr(), &mut lh, std::ptr::null_mut()) },
OK
);
let hello = cs("hello");
assert_eq!(unsafe { (s.param_set_value)(lh, hello.as_ptr()) }, OK);
let cur = node::stub::input(42, "label").expect("字符串已回写");
assert_eq!(cur.string.as_deref(), Some("hello"));
}
#[cfg(not(feature = "test-stubs"))]
{
// 无 liboaknode:桥符号缺失 → node_from_identity 空句柄,
// 回写 no-opparamSetValue 仍成功且值本地生效。
let r = unsafe { (s.param_set_value)(gain_h, 0.75) };
assert_eq!(r, OK);
let mut out = 0.0;
assert_eq!(unsafe { (s.param_get_value)(gain_h, &mut out) }, OK);
assert_eq!(out, 0.75);
}
});
}
/// 未绑定节点的实例收到 instanceChangedno-op 不崩(身份注册表
/// 查无此项的路径)。
#[test]
fn writeback_without_bound_node_is_noop() {
common::with_host(|| {
let Some((inst, gain_h)) = bound_inst(0) else { return };
let s = param_suite();
// identity 0(未绑定):回写直接短路,值只写本地参数。
assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.5) }, OK);
let mut out = 0.0;
assert_eq!(unsafe { (s.param_get_value)(gain_h, &mut out) }, OK);
assert_eq!(out, 0.5);
#[cfg(feature = "test-stubs")]
{
use oakplugin::bridge::{node, undo};
node::stub::reset();
undo::stub::reset();
node::stub::register_node(9);
// 已绑定但身份在注册表查无 → node_from_identity 空句柄。
inst.value.bind_node(9999);
assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.6) }, OK);
assert!(
undo::stub::records().is_empty(),
"身份查无不应产生命令:{:?}",
undo::stub::records()
);
// 已登记身份但输入名不存在 → 桥返回错误,同样 no-op。
inst.value.bind_node(9);
assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.7) }, OK);
assert!(undo::stub::records().is_empty(), "输入不存在不应产生命令");
// 节点输入值保持未回写(本地参数值仍有效)。
let mut out = 0.0;
assert_eq!(unsafe { (s.param_get_value)(gain_h, &mut out) }, OK);
assert_eq!(out, 0.7);
}
});
}
/// clip 输入:oakrender 纹理挂接后 fetch_image 读回的像素与
/// 纹理内容一致(F32 格式不丢精度)。
#[test]
fn clip_texture_roundtrip_f32() {
let clip = make_clip("Source");
let scale = RenderScale { x: 1.0, y: 1.0 };
// 未挂纹理 → NotFound(两模式一致)。
assert!(matches!(
clip.fetch_image(0.0, scale, None),
Err(oakplugin::error::Error::NotFound)
));
// 子区域第 1 期不支持 → Failed。
assert!(matches!(
clip.fetch_image(
0.0,
scale,
Some(OfxRectD { x1: 0.0, y1: 0.0, x2: 2.0, y2: 2.0 })
),
Err(oakplugin::error::Error::Failed(_))
));
#[cfg(feature = "test-stubs")]
{
use oakplugin::bridge::render::{self, PIXEL_FORMAT_F32, PIXEL_FORMAT_U8};
render::stub::reset();
// 构造可识别的 F32 RGBA 帧(r=行, g=列, b=0, a=1)。
let (w, h) = (8usize, 4usize);
let mut pixels = Vec::with_capacity(w * h * 16);
for y in 0..h {
for x in 0..w {
for v in [y as f32, x as f32, 0.0, 1.0] {
pixels.extend_from_slice(&v.to_le_bytes());
}
}
}
render::stub::setup_src(w as i32, h as i32, PIXEL_FORMAT_F32, pixels.clone());
let tex = fake_texture(0xA2);
clip.set_input_texture(tex, 0.0);
let img = clip.fetch_image(0.0, scale, None).expect("fetch_image 成功");
assert_eq!(img.pixels(), &pixels, "F32 像素 round-trip 不丢精度");
assert_eq!(img.components(), Components::Rgba);
assert_eq!(img.depth(), BitDepth::Float);
// dummy 纹理输入 → 空输入(NotFound)。
render::stub::set_dummy(0xA2, true);
assert!(matches!(
clip.fetch_image(0.0, scale, None),
Err(oakplugin::error::Error::NotFound)
));
render::stub::set_dummy(0xA2, false);
// 非 F32 输入帧 → 明确失败。
render::stub::setup_src(w as i32, h as i32, PIXEL_FORMAT_U8, vec![0u8; w * h * 4]);
assert!(matches!(
clip.fetch_image(0.0, scale, None),
Err(oakplugin::error::Error::Failed(_))
));
}
}
/// 输出:store_output_image 产出的纹理在 oakrender 侧可读且
/// 像素一致;dummy 纹理输入按空输入处理。
#[test]
fn output_image_to_texture() {
let clip = make_clip("Output");
let img = Image::allocate(
BitDepth::Float,
Components::Rgba,
OfxRectD { x1: 0.0, y1: 0.0, x2: 8.0, y2: 4.0 },
);
// 未挂输出纹理 → NotFound(两模式一致)。
assert!(matches!(
clip.store_output_image(&img),
Err(oakplugin::error::Error::NotFound)
));
#[cfg(feature = "test-stubs")]
{
use oakplugin::bridge::render::{self, PIXEL_FORMAT_F32, PIXEL_FORMAT_U8};
render::stub::reset();
render::stub::setup_dst(8, 4, PIXEL_FORMAT_F32);
let tex = fake_texture(0xA1);
clip.set_output_texture(tex, 0.0);
// 填非平凡像素后回写 → oakrender 侧读到的与图像一致。
let mut img = img;
for (i, b) in img.pixels_mut().iter_mut().enumerate() {
*b = (i % 256) as u8;
}
let tex2 = clip.store_output_image(&img).expect("store_output_image 成功");
assert_eq!(tex2.ctx, tex.ctx, "返回挂入的纹理句柄");
assert_eq!(render::stub::dst_pixels(), img.pixels(), "输出像素一致");
// dummy 输出纹理 → NotFound。
render::stub::set_dummy(0xA1, true);
assert!(matches!(
clip.store_output_image(&img),
Err(oakplugin::error::Error::NotFound)
));
render::stub::set_dummy(0xA1, false);
// 帧尺寸与图像不一致 → 明确失败。
render::stub::setup_dst(16, 4, PIXEL_FORMAT_F32);
assert!(matches!(
clip.store_output_image(&img),
Err(oakplugin::error::Error::Failed(_))
));
// 非 F32 输出帧 → 明确失败。
render::stub::setup_dst(8, 4, PIXEL_FORMAT_U8);
assert!(matches!(
clip.store_output_image(&img),
Err(oakplugin::error::Error::Failed(_))
));
}
}
/// undo 打包:一次编辑事务(paramEditBegin/End)内的多次参数
/// 修改合并为一条命令。
#[test]
fn edit_transaction_coalescing() {
common::with_host(|| {
let Some((_inst, gain_h)) = bound_inst(42) else { return };
let s = param_suite();
#[cfg(feature = "test-stubs")]
{
use oakplugin::bridge::{node, undo};
node::stub::reset();
undo::stub::reset();
node::stub::register_node(42);
node::stub::set_input(42, "gain", node::Value::float(0.0));
// 编辑事务内两次回写 → 一条 multi。
assert_eq!(unsafe { (s.param_edit_begin)(gain_h) }, OK);
assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.25) }, OK);
assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.5) }, OK);
assert_eq!(unsafe { (s.param_edit_end)(gain_h) }, OK);
let recs = undo::stub::records();
let multi = recs.iter().find(|r| r.is_multi).expect("存在 multi 命令");
assert_eq!(multi.children.len(), 2, "两次修改合并为一条 multi");
for c in &multi.children {
let child = recs.iter().find(|r| r.id == *c).expect("子命令在表");
assert!(!child.is_multi);
assert!(child.applied, "子命令已 redo 生效");
assert_eq!(child.node, 42);
assert_eq!(child.input, "gain");
}
// 值即时生效(事务内子命令 redoeditEnd 的 multi redo 幂等)。
assert_eq!(node::stub::input(42, "gain").unwrap().value.f[0], 0.5);
// 事务外回写 → 独立单命令(不并入 multi)。
let before = undo::stub::records().len();
assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.75) }, OK);
let after = undo::stub::records().len();
assert_eq!(after, before + 1, "事务外每次回写一条独立命令");
let last = undo::stub::last_command().unwrap();
assert!(!last.is_multi);
assert_eq!(node::stub::input(42, "gain").unwrap().value.f[0], 0.75);
// 嵌套 editBegin/End:内层结束不提交(multi 保留未 free),
// 最外层结束才提交(redo + free)。
assert_eq!(unsafe { (s.param_edit_begin)(gain_h) }, OK);
assert_eq!(unsafe { (s.param_edit_begin)(gain_h) }, OK);
assert_eq!(unsafe { (s.param_set_value)(gain_h, 1.0) }, OK);
assert_eq!(unsafe { (s.param_edit_end)(gain_h) }, OK);
let newest_multi = || {
undo::stub::records()
.iter()
.filter(|r| r.is_multi)
.max_by_key(|r| r.id)
.cloned()
.expect("存在 multi")
};
let m = newest_multi();
assert_eq!(m.children.len(), 1, "事务内回写并入新 multi");
assert!(!m.freed, "内层 editEnd 不提交");
assert_eq!(unsafe { (s.param_edit_end)(gain_h) }, OK);
assert!(newest_multi().freed, "最外层 editEnd 提交(redo+free");
assert_eq!(node::stub::input(42, "gain").unwrap().value.f[0], 1.0);
}
#[cfg(not(feature = "test-stubs"))]
{
// 无桥:editBegin/End 与回写 no-op;参数值本地生效。
assert_eq!(unsafe { (s.param_edit_begin)(gain_h) }, OK);
assert_eq!(unsafe { (s.param_set_value)(gain_h, 0.25) }, OK);
assert_eq!(unsafe { (s.param_edit_end)(gain_h) }, OK);
let mut out = 0.0;
assert_eq!(unsafe { (s.param_get_value)(gain_h, &mut out) }, OK);
assert_eq!(out, 0.25);
}
});
}
/// 系统级冒烟:真实 OFX 插件 bundle/Library/OFX/Plugins/
/// Misc.ofx.bundle,本机安装的标准 openfx-misc)在存在时扫描 →
/// describe → create_instance → 协商;absent 时 skip。
///
/// 这是 0 期 golden master 的轻量替代(0 期基建可能尚未落地——见
/// README「与 M11 §3.5 的对照」):只验"能加载、能建实例、协商
/// 有结果",不比对快照。
#[test]
fn system_misc_ofx_bundle_smoke() {
let bundle = std::path::Path::new("/Library/OFX/Plugins/Misc.ofx.bundle");
if !bundle.is_dir() {
common::skip("/Library/OFX/Plugins/Misc.ofx.bundle 不存在");
return;
}
common::with_host(|| {
use oakplugin::host::Host;
let host = Host::global();
// 只扫 Misc:临时目录内建软链(scan_path 会 canonicalize)。
let tmp = std::env::temp_dir().join(format!("oak-misc-scan-{}", std::process::id()));
let link = tmp.join("Misc.ofx.bundle");
if !link.exists() {
std::fs::create_dir_all(&tmp).ok();
std::os::unix::fs::symlink(bundle, &link).expect("建软链");
}
host.cache.scan_path(&tmp).expect("扫描 Misc.ofx.bundle");
std::fs::remove_dir_all(&tmp).ok();
// bundle 内的插件已 describe 入缓存。
let ids: Vec<String> = (0..host.cache.count())
.filter_map(|i| host.cache.at(i).map(|p| p.identifier.clone()))
.collect();
if ids.is_empty() {
// 本机 bundle 若为 Natron 分支构建(describe 返回
// MissingHostFeature),扫描必然为空——README 已记录该
// 不兼容;兼容 bundle 的机器上此处照常断言。
common::skip("/Library/OFX/Plugins/Misc.ofx.bundle 为 Natron 分支构建(describe 返回 MissingHostFeature)或空 bundle");
return;
}
println!("Misc.ofx.bundle 扫描到 {} 个插件:{ids:?}", ids.len());
// create_instance 冒烟(首个插件的首选上下文)。
let first = host.cache.at(0).expect("首个插件").identifier.clone();
let inst = host.create_instance(&first, None);
assert!(inst.is_ok(), "create_instance({first}) 应成功");
// 协商冒烟(输出 clip 的分量/位深/帧率应返回)。
let inst = inst.unwrap();
let prefs = inst.value.get_clip_preferences();
assert!(prefs.is_ok(), "getClipPreferences({first}) 应成功: {prefs:?}");
});
}
+184
View File
@@ -0,0 +1,184 @@
// 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/>.
//! ofxColourM11 §4):宿主 OCIO 能力宣告、实例协商属性、输入 clip
//! 工作空间(ACEScg)、GetOutputColourspace 往返(偏好采纳 +
//! 交叉引用解析 + 输出写回)。
mod common;
use std::ffi::{c_void, CString};
use oakplugin::ffi::{oakplugin_host_scan, oakplugin_instance_create, oakplugin_instance_free};
use oakplugin::handle::{get, CHandle};
use oakplugin::host::Host;
use oakplugin::property::Value;
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
fn scan_and_create(id: &str) -> CHandle {
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
return CHandle::null();
}
let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap());
let dirs = [dir.as_ptr()];
unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) };
let id = cs(id);
unsafe { oakplugin_instance_create(id.as_ptr()) }
}
fn instance_of(h: &CHandle) -> Option<std::sync::Arc<oakplugin::handle::RefBox<oakplugin::instance::Instance>>> {
unsafe { get::<std::sync::Arc<oakplugin::handle::RefBox<oakplugin::instance::Instance>>>(h) }
.cloned()
}
fn prop_str(props: &oakplugin::property::PropertySet, name: &str) -> Option<String> {
match props.get(name, 0)? {
Value::String(s) => Some(s.to_string_lossy().into_owned()),
_ => None,
}
}
/// 宿主能力宣告:OCIO 模式 + native 配置列表 + GL 支持。
#[test]
fn host_declares_ocio_capability() {
let host = Host::global();
assert_eq!(
prop_str(&host.props, "OfxImageEffectPropColourManagementStyle").as_deref(),
Some("OfxImageEffectColourManagementOCIO")
);
assert_eq!(
host.props.dimension("OfxImageEffectPropColourManagementAvailableConfigs"),
1,
"可用配置至少含 ofx-native-v1.5_aces-v1.3_ocio-v2.3"
);
assert_eq!(
prop_str(&host.props, "OfxImageEffectPropOpenGLRenderSupported").as_deref(),
Some("true")
);
}
/// 实例期协商属性:style/config/OCIOConfig 已写;输入 clip 色彩空间
/// = 工作空间 ACEScg;输出 clip 未设(待 GetOutputColourspace)。
#[test]
fn instance_and_clip_colourspace_props() {
common::with_host(|| {
let mut h = scan_and_create(TEST_PLUGIN_ID);
if h.is_null() {
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
assert_eq!(
prop_str(&inst.value.props, "OfxImageEffectPropColourManagementStyle").as_deref(),
Some("OfxImageEffectColourManagementOCIO")
);
assert_eq!(
prop_str(&inst.value.props, "OfxImageEffectPropColourManagementConfig").as_deref(),
Some("ofx-native-v1.5_aces-v1.3_ocio-v2.3")
);
assert_eq!(
prop_str(&inst.value.props, "OfxImageEffectPropOCIOConfig").as_deref(),
Some("ocio://default")
);
let source = inst.value.clips.iter().find(|c| c.name == "Source").unwrap();
assert_eq!(
prop_str(&source.props, "OfxImageClipPropColourspace").as_deref(),
Some("ACEScg"),
"输入 clip 色彩空间 = 工作空间"
);
let output = inst.value.clips.iter().find(|c| c.name == "Output").unwrap();
assert!(
prop_str(&output.props, "OfxImageClipPropColourspace")
.map(|s| s.is_empty())
.unwrap_or(true),
"输出 clip 色彩空间在 GetOutputColourspace 前未设"
);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// GetOutputColourspace 往返:无偏好 → 插件回 "OfxColourspace_Source"
/// 交叉引用 → 宿主解析为 Source 的实际色彩空间(ACEScg)并写回输出。
#[test]
fn get_output_colourspace_cross_reference() {
common::with_host(|| {
let mut h = scan_and_create(TEST_PLUGIN_ID);
if h.is_null() {
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let cs = inst
.value
.get_output_colourspace(&[])
.expect("GetOutputColourspace 应成功");
assert_eq!(cs, "ACEScg", "交叉引用 OfxColourspace_Source → ACEScg");
inst.value.set_output_colourspace(&cs);
let output = inst.value.clips.iter().find(|c| c.name == "Output").unwrap();
assert_eq!(
prop_str(&output.props, "OfxImageClipPropColourspace").as_deref(),
Some("ACEScg"),
"输出 clip 已写回解析后的色彩空间"
);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// GetOutputColourspace 偏好采纳:插件优先取宿主偏好的第一个色彩
/// 空间。
#[test]
fn get_output_colourspace_preferred() {
common::with_host(|| {
let mut h = scan_and_create(TEST_PLUGIN_ID);
if h.is_null() {
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let cs = inst
.value
.get_output_colourspace(&["ACEScg".to_string(), "linear".to_string()])
.expect("GetOutputColourspace 应成功");
assert_eq!(cs, "ACEScg", "偏好列表第一个被采纳");
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 交叉引用解析的边界:未知 clip 引用原样返回;非交叉引用原样返回。
#[test]
fn resolve_colourspace_edge_cases() {
common::with_host(|| {
let mut h = scan_and_create(TEST_PLUGIN_ID);
if h.is_null() {
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
assert_eq!(
inst.value.resolve_colourspace("OfxColourspace_NoSuchClip".to_string()),
"OfxColourspace_NoSuchClip",
"未知 clip 引用保持原样"
);
assert_eq!(
inst.value.resolve_colourspace("ACEScg".to_string()),
"ACEScg",
"非交叉引用原样返回"
);
unsafe { oakplugin_instance_free(&mut h) };
});
}
+99
View File
@@ -0,0 +1,99 @@
// 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/>.
//! 测试公共件:最小测试插件定位、快照/golden 文件路径、夹具。
//!
//! 最小测试插件(cbits/oak_test_plugin.cbuild.rs 编译为共享库,
//! 运行时装配成 oak-test-plugin.ofx.bundle):filter 上下文、
//! Double 参数 gain、双 clipSource/Output)。插件未构建时相关
//! 用例经 [`skip`] 提前返回。
//! 测试桩在库内(bridge::render::stub`--features test-stubs`):
//! 全链路像素路径可在 cargo test 跑通;真实链路在 standalone
//! ctest。本文档的 run 命令见 crate README。
use std::path::PathBuf;
/// 构建系统注入插件路径的环境变量名。
pub const TEST_PLUGIN_ENV: &str = "OAK_TEST_PLUGIN_DIR";
/// 测试插件 bundle 的绝对路径(由构建系统经环境变量注入;
/// 未注入时用 build.rs 编的共享库现场装配 bundle 目录;均不可用
/// 时返回 None,调用方 skip)。
pub fn test_plugin_dir() -> Option<PathBuf> {
if let Some(p) = std::env::var_os(TEST_PLUGIN_ENV) {
return Some(PathBuf::from(p));
}
static BUNDLE: std::sync::OnceLock<Option<PathBuf>> = std::sync::OnceLock::new();
BUNDLE
.get_or_init(|| {
let out = PathBuf::from(env!("OUT_DIR"));
let lib = if cfg!(target_os = "macos") {
out.join("oak_test_plugin.dylib")
} else {
out.join("oak_test_plugin.so")
};
if !lib.is_file() {
return None;
}
let bundle = std::env::temp_dir()
.join(format!("oak-test-plugin-{}", std::process::id()))
.join("oak-test-plugin.ofx.bundle");
let platform = if cfg!(target_os = "macos") { "MacOS" } else { "Linux-x86-64" };
let bin_dir = bundle.join("Contents").join(platform);
std::fs::create_dir_all(&bin_dir).ok()?;
let target = bin_dir.join("plugin");
if !target.exists() {
std::fs::copy(&lib, &target).ok()?;
}
Some(bundle)
})
.clone()
}
/// 测试插件 bundle 的父目录(host_scan 的入参)。
pub fn test_plugin_scan_dir() -> Option<PathBuf> {
let bundle = test_plugin_dir()?;
let parent = bundle.parent()?;
// scan_path 会 canonicalizebundle 在临时目录下,直接给父目录。
Some(parent.to_path_buf())
}
/// golden master 目录(tests/ofx/)。描述符快照 JSON 与 EXR 帧都在这。
pub fn golden_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/ofx")
}
/// 当前平台是否有 GPUGL golden 用例的门:无 GPU 一律 skip)。
///
/// 第 1 期无 GL 用例:显式环境变量开启(GL 验收需人工本机确认,
/// CI 一律跳过)。
pub fn gpu_available() -> bool {
std::env::var_os("OAK_GPU_TESTS").is_some()
}
/// 通用 skip 宏的函数形态:前置条件不满足时打印原因并提前返回。
/// cargo 没有 GTEST_SKIP,约定为 `return`,并在输出里打印 SKIP。)
pub fn skip(reason: &str) {
println!("SKIP: {reason}");
}
/// 宿主单例测试串行化:同一二进制的测试并行跑会互相踩
/// init/shutdown/scan(进程单例无锁);所有触碰宿主面的用例经它。
pub fn with_host(f: impl FnOnce()) {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
f();
}
+221
View File
@@ -0,0 +1,221 @@
// 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/>.
//! FFI 出口层(ffi.rs)契约测试:host.h 面。
//!
//! 每个导出函数至少一正常一错误路径(项目铁律)。
//! 宿主单例无锁:全部用例经 [`common::with_host`] 串行化。
//! 需要最小测试插件(build.rs 编,common::test_plugin_dir 装配);
//! 不可用时 skip。
mod common;
use std::ffi::{c_char, c_int, CStr, CString};
use oakplugin::ffi::{
oakplugin_host_init, oakplugin_host_plugin_count, oakplugin_host_plugin_id_at,
oakplugin_host_plugin_label, oakplugin_host_scan, oakplugin_host_set_message_handler,
oakplugin_host_shutdown,
};
const E_INVALID: i32 = -90001;
const E_NOT_FOUND: i32 = -90004;
const OK: i32 = 0;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
/// 扫描测试插件;失败/缺失返回 false(调用方 skip)。
fn scan_test_plugin() -> bool {
let Some(dir) = common::test_plugin_scan_dir() else {
common::skip("最小测试插件未构建");
return false;
};
let dir = cs(dir.to_str().unwrap());
let dirs = [dir.as_ptr()];
unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) == OK }
}
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
/// init/shutdown 幂等:重复 init 无副作用;shutdown 后再 init 可用。
#[test]
fn host_init_shutdown_idempotent() {
common::with_host(|| {
assert_eq!(unsafe { oakplugin_host_init() }, OK);
assert_eq!(unsafe { oakplugin_host_init() }, OK);
unsafe { oakplugin_host_shutdown() };
assert_eq!(unsafe { oakplugin_host_init() }, OK);
unsafe { oakplugin_host_shutdown() };
});
}
/// scan 默认路径(NULL/0):返回 OAKPLUGIN_OK 或 E_FAILED(无插件
/// 目录的机器),二者之外不允许别的码。
#[test]
fn host_scan_default_paths() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let r = unsafe { oakplugin_host_scan(std::ptr::null(), 0) };
assert!(r == OK || r == -90003, "scan(NULL,0) = {r}");
unsafe { oakplugin_host_shutdown() };
});
}
/// scan 指定目录(测试插件所在):插件计数 ≥1 且能找到测试插件
/// 标识;目录不存在返回 E_FAILED 而不崩。
#[test]
fn host_scan_explicit_dir() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
assert!(scan_test_plugin(), "测试插件扫描失败");
assert!(unsafe { oakplugin_host_plugin_count() } >= 1);
// 标识可见(两段式第一段)。
let len = unsafe { oakplugin_host_plugin_id_at(0, std::ptr::null_mut(), 0) };
assert!(len > 0);
let mut buf = vec![0u8; len as usize];
let r = unsafe { oakplugin_host_plugin_id_at(0, buf.as_mut_ptr() as *mut c_char, len) };
assert_eq!(r, OK);
assert_eq!(
unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }.to_str().unwrap(),
TEST_PLUGIN_ID
);
// 目录不存在:静默跳过返回 OK 而不崩(C++ olivehost.cpp:59-62
// 的 add_plugin_path 语义——声明原期望 E_FAILED,与参照系
// 不符,以 C++ 行为为准)。
let nope = cs("/nonexistent/ofx/plugins");
let dirs = [nope.as_ptr()];
assert_eq!(unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }, OK);
unsafe { oakplugin_host_shutdown() };
});
}
/// plugin_id_at:两段式(先问长度再取内容);越界索引返回
/// E_NOT_FOUND;缓冲区不足时返回所需长度且不越界写。
#[test]
fn host_plugin_id_at_two_stage() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
if !scan_test_plugin() {
unsafe { oakplugin_host_shutdown() };
return;
}
// 越界 → E_NOT_FOUND。
assert_eq!(
unsafe { oakplugin_host_plugin_id_at(99, std::ptr::null_mut(), 0) },
E_NOT_FOUND
);
// 缓冲区不足:返回所需长度(> 0),不越界写(哨兵在 [4]——
// [3] 是 4 字节缓冲内的 NUL 位)。
let mut small = [0x7fu8; 5];
let r = unsafe { oakplugin_host_plugin_id_at(0, small.as_mut_ptr() as *mut c_char, 4) };
assert!(r > 4);
assert_eq!(small[4], 0x7f, "缓冲区外不应被写");
unsafe { oakplugin_host_shutdown() };
});
}
/// plugin_label:已知插件返回非空 label;未知标识 E_NOT_FOUND
/// NULL 参数 E_INVALID。
#[test]
fn host_plugin_label_paths() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
if !scan_test_plugin() {
unsafe { oakplugin_host_shutdown() };
return;
}
let id = cs(TEST_PLUGIN_ID);
let len = unsafe { oakplugin_host_plugin_label(id.as_ptr(), std::ptr::null_mut(), 0) };
assert!(len > 0);
let mut buf = vec![0u8; len as usize];
assert_eq!(
unsafe { oakplugin_host_plugin_label(id.as_ptr(), buf.as_mut_ptr() as *mut c_char, len) },
OK
);
assert!(!unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }.to_bytes().is_empty());
// 未知标识。
let nope = cs("org.oak.nope");
assert_eq!(
unsafe { oakplugin_host_plugin_label(nope.as_ptr(), std::ptr::null_mut(), 0) },
E_NOT_FOUND
);
// NULL 参数。
assert_eq!(
unsafe { oakplugin_host_plugin_label(std::ptr::null(), std::ptr::null_mut(), 0) },
E_INVALID
);
unsafe { oakplugin_host_shutdown() };
});
}
/// message handler:注册后测试插件的 message 回调到达(级别与
/// 文本透传,含 %d 格式化);注册 NULL 后消息被丢弃而不崩。
#[test]
fn host_message_handler_dispatch() {
common::with_host(|| {
use oakplugin::ffi::{oakplugin_instance_create, oakplugin_instance_free};
unsafe extern "C" fn capture(
type_: *const c_char,
message: *const c_char,
userdata: *mut std::ffi::c_void,
) -> c_int {
let v = unsafe { &mut *(userdata as *mut Vec<(String, String)>) };
v.push((
unsafe { CStr::from_ptr(type_) }.to_string_lossy().into_owned(),
unsafe { CStr::from_ptr(message) }.to_string_lossy().into_owned(),
));
0
}
unsafe { oakplugin_host_init() };
if !scan_test_plugin() {
unsafe { oakplugin_host_shutdown() };
return;
}
// 注册捕获器 → 创建实例(插件在 createInstance 发 message)。
let mut captured: Vec<(String, String)> = Vec::new();
unsafe {
oakplugin_host_set_message_handler(
Some(capture),
&mut captured as *mut _ as *mut std::ffi::c_void,
)
};
let id = cs(TEST_PLUGIN_ID);
let mut h = unsafe { oakplugin_instance_create(id.as_ptr()) };
assert!(!h.is_null());
assert_eq!(captured.len(), 1);
assert_eq!(captured[0].0, "OfxMessageMessage");
assert_eq!(captured[0].1, "created id=7", "v1 变长格式化应生效");
// 注销 → 再创建不崩、无捕获。
unsafe { oakplugin_host_set_message_handler(None, std::ptr::null_mut()) };
captured.clear();
let mut h2 = unsafe { oakplugin_instance_create(id.as_ptr()) };
assert!(!h2.is_null());
assert!(captured.is_empty());
unsafe { oakplugin_instance_free(&mut h2) };
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
+452
View File
@@ -0,0 +1,452 @@
// 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/>.
//! FFI 出口层契约测试:instance.h 面(含 M11 §2.1 内省族)。
//!
//! 宿主单例无锁:全部用例经 [`common::with_host`] 串行化。
//! render 路径经 oakrender 测试桩(`--features test-stubs`);
//! 未启用时相关用例 skip。
mod common;
use std::ffi::{c_char, c_int, c_void, CStr, CString};
use oakplugin::ffi::{
node_value_type, oakplugin_debug_alive_count, oakplugin_instance_cancel,
oakplugin_instance_clip_count, oakplugin_instance_clip_info, oakplugin_instance_clip_name,
oakplugin_instance_create, oakplugin_instance_free, oakplugin_instance_get_param,
oakplugin_instance_get_param_string, oakplugin_instance_param_choice_count,
oakplugin_instance_param_choice_label, oakplugin_instance_param_count,
oakplugin_instance_param_default_double, oakplugin_instance_param_default_string,
oakplugin_instance_param_display_range, oakplugin_instance_param_hint,
oakplugin_instance_param_label, oakplugin_instance_param_name, oakplugin_instance_param_parent,
oakplugin_instance_param_secret, oakplugin_instance_param_type, oakplugin_instance_render,
oakplugin_instance_set_param, oakplugin_instance_set_param_string,
oakplugin_instance_set_progress_cb, OakNodeValue,
};
use oakplugin::handle::CHandle;
const E_INVALID: i32 = -90001;
const E_NOT_FOUND: i32 = -90004;
const OK: i32 = 0;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
/// 扫描测试插件并创建实例;不可用返回空句柄。
fn create_instance() -> CHandle {
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
return CHandle::null();
}
let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap());
let dirs = [dir.as_ptr()];
unsafe { oakplugin_host_scan_import(dirs.as_ptr(), 1) };
let id = cs(TEST_PLUGIN_ID);
unsafe { oakplugin_instance_create(id.as_ptr()) }
}
// 避免重复 import host_scanffi 模块内路径统一)。
use oakplugin::ffi::oakplugin_host_scan as oakplugin_host_scan_import;
/// 两段式读字符串。
fn two_stage(get: unsafe extern "C" fn(CHandle, c_int, *mut c_char, c_int) -> c_int, h: CHandle, index: c_int) -> Option<String> {
let len = unsafe { get(h, index, std::ptr::null_mut(), 0) };
if len <= 0 {
return None;
}
let mut buf = vec![0u8; len as usize];
let r = unsafe { get(h, index, buf.as_mut_ptr() as *mut c_char, len) };
if r != OK {
return None;
}
Some(unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }.to_string_lossy().into_owned())
}
/// 简易实例句柄(测试桩纹理用)。
fn fake_texture(ctx: usize) -> CHandle {
CHandle {
ctx: ctx as *mut c_void,
addref: None,
release: None,
abi_version: 1,
}
}
/// create/free:已知插件返回非空句柄;free 后 alive 回基线;
/// free(NULL)/free(空) no-op。
#[test]
fn instance_create_free() {
common::with_host(|| {
unsafe { oakplugin_host_scan_import(std::ptr::null(), 0) };
let base = unsafe { oakplugin_debug_alive_count() };
let mut h = create_instance();
assert!(!h.is_null());
assert_eq!(unsafe { oakplugin_debug_alive_count() }, base + 1);
unsafe { oakplugin_instance_free(&mut h) };
assert_eq!(unsafe { oakplugin_debug_alive_count() }, base);
// free(NULL)/free(空) no-op。
unsafe { oakplugin_instance_free(std::ptr::null_mut()) };
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// create 未知标识:返回空句柄(init 失败返回空,契约 §a)。
#[test]
fn instance_create_unknown_id() {
common::with_host(|| {
let id = cs("org.oak.does-not-exist");
let h = unsafe { oakplugin_instance_create(id.as_ptr()) };
assert!(h.is_null());
});
}
/// set/get_paramdouble):round-trip 一致;未知参数名
/// E_NOT_FOUND;空句柄 E_INVALIDout 为 NULL 时 E_INVALID。
#[test]
fn instance_param_double_roundtrip() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let name = cs("gain");
let mut v = OakNodeValue::default();
v.r#type = node_value_type::FLOAT;
v.f = [1.25, 0.0, 0.0, 0.0];
assert_eq!(unsafe { oakplugin_instance_set_param(h, name.as_ptr(), &v) }, OK);
let mut out = OakNodeValue::default();
assert_eq!(unsafe { oakplugin_instance_get_param(h, name.as_ptr(), &mut out) }, OK);
assert_eq!(out.r#type, node_value_type::FLOAT);
assert_eq!(out.f[0], 1.25);
// 未知参数名 → E_NOT_FOUND。
let nope = cs("nope");
assert_eq!(unsafe { oakplugin_instance_set_param(h, nope.as_ptr(), &v) }, E_NOT_FOUND);
// 空句柄 → E_INVALID。
assert_eq!(unsafe { oakplugin_instance_set_param(CHandle::null(), name.as_ptr(), &v) }, E_INVALID);
// out NULL → E_INVALID。
assert_eq!(unsafe { oakplugin_instance_get_param(h, name.as_ptr(), std::ptr::null_mut()) }, E_INVALID);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// set/get_param_stringround-trip 一致(含空串与长串两段式)。
#[test]
fn instance_param_string_roundtrip() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let name = cs("label");
let value = cs("hello 你好");
assert_eq!(unsafe { oakplugin_instance_set_param_string(h, name.as_ptr(), value.as_ptr()) }, OK);
// 两段式。
let len = unsafe { oakplugin_instance_get_param_string(h, name.as_ptr(), std::ptr::null_mut(), 0) };
assert!(len > 0);
let mut buf = vec![0u8; len as usize];
assert_eq!(unsafe { oakplugin_instance_get_param_string(h, name.as_ptr(), buf.as_mut_ptr() as *mut c_char, len) }, OK);
assert_eq!(
unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }.to_string_lossy(),
"hello 你好"
);
// 空串。
let empty = cs("");
assert_eq!(unsafe { oakplugin_instance_set_param_string(h, name.as_ptr(), empty.as_ptr()) }, OK);
assert_eq!(unsafe { oakplugin_instance_get_param_string(h, name.as_ptr(), std::ptr::null_mut(), 0) }, 1);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// render:对测试插件渲一帧,输出纹理像素符合插件的常量填充
/// 0.5 RGBA F32alpha=1)。time 为 NaN 时 E_INVALID。
#[cfg(feature = "test-stubs")]
#[test]
fn instance_render_one_frame() {
common::with_host(|| {
use oakplugin::bridge::render::stub;
let mut h = create_instance();
if h.is_null() {
return;
}
// 输出帧 320×240 F32。
stub::setup_dst(320, 240, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let dst = fake_texture(0xA1);
let src = CHandle::null(); // 无输入(插件只写常量)
assert_eq!(unsafe { oakplugin_instance_render(h, dst, src, 0.0) }, OK);
// 断言:常量 0.5alpha 1.0(每像素 4 通道 × 4 字节 F32)。
let pixels = stub::dst_pixels();
let n = pixels.len() / 16;
assert_eq!(n, 320 * 240, "像素数 = len/16 = {n}");
for i in 0..n {
// 像素基址 = i * 16(4 通道 × 4 字节)。
let f = |o: usize| f32::from_le_bytes(pixels[i * 16 + o * 4..i * 16 + o * 4 + 4].try_into().unwrap());
assert_eq!(f(0), 0.5, "pixel {i} r");
assert_eq!(f(1), 0.5, "pixel {i} g");
assert_eq!(f(2), 0.5, "pixel {i} b");
assert_eq!(f(3), 1.0, "pixel {i} a");
}
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// render 无桩(默认构建):桥符号缺失 → 明确失败码而非崩溃。
#[cfg(not(feature = "test-stubs"))]
#[test]
fn instance_render_one_frame() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let dst = fake_texture(0xA1);
assert_eq!(unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) }, -90003);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// progress 回调:render 期间被调用且进度单调不减;回调返回非 0
/// 时 render 以取消码结束(插件检查 progressUpdate 结果并中止)。
#[cfg(feature = "test-stubs")]
#[test]
fn instance_progress_callback() {
common::with_host(|| {
use oakplugin::bridge::render::stub;
let mut h = create_instance();
if h.is_null() {
return;
}
let mut seen: Vec<f64> = Vec::new();
unsafe extern "C" fn capture(p: f64, userdata: *mut c_void) -> c_int {
let v = unsafe { &mut *(userdata as *mut Vec<f64>) };
v.push(p);
0
}
unsafe {
oakplugin_instance_set_progress_cb(
h,
Some(capture),
&mut seen as *mut _ as *mut c_void,
)
};
stub::setup_dst(64, 64, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let dst = fake_texture(0xA1);
assert_eq!(unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) }, OK);
assert_eq!(seen, vec![0.5], "插件在 render 内报一次 0.5");
// 取消回调(非 0 = 中止)。
unsafe extern "C" fn abort_cb(_p: f64, _u: *mut c_void) -> c_int {
1
}
unsafe { oakplugin_instance_set_progress_cb(h, Some(abort_cb), std::ptr::null_mut()) };
stub::setup_dst(64, 64, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let r = unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) };
assert_eq!(r, -90003, "取消应使 render 失败");
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// cancel:无活动渲染时 no-op;置位后 render 立即以取消码返回。
#[test]
fn instance_cancel_semantics() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
// 无活动渲染:OK no-op。
assert_eq!(unsafe { oakplugin_instance_cancel(h) }, OK);
// 置位后 render 入口即取消(先备好合法输出帧,确保走到
// render 的取消检查而不是帧校验)。
assert_eq!(unsafe { oakplugin_instance_cancel(h) }, OK);
#[cfg(feature = "test-stubs")]
oakplugin::bridge::render::stub::setup_dst(64, 64, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let dst = fake_texture(0xA1);
assert_eq!(unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) }, -90003);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 内省:param_count > 0name/type/label/hint/parent 与插件定义
/// 一致;index 越界 E_NOT_FOUND。
#[test]
fn introspection_param_fields() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let count = unsafe { oakplugin_instance_param_count(h) };
assert!(count >= 4, "gain/mode/debug/labelcount={count}");
// gain 的字段。
assert_eq!(two_stage(oakplugin_instance_param_name, h, 0), Some("gain".into()));
assert_eq!(two_stage(oakplugin_instance_param_type, h, 0), Some("OfxParamTypeDouble".into()));
assert_eq!(two_stage(oakplugin_instance_param_label, h, 0), Some("Gain".into()));
assert_eq!(two_stage(oakplugin_instance_param_hint, h, 0), Some(String::new()));
assert_eq!(two_stage(oakplugin_instance_param_parent, h, 0), Some(String::new()));
// 越界 → E_NOT_FOUND。
assert_eq!(unsafe { oakplugin_instance_param_name(h, count, std::ptr::null_mut(), 0) }, E_NOT_FOUND);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 内省:secret/display_range/choice 三族在测试插件的对应参数上
/// 逐字段断言。
#[test]
fn introspection_secret_range_choice() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let count = unsafe { oakplugin_instance_param_count(h) };
// 按名定位索引。
let mut gain = -1;
let mut mode = -1;
let mut debug = -1;
for i in 0..count {
match two_stage(oakplugin_instance_param_name, h, i).as_deref() {
Some("gain") => gain = i,
Some("mode") => mode = i,
Some("debug") => debug = i,
_ => {}
}
}
assert!(gain >= 0 && mode >= 0 && debug >= 0);
// display rangegain-2..2)。
let (mut min, mut max) = (0.0, 0.0);
assert_eq!(unsafe { oakplugin_instance_param_display_range(h, gain, &mut min, &mut max) }, OK);
assert_eq!((min, max), (-2.0, 2.0));
// secretdebug = 1gain = 0)。
let mut s = 0;
assert_eq!(unsafe { oakplugin_instance_param_secret(h, debug, &mut s) }, OK);
assert_eq!(s, 1);
assert_eq!(unsafe { oakplugin_instance_param_secret(h, gain, &mut s) }, OK);
assert_eq!(s, 0);
// choicemode2 个选项;选项内容见 introspection_choice_labels)。
assert_eq!(unsafe { oakplugin_instance_param_choice_count(h, mode) }, 2);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// choice label/value 的带选项两段式。
#[test]
fn introspection_choice_labels() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let count = unsafe { oakplugin_instance_param_count(h) };
let mut mode = -1;
for i in 0..count {
if two_stage(oakplugin_instance_param_name, h, i).as_deref() == Some("mode") {
mode = i;
}
}
assert!(mode >= 0);
assert_eq!(unsafe { oakplugin_instance_param_choice_count(h, mode) }, 2);
for c in 0..2 {
let len = unsafe { oakplugin_instance_param_choice_label(h, mode, c, std::ptr::null_mut(), 0) };
assert!(len > 0);
let mut buf = vec![0u8; len as usize];
assert_eq!(unsafe { oakplugin_instance_param_choice_label(h, mode, c, buf.as_mut_ptr() as *mut c_char, len) }, OK);
let s = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }.to_string_lossy().into_owned();
assert!(s == "Fast" || s == "High", "选项 {c} = {s}");
}
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 内省:默认值(double 族与字符串)。
#[test]
fn introspection_defaults() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let count = unsafe { oakplugin_instance_param_count(h) };
let mut gain = -1;
let mut label = -1;
for i in 0..count {
match two_stage(oakplugin_instance_param_name, h, i).as_deref() {
Some("gain") => gain = i,
Some("label") => label = i,
_ => {}
}
}
let mut d = 99.0;
assert_eq!(unsafe { oakplugin_instance_param_default_double(h, gain, 0, &mut d) }, OK);
assert_eq!(d, 0.0);
let s = two_stage(oakplugin_instance_param_default_string, h, label);
assert_eq!(s, Some(String::new()));
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 内省:clip_count/name/info(含 Output clipoptional 标记)。
#[test]
fn introspection_clips() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
assert_eq!(unsafe { oakplugin_instance_clip_count(h) }, 2);
let names: Vec<String> = (0..2)
.map(|i| two_stage(oakplugin_instance_clip_name, h, i).unwrap_or_default())
.collect();
assert!(names.contains(&"Source".into()), "{names:?}");
assert!(names.contains(&"Output".into()), "{names:?}");
for i in 0..2 {
let mut optional = -1;
let len = unsafe { oakplugin_instance_clip_info(h, i, &mut optional, std::ptr::null_mut(), 0) };
assert!(len > 0);
let mut buf = vec![0u8; len as usize];
assert_eq!(unsafe { oakplugin_instance_clip_info(h, i, &mut optional, buf.as_mut_ptr() as *mut c_char, len) }, OK);
assert!(optional == 0, "测试插件 clip 非 optional");
}
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// debug_alive_countcreate 增、free 减;全部释放后回零基线。
#[test]
fn alive_count_accounting() {
common::with_host(|| {
unsafe { oakplugin_host_scan_import(std::ptr::null(), 0) };
let base = unsafe { oakplugin_debug_alive_count() };
let mut h = create_instance();
if h.is_null() {
return;
}
assert_eq!(unsafe { oakplugin_debug_alive_count() }, base + 1);
unsafe { oakplugin_instance_free(&mut h) };
assert_eq!(unsafe { oakplugin_debug_alive_count() }, base);
});
}
+337
View File
@@ -0,0 +1,337 @@
// 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/>.
//! OpenGL 渲染路径测试(M11 §4)。
//!
//! GL 测试策略:
//! - `--features test-stubs`:库内桩 GL 渲染器(renderer_is_open_gl /
//! texture_create / texture_id 等)模拟 GPU——suite 往返、GL render
//! 路径、错误路径全链路可跑(无需真实 GPU/liboakrender);
//! - 默认模式(无 liboakrender 符号):GL 决策回退 CPU、GL suite 在
//! 无上下文时返回 kOfxStatErrMissingHostFeature——用例断言该优雅
//! 降级("无 GPU 优雅跳过")。
//! - 真实 GPU goldenEXR 容差比对)为 M11 0 期基建 + 本机人工确认
//! 项:`OAK_GPU_TESTS` 环境变量门,见 common::gpu_available。
mod common;
use std::ffi::{c_char, c_void, CString};
use oakplugin::ffi::{
oakplugin_host_set_message_handler, oakplugin_host_scan, oakplugin_instance_create,
oakplugin_instance_free, oakplugin_instance_render_begin_sequence,
oakplugin_instance_render_end_sequence, oakplugin_instance_render_job,
};
use oakplugin::handle::CHandle;
use oakplugin::suites::gl_render::GlRenderSuiteV1;
use oakplugin::suites::{fetch_suite, status, tag};
const OK: i32 = 0;
const GL_PLUGIN_ID: &str = "org.oak.test-plugin.gl";
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
/// 简易句柄(桩纹理/渲染器)。
fn fake_handle(ctx: usize) -> CHandle {
CHandle {
ctx: ctx as *mut c_void,
addref: None,
release: None,
abi_version: 1,
}
}
/// 扫描测试插件并创建 GL 变体实例;不可用返回空句柄。
fn create_gl_instance() -> CHandle {
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
return CHandle::null();
}
let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap());
let dirs = [dir.as_ptr()];
unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) };
let id = cs(GL_PLUGIN_ID);
unsafe { oakplugin_instance_create(id.as_ptr()) }
}
/// GL suite 表(fetchSuite 分发表)。
fn gl_suite() -> Option<&'static GlRenderSuiteV1> {
unsafe { fetch_suite("OfxImageEffectOpenGLRenderSuite", 1) }
.map(|p| unsafe { &*(p as *const GlRenderSuiteV1) })
}
/// 构造一个带输入纹理的 Source clip(实例期 clip 句柄)。
fn source_clip_handle(tex: CHandle) -> (*mut c_void, std::sync::Arc<oakplugin::clip::ClipInstance>) {
let desc = oakplugin::descriptor::ClipDescriptor::new("Source");
let clip = std::sync::Arc::new(oakplugin::clip::ClipInstance::from_descriptor(&desc));
clip.set_input_texture(tex, 0.0);
let h = tag::make(&clip.props as *const oakplugin::property::PropertySet, tag::CLIP);
(h, clip)
}
/// 当前渲染/GL 上下文注入(suite 的 TLS 依赖)。
fn inject_gl_ctx(renderer: CHandle, output: CHandle) {
oakplugin::suites::set_render_ctx(Some(oakplugin::suites::RenderCtx {
time: 0.0,
scale: oakplugin::instance::RenderScale { x: 1.0, y: 1.0 },
range: oakplugin::instance::OfxRangeD { min: 0.0, max: 1.0 },
}));
oakplugin::suites::set_gl_ctx(Some(oakplugin::suites::GlCtx {
renderer,
output_texture: output,
gl_pixel_depth: "OfxBitDepthFloat",
}));
}
/// 读纹理句柄(属性集)的 int 属性。
fn tex_int(props: &oakplugin::property::PropertySet, name: &str) -> Option<i32> {
match props.get(name, 0)? {
oakplugin::property::Value::Int(i) => Some(i),
_ => None,
}
}
fn tex_string(props: &oakplugin::property::PropertySet, name: &str) -> Option<String> {
match props.get(name, 0)? {
oakplugin::property::Value::String(s) => Some(s.to_string_lossy().into_owned()),
_ => None,
}
}
/// 清理 TLS(每个用例结尾)。
fn clear_ctxs() {
oakplugin::suites::set_gl_ctx(None);
oakplugin::suites::set_render_ctx(None);
}
/// clipLoadTexture 往返:输入 clip 建纹理、属性齐全、clipFreeTexture
/// 释放;Output clip 返回附着输出纹理;flushResources → ReplyDefault。
#[cfg(feature = "test-stubs")]
#[test]
fn clip_load_texture_roundtrip() {
use oakplugin::bridge::render::stub;
common::with_host(|| {
stub::reset();
stub::set_gl_available(true);
let renderer = stub::make_gl_renderer();
let dst = stub::make_gl_texture(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let src = stub::make_gl_texture(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
inject_gl_ctx(renderer, dst);
let suite = gl_suite().expect("GL suite 已注册");
// 输入 clip。
let (clip_h, _clip) = source_clip_handle(src);
let before = stub::gl_texture_ids();
let mut tex: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((suite.clip_load_texture)(clip_h, 0.0, std::ptr::null(), std::ptr::null(), &mut tex), 0);
}
assert!(!tex.is_null(), "应返回纹理属性集句柄");
let props = unsafe { &*(tag::strip(tex) as *const oakplugin::property::PropertySet) };
let idx = tex_int(props, "OfxImageEffectPropOpenGLTextureIndex").expect("texture index");
assert!(idx > 0, "新建纹理 id 为正:{idx}");
assert_eq!(tex_int(props, "OfxImageEffectPropOpenGLTextureTarget"), Some(0x0DE1), "GL_TEXTURE_2D");
assert_eq!(tex_string(props, "OfxImageEffectPropPixelDepth").as_deref(), Some("OfxBitDepthFloat"));
assert_eq!(tex_string(props, "OfxImageEffectPropComponents").as_deref(), Some("OfxImageComponentRGBA"));
assert!(props.get("OfxImagePropBounds", 0).is_some(), "Bounds");
assert!(props.get("OfxImagePropRowBytes", 0).is_some(), "RowBytes");
assert!(props.get("OfxImagePropUniqueIdentifier", 0).is_some(), "UniqueIdentifier");
// 注册表新增一个纹理。
let after = stub::gl_texture_ids();
assert_eq!(after.len(), before.len() + 1, "clipLoadTexture 建新纹理");
assert!(after.contains(&idx), "新纹理 id 在注册表");
// clipFreeTexture:删输入纹理。
unsafe { assert_eq!((suite.clip_free_texture)(tex), 0); }
assert!(!stub::gl_texture_ids().contains(&idx), "输入纹理已删除");
// 重复释放 → BadHandle。
unsafe { assert_eq!((suite.clip_free_texture)(tex), status::ERR_BAD_HANDLE); }
// Output clip:返回附着输出纹理;free 不删纹理(宿主读)。
let mut out: *mut c_void = std::ptr::null_mut();
let out_clip_instance =
oakplugin::clip::ClipInstance::from_descriptor(&oakplugin::descriptor::ClipDescriptor::new("Output"));
let out_clip = tag::make(
&out_clip_instance.props as *const oakplugin::property::PropertySet,
tag::CLIP,
);
unsafe {
assert_eq!((suite.clip_load_texture)(out_clip, 0.0, std::ptr::null(), std::ptr::null(), &mut out), 0);
}
assert!(!out.is_null());
let props = unsafe { &*(tag::strip(out) as *const oakplugin::property::PropertySet) };
let dst_id = tex_int(props, "OfxImageEffectPropOpenGLTextureIndex").unwrap();
assert!(stub::gl_texture_ids().contains(&dst_id), "输出纹理 id 来自附着目标");
unsafe { assert_eq!((suite.clip_free_texture)(out), 0); }
assert!(stub::gl_texture_ids().contains(&dst_id), "Output free 不删纹理");
// flushResources:无宿主缓存 → ReplyDefault。
unsafe { assert_eq!((suite.flush_resources)(), status::REPLY_DEFAULT); }
clear_ctxs();
});
}
/// 错误路径:无 GL 上下文 → MissingHostFeature;空 out/BadHandle
/// 子区域 → Failed;请求分量不匹配 → Failed。
#[cfg(feature = "test-stubs")]
#[test]
fn clip_load_texture_error_paths() {
use oakplugin::bridge::render::stub;
common::with_host(|| {
stub::reset();
stub::set_gl_available(true);
let renderer = stub::make_gl_renderer();
let dst = stub::make_gl_texture(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let src = stub::make_gl_texture(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let suite = gl_suite().expect("GL suite 已注册");
// 无 GL 上下文(非 GL 渲染期)→ MissingHostFeature。
let (clip_h, _clip) = source_clip_handle(src);
let mut tex: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((suite.clip_load_texture)(clip_h, 0.0, std::ptr::null(), std::ptr::null(), &mut tex), status::ERR_MISSING_HOST_FEATURE);
// 空 out → BadHandle;空 clip → BadHandle。
assert_eq!((suite.clip_load_texture)(clip_h, 0.0, std::ptr::null(), std::ptr::null(), std::ptr::null_mut()), status::ERR_BAD_HANDLE);
assert_eq!((suite.clip_load_texture)(std::ptr::null_mut(), 0.0, std::ptr::null(), std::ptr::null(), &mut tex), status::ERR_BAD_HANDLE);
// clipFreeTexture 空句柄 → BadHandle。
assert_eq!((suite.clip_free_texture)(std::ptr::null_mut()), status::ERR_BAD_HANDLE);
}
// 有 GL 上下文后:子区域(Phase 2 不支持)→ Failed。
inject_gl_ctx(renderer, dst);
let region = oakplugin::instance::OfxRectD { x1: 0.0, y1: 0.0, x2: 2.0, y2: 2.0 };
unsafe {
assert_eq!((suite.clip_load_texture)(clip_h, 0.0, std::ptr::null(), &region as *const _ as *const c_void, &mut tex), status::FAILED);
}
// 请求分量不匹配(RGBA 输入 + GLFormatRGB)→ Failed。
let fmt = cs("OfxImageEffectGLFormatRGB");
unsafe {
assert_eq!((suite.clip_load_texture)(clip_h, 0.0, fmt.as_ptr(), std::ptr::null(), &mut tex), status::FAILED);
}
// 匹配请求(GLFormatRGBA)→ OK。
let fmt = cs("OfxImageEffectGLFormatRGBA");
unsafe {
assert_eq!((suite.clip_load_texture)(clip_h, 0.0, fmt.as_ptr(), std::ptr::null(), &mut tex), 0);
assert_eq!((suite.clip_free_texture)(tex), 0);
}
clear_ctxs();
});
}
/// 无桩模式(默认构建):GL suite 在无上下文时返回 MissingHostFeature
/// (优雅降级,不崩);render_job 带渲染器句柄时回退 CPU 路径。
#[cfg(not(feature = "test-stubs"))]
#[test]
fn gl_path_graceful_without_gpu() {
common::with_host(|| {
let suite = gl_suite().expect("GL suite 已注册");
let desc = oakplugin::descriptor::ClipDescriptor::new("Source");
let clip = oakplugin::clip::ClipInstance::from_descriptor(&desc);
let clip_h = tag::make(&clip.props as *const _, tag::CLIP);
let mut tex: *mut c_void = std::ptr::null_mut();
unsafe {
// 无 GL 上下文 → MissingHostFeature(规范语义)。
assert_eq!((suite.clip_load_texture)(clip_h, 0.0, std::ptr::null(), std::ptr::null(), &mut tex), status::ERR_MISSING_HOST_FEATURE);
// flushResources → ReplyDefault。
assert_eq!((suite.flush_resources)(), status::REPLY_DEFAULT);
}
});
}
/// GL render 路径端到端(test-stubs):render_job 带 GL 渲染器 →
/// 插件收到 OpenGLEnabled=1、attach/detach 配对、clipLoadTexture
/// 取 Source/Output 纹理(插件经 message suite 上报索引)。
#[cfg(feature = "test-stubs")]
#[test]
fn gl_render_path_drives_plugin() {
use oakplugin::bridge::render::stub;
common::with_host(|| {
stub::reset();
let mut h = create_gl_instance();
if h.is_null() {
return;
}
// 消息捕获(插件 GL 行为的上报通道)。
let captured: std::sync::Arc<std::sync::Mutex<Vec<String>>> =
std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
{
let cap = captured.clone();
unsafe extern "C" fn capture(
_type: *const c_char,
message: *const c_char,
userdata: *mut c_void,
) -> std::ffi::c_int {
let v = unsafe { &mut *(userdata as *mut std::sync::Mutex<Vec<String>>) };
if !message.is_null() {
let m = unsafe { std::ffi::CStr::from_ptr(message) }
.to_string_lossy()
.into_owned();
v.lock().unwrap_or_else(|e| e.into_inner()).push(m);
}
0
}
unsafe {
oakplugin_host_set_message_handler(
Some(capture),
&*cap as *const _ as *mut c_void,
)
};
}
// GL 环境:渲染器可用;dst/src 均为 GL 纹理(2×2 F32)。
stub::set_gl_available(true);
let renderer = stub::make_gl_renderer();
let dst = stub::make_gl_texture(2, 2, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let src = stub::make_gl_texture(2, 2, oakplugin::bridge::render::PIXEL_FORMAT_F32);
// begin → job → end(序列括号)。
assert_eq!(unsafe { oakplugin_instance_render_begin_sequence(h, 0.0, 1.0, 0) }, OK);
let r = unsafe {
oakplugin_instance_render_job(
h,
dst,
0.0,
0,
0,
cs("Source").as_ptr(),
src,
std::ptr::null(),
0,
std::ptr::null(),
0,
renderer,
)
};
assert_eq!(r, OK, "GL render_job 应成功");
assert_eq!(unsafe { oakplugin_instance_render_end_sequence(h, 0.0, 1.0, 0) }, OK);
// 插件上报:attach/detach 配对、Source 纹理索引、Output 纹理索引。
let msgs = captured.lock().unwrap_or_else(|e| e.into_inner()).clone();
let joined = msgs.join("|");
assert!(joined.contains("gl-attached"), "attach 应调用:{joined}");
assert!(joined.contains("gl-detached"), "detach 应调用:{joined}");
assert!(joined.contains("gl-source-index="), "Source 纹理索引:{joined}");
assert!(joined.contains("gl-output-index="), "Output 纹理索引:{joined}");
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_set_message_handler(None, std::ptr::null_mut()) };
});
}
+118
View File
@@ -0,0 +1,118 @@
// 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/>.
//! Golden master:描述符快照 diff 与渲染帧比对(M11 §2.2/§2.3)。
//!
//! 快照在 0 期由**现行 C++ 实现**抓取入库(tests/ofx/snapshots/ 与
//! tests/ofx/frames/);本文件的测试断言 Rust 实现与之逐字段/逐像素
//! 一致。**0 期基建尚未落地**(tests/ofx/ 目录为空)——依赖快照/
//! 帧/GL 的用例一律 `#[ignore]`,落地后摘掉并补全断言(见
//! [`descriptor_snapshots_match`] 的说明)。无真实 bundle 的环境
//! CI)整文件 skip。
//!
//! [`cimg_full_describe_smoke`] 不依赖快照:真实 CImg bundle 存在时
//! 全量 describe + 协商冒烟(健壮性,不比对),是本次唯一实际执行
//! 的用例。
mod common;
/// 描述符快照:对快照清单中的每个插件,字段级 diff——标识、版本、
/// 上下文集合、参数矩阵(名称/类型/默认值/标签/hint/父组/secret/
/// display range/choice labels+values+排序结果)、clip 表。任何 diff
/// 打印成 unified diff 供定位。
///
/// # ignore 原因(M11 0 期基建缺失)
///
/// `tests/ofx/snapshots/`C++ 实现抓取的描述符 JSON 库)尚未生成。
/// 快照落地后摘掉本注解并实现:扫描快照清单 → 每插件 describe →
/// 与 JSON 逐字段比对。当前无快照可比对,硬跑必假红。
#[test]
#[ignore = "M11 0 期快照库(tests/ofx/snapshots/)尚未生成;落地后实现并摘除"]
fn descriptor_snapshots_match() {
todo!("0 期快照落地后实现:扫描快照清单 → describe → 逐字段 diff")
}
/// clip 协商快照:getClipPreferences 之后的输出分量/位深/像素比/
/// 帧率/field 与快照一致(隐式行为的主要回归防线)。
///
/// # ignore 原因
///
/// 同 [`descriptor_snapshots_match`]:协商快照(含 getClipPreferences
/// 协商后结果)在 `tests/ofx/snapshots/`,尚未生成。
#[test]
#[ignore = "M11 0 期协商快照尚未生成;落地后实现并摘除"]
fn negotiated_clip_preferences_match() {
todo!("0 期协商快照落地后实现")
}
/// CImg 全量插件冒烟:每个插件 describe + 协商不崩、不泄漏
/// (不比对快照,只验健壮性——覆盖快照集之外的怪癖)。
///
/// # ignore 原因(本机 CImg bundle 的宿主兼容缺口)
///
/// 本机安装的 CImg.ofx.bundle 是 Natron 分支构建:describe 期间写
/// 专属属性 `NatronOfxImageEffectPropDeprecated`,且其 OFX support
/// 的 `Property::Set` 句柄语义与本 crate 的打标句柄约定不兼容
/// (插件侧属性写入在进入本宿主属性套件前失败 → describe 返回
/// kOfxStatErrMissingHostFeature)。真实插件的端到端加载/建实例/
/// 协商路径由 [`crate::bridge_test` 的 `system_misc_ofx_bundle_smoke`]
/// Misc.ofx.bundle,标准 openfx-misc 构建)覆盖。CImg 兼容属
/// M11 §3.5「CImg 全量 describe/协商冒烟」的宿主保真缺口——属性
/// 套件适配插件侧 `Property::Set` 句柄后摘除本注解。
#[test]
#[ignore = "本机 CImg.ofx.bundle 为 Natron 分支构建,属性套件句柄语义不兼容(describe 返回 MissingHostFeature);Misc 系统冒烟已覆盖真实插件路径"]
fn cimg_full_describe_smoke() {
todo!("属性套件适配 Natron 分支的 Property::Set 句柄后实现:全量 describe + 协商不崩、不泄漏")
}
/// CPU 渲染 golden:代表性效果集逐帧 SHA256 一致(bit 级)。
/// 输入帧为合成渐变+色块(确定性生成器在 common 里)。
///
/// # ignore 原因
///
/// `tests/ofx/frames/`C++ 实现抓取的 EXR + SHA256 库)尚未生成;
/// 且 CPU 渲染链路依赖 renderer 桥(cargo 内无 liboakrender 真实现,
/// 只有测试桩)。快照与真桥都落地后实现并摘除。
#[test]
#[ignore = "M11 0 期渲染 goldentests/ofx/frames/+ 真 liboakrender 缺失;落地后实现并摘除"]
fn render_golden_cpu_bitexact() {
todo!("0 期渲染 golden 落地后实现")
}
/// GL 渲染 golden1e-4 容差(驱动差异);无 GPU 环境 skip。
///
/// # ignore 原因
///
/// GL 路径属 M11 第 2 期(OpenGLRender suite 未实现),帧库亦未生成。
#[test]
#[ignore = "GL 路径属 M11 第 2 期,且帧库未生成;2 期后实现并摘除"]
fn render_golden_gl_tolerant() {
todo!("M11 第 2 期 GL 路径落地后实现")
}
/// F32+ACEScg 链路断言:golden 帧的像素格式为 F32,且经 OCIO
/// display transform 后与参考值在容差内(色彩链路没被偷换成 8-bit
/// 的回归防线)。
///
/// # ignore 原因
///
/// 同 [`render_golden_cpu_bitexact`]:依赖帧库与 OCIO 链路
/// ofxColour 属 M11 第 2 期)。
#[test]
#[ignore = "M11 0 期帧库 + 第 2 期 ofxColour/OCIO 链路缺失;落地后实现并摘除"]
fn pipeline_is_f32_acescg() {
todo!("帧库与 OCIO 链路落地后实现")
}
+239
View File
@@ -0,0 +1,239 @@
// 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/>.
//! handle.rs 的契约测试:引用计数语义、free 容错、借用盒、Registry。
//!
//! 对应实现:crate::handle。每个测试只验一条规则,命名即规约。
mod common;
use std::ptr;
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use std::sync::Arc;
use oakplugin::error::{Error, OAKPLUGIN_E_FAILED, OAKPLUGIN_E_NOT_FOUND, OAKPLUGIN_OK};
use oakplugin::handle::{
get, guard, guard_handle, make_borrowed, make_owned, RefBox, Registry, CHandle,
};
/// 析构标志:以"被析构次数"断言对象的销毁时机(引用计数语义的
/// 行为探针)。
struct DropFlag(Arc<AtomicUsize>);
impl Drop for DropFlag {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
/// 模拟 C 侧 addref(头文件契约:复制句柄时先 addref)。
fn addref(h: &CHandle) {
unsafe { (h.addref.expect("addref fn 缺失"))(h.ctx) };
}
/// 模拟 C 侧 free`oakplugin_instance_free` 语义:ctx 非空才调
/// release,随后清空 ctx;空句柄/已清空句柄是 no-op)。
fn free(h: &mut CHandle) {
if !h.is_null() {
unsafe { (h.release.expect("release fn 缺失"))(h.ctx) };
}
h.ctx = ptr::null_mut();
}
/// 拥有型句柄:创建计数为 1addref 后 release 一次对象仍活;
/// 再 release 对象销毁(用析构标志位断言)。
#[test]
fn owned_handle_refcount_lifecycle() {
let drops = Arc::new(AtomicUsize::new(0));
// 按值传句柄的位级复制(C 侧 `OakPluginInstance` 结构体拷贝),
// 复制方必须先 addrefrefs 1 -> 2。
let h1 = make_owned(DropFlag(drops.clone()));
let mut h2 = unsafe { ptr::read(&h1) };
addref(&h2);
// 释放一份:2 -> 1,对象仍活。
let mut h1 = h1;
free(&mut h1);
assert_eq!(drops.load(Ordering::Relaxed), 0);
// 释放最后一份:1 -> 0,对象恰好销毁一次。
free(&mut h2);
assert_eq!(drops.load(Ordering::Relaxed), 1);
}
/// free(NULL)/free(空句柄)/重复 free 同一个已清空句柄:全部 no-op,
/// 不崩、不计数变化(alive 计数前后一致)。
#[test]
fn free_null_and_empty_is_noop() {
let drops = Arc::new(AtomicUsize::new(0));
// free(空句柄)no-op,不崩。
let mut null = CHandle::null();
free(&mut null);
assert!(null.is_null());
// 正常销毁后句柄已清空;重复 free 是 no-op,计数不再变化。
let mut h = make_owned(DropFlag(drops.clone()));
free(&mut h);
assert_eq!(drops.load(Ordering::Relaxed), 1);
free(&mut h);
free(&mut h);
assert_eq!(drops.load(Ordering::Relaxed), 1);
}
/// 借用句柄:release 只释放盒子,被借用的对象仍然存活
/// (用外部栈对象的析构标志断言)。
#[test]
fn borrowed_handle_never_destroys_object() {
let drops = Arc::new(AtomicUsize::new(0));
let mut obj = DropFlag(drops.clone());
let mut h = unsafe { make_borrowed(&mut obj as *mut DropFlag) };
assert!(!h.is_null());
free(&mut h);
// 盒子释放了,但对象归调用方:析构不在 release 时发生。
assert_eq!(drops.load(Ordering::Relaxed), 0);
assert!(h.is_null());
// 对象最终由调用方析构,恰好一次。
drop(obj);
assert_eq!(drops.load(Ordering::Relaxed), 1);
}
/// 空句柄的 get::<T>() 返回 None;类型不符的 get 是调用方责任
/// (文档约定),此处只验空句柄路径。
#[test]
fn get_on_empty_handle_is_none() {
assert!(unsafe { get::<u32>(&CHandle::null()) }.is_none());
// 对照:正常句柄能取回引用。
let mut h = make_owned(42u32);
assert_eq!(unsafe { get::<u32>(&h) }, Some(&42));
free(&mut h);
}
/// guard:闭包 panic 被捕获并映射为 OAKPLUGIN_E_FAILED
/// 不 unwind 出 FFIErr 映射为对应负码;Ok 映射为 OAKPLUGIN_OK。
#[test]
fn guard_maps_panic_err_ok() {
// Ok -> OAKPLUGIN_OK。
assert_eq!(guard(|| Ok(())), OAKPLUGIN_OK);
// Err -> 对应负码(错误码与 include/plugin/error.h 一致,
// 项目 -MMCCCC 方案:-90001..-90005)。
assert_eq!(guard(|| Err(Error::NotFound)), OAKPLUGIN_E_NOT_FOUND);
assert_eq!(
guard(|| Err(Error::Invalid)),
oakplugin::error::OAKPLUGIN_E_INVALID
);
assert_eq!(guard(|| Err(Error::State)), oakplugin::error::OAKPLUGIN_E_STATE);
assert_eq!(
guard(|| Err(Error::Failed("x".into()))),
oakplugin::error::OAKPLUGIN_E_FAILED
);
assert_eq!(guard(|| Err(Error::NoMem)), oakplugin::error::OAKPLUGIN_E_NOMEM);
// panic -> OAKPLUGIN_E_FAILED,且 panic 不越过 guard 边界
// catch_unwind 语义:本测试线程存活即证明未 unwind)。
assert_eq!(guard(|| panic!("boom")), OAKPLUGIN_E_FAILED);
}
/// guard_handlepanic/Err 返回空句柄;Ok 透传非空句柄。
#[test]
fn guard_handle_maps_to_null_on_failure() {
let mut ok = guard_handle(|| Ok(make_owned(7u32)));
assert!(!ok.is_null());
free(&mut ok);
let err = guard_handle(|| Err::<CHandle, _>(Error::State));
assert!(err.is_null());
let panicked: CHandle = guard_handle(|| panic!("boom"));
assert!(panicked.is_null());
}
/// Registryregister 返回唯一身份;lookup 命中;对象销毁后
/// lookup 返回 None(弱引用语义);unregister 未知身份 no-op。
#[test]
fn registry_register_lookup_unregister() {
let reg: Registry<u32> = Registry::new();
let arc = Arc::new(RefBox {
refs: AtomicU32::new(1),
value: 7u32,
});
let id = reg.register(&arc);
assert_eq!(id, Arc::as_ptr(&arc) as *const () as usize);
assert_eq!(reg.lookup(id).unwrap().value, 7);
// 同一对象重复登记:身份稳定(地址语义),值覆盖。
let id2 = reg.register(&arc);
assert_eq!(id2, id);
// 摘除后 lookup 命中失败;再摘除未知身份是 no-op。
reg.unregister(id);
assert!(reg.lookup(id).is_none());
reg.unregister(id);
// 对象销毁后弱引用失效:lookup 返回 None。
let dying = Arc::new(RefBox {
refs: AtomicU32::new(1),
value: 9u32,
});
let dying_id = reg.register(&dying);
drop(dying);
assert!(reg.lookup(dying_id).is_none());
drop(arc);
}
/// 并发:64 线程对同一句柄 addref/release 各一千次,最终计数正确、
/// 对象恰好销毁一次(线程模型是 multithread suite 的直接投影)。
#[test]
fn refcount_is_thread_safe() {
let drops = Arc::new(AtomicUsize::new(0));
let h = make_owned(DropFlag(drops.clone()));
// 每个线程持有 ctx + 函数指针的拷贝(对应 C 侧各线程各持一份
// 句柄值),对同一对象做 1000 次 addref/release 配对。
// 裸指针不可 Send,测试侧经 usize 搬运(C 侧本来也是整数传递)。
let threads: Vec<_> = (0..64)
.map(|_| {
let ctx = h.ctx as usize;
let addref = h.addref.expect("addref fn 缺失");
let release = h.release.expect("release fn 缺失");
std::thread::spawn(move || {
for _ in 0..1000 {
unsafe { addref(ctx as *mut std::ffi::c_void) };
unsafe { release(ctx as *mut std::ffi::c_void) };
}
})
})
.collect();
for t in threads {
t.join().unwrap();
}
// 全部配对完成:计数回到创建值,对象存活。
assert_eq!(drops.load(Ordering::Relaxed), 0);
// 最终一次 release 恰好销毁。
let mut h = h;
free(&mut h);
assert_eq!(drops.load(Ordering::Relaxed), 1);
}
+238
View File
@@ -0,0 +1,238 @@
// 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/>.
//! 生命周期测试:实例创建/销毁配对、重复扫描、渲染中取消、
//! alive 泄漏断言。对应 M11 §3.5 验收线。
//!
//! 宿主单例无锁:全部用例经 [`common::with_host`] 串行化。
mod common;
use std::ffi::{c_char, c_int, c_void, CString};
use oakplugin::ffi::{
oakplugin_debug_alive_count, oakplugin_host_init, oakplugin_host_plugin_count,
oakplugin_host_scan, oakplugin_host_shutdown, oakplugin_instance_cancel,
oakplugin_instance_create, oakplugin_instance_free, oakplugin_instance_render,
oakplugin_instance_set_progress_cb,
};
use oakplugin::handle::CHandle;
const OK: i32 = 0;
const E_FAILED: i32 = -90003;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
fn fake_texture(ctx: usize) -> CHandle {
CHandle {
ctx: ctx as *mut c_void,
addref: None,
release: None,
abi_version: 1,
}
}
/// 扫描并创建实例;不可用返回空句柄。
fn create_instance() -> CHandle {
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
return CHandle::null();
}
let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap());
let dirs = [dir.as_ptr()];
unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) };
let id = cs(TEST_PLUGIN_ID);
unsafe { oakplugin_instance_create(id.as_ptr()) }
}
/// createInstance → destroyInstance 严格配对:alive 计数回到基线。
#[test]
fn instance_create_destroy_pairing() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let base = unsafe { oakplugin_debug_alive_count() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
assert_eq!(unsafe { oakplugin_debug_alive_count() }, base + 1);
unsafe { oakplugin_instance_free(&mut h) };
assert_eq!(unsafe { oakplugin_debug_alive_count() }, base, "destroy 后 alive 必须回基线");
unsafe { oakplugin_host_shutdown() };
});
}
/// 重复 scan 同一路径:插件列表不重复、顺序稳定(缓存去重语义,
/// HS: ofxhPluginCache.cpp)。
#[test]
fn rescan_is_idempotent() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
unsafe { oakplugin_host_shutdown() };
return;
}
let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap());
let dirs = [dir.as_ptr()];
assert_eq!(unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }, OK);
let first = unsafe { oakplugin_host_plugin_count() };
assert!(first >= 1);
// 再扫两次:数量不变。
assert_eq!(unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }, OK);
assert_eq!(unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }, OK);
assert_eq!(unsafe { oakplugin_host_plugin_count() }, first, "重复扫描不得重复加载");
unsafe { oakplugin_host_shutdown() };
});
}
/// render 中途取消:进度回调返回非 0 后,插件的 progressUpdate 收到
/// 取消并中止 render;输出纹理不写半帧(测试插件在取消路径返回
/// Failed,宿主不落帧)。
#[cfg(feature = "test-stubs")]
#[test]
fn render_cancellation_is_atomic() {
common::with_host(|| {
use oakplugin::bridge::render::stub;
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
unsafe extern "C" fn abort_cb(_p: f64, _u: *mut c_void) -> c_int {
1
}
unsafe { oakplugin_instance_set_progress_cb(h, Some(abort_cb), std::ptr::null_mut()) };
// 预置输出帧并填 0xAA 哨兵:取消路径不得写半帧。
stub::setup_dst(64, 64, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let before = stub::dst_pixels();
assert!(!before.is_empty());
let dst = fake_texture(0xA1);
let r = unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) };
assert_eq!(r, E_FAILED, "取消应使 render 失败");
// 全有或全无:失败时不写。
assert_eq!(stub::dst_pixels(), before, "取消后输出不得被写");
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// 压测:同一插件 256 次 create/render/destroy 循环后 alive 回到
/// 基线(当年内存问题的回归防线)。
#[cfg(feature = "test-stubs")]
#[test]
fn create_render_destroy_loop_no_leak() {
common::with_host(|| {
use oakplugin::bridge::render::stub;
unsafe { oakplugin_host_init() };
let base = unsafe { oakplugin_debug_alive_count() };
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
unsafe { oakplugin_host_shutdown() };
return;
}
stub::setup_dst(16, 16, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let dst = fake_texture(0xA1);
for i in 0..256 {
let mut h = create_instance();
assert!(!h.is_null());
let r = unsafe { oakplugin_instance_render(h, dst, CHandle::null(), i as f64) };
assert_eq!(r, OK);
unsafe { oakplugin_instance_free(&mut h) };
assert_eq!(
unsafe { oakplugin_debug_alive_count() },
base,
"第 {i} 次循环后泄漏"
);
}
unsafe { oakplugin_host_shutdown() };
});
}
/// host_shutdown 时仍有活实例:宿主发 destroy 通知并卸载 bundle
/// 之后该实例的任何 render 立即取消(不碰已卸载入口);再 create
/// 返回空句柄(缓存已清空——声明原期望 E_STATE,本实现的 create
/// 契约是"失败返回空句柄",以实例化为准)。
#[test]
fn shutdown_with_live_instances() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
// 带活实例 shutdown:不崩;实例被打取消标记。
unsafe { oakplugin_host_shutdown() };
// C 侧句柄仍持 Arc:实例内存存活,但 render 必须失败。
let dst = fake_texture(0xA1);
assert_eq!(unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) }, E_FAILED);
unsafe { oakplugin_instance_free(&mut h) };
assert_eq!(unsafe { oakplugin_debug_alive_count() }, 0, "句柄释放后归零");
// 之后再 create:插件已卸载 → 空句柄。
let id = cs(TEST_PLUGIN_ID);
let h2 = unsafe { oakplugin_instance_create(id.as_ptr()) };
assert!(h2.is_null());
});
}
/// 多实例并发:同一插件 16 实例并发 render(宿主侧不串——插件
/// 全局状态由插件自保)。
#[cfg(feature = "test-stubs")]
#[test]
fn concurrent_instances_render() {
common::with_host(|| {
use oakplugin::bridge::render::stub;
unsafe { oakplugin_host_init() };
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
unsafe { oakplugin_host_shutdown() };
return;
}
// 16 个实例(句柄数组;经线程并发 render)。
let mut handles: Vec<CHandle> = (0..16)
.map(|_| create_instance())
.collect();
assert!(handles.iter().all(|h| !h.is_null()));
stub::setup_dst(32, 32, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let dst = fake_texture(0xA1);
let threads: Vec<_> = handles
.iter()
.enumerate()
.map(|(i, h)| {
let h = *h;
std::thread::spawn(move || unsafe {
oakplugin_instance_render(h, dst, CHandle::null(), i as f64)
})
})
.collect();
for t in threads {
assert_eq!(t.join().unwrap(), OK);
}
for h in handles.iter_mut() {
unsafe { oakplugin_instance_free(h) };
}
assert_eq!(unsafe { oakplugin_debug_alive_count() }, 0);
unsafe { oakplugin_host_shutdown() };
});
}
+230
View File
@@ -0,0 +1,230 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! 协商语义专项:clip 偏好协商矩阵、RoD/RoI、isIdentity、field
//! 透传。当年调试重灾区,每条对照 HostSupport 行为(注释标行号)。
//!
//! 最小测试插件固定声明 RGBA/F32、实现 RoD1920×1080)与 RoI
//! (回写 region)、isIdentity 恒非透传、无 field——需要插件变体
//! 的矩阵案(回退链、身份透传、field)标记 `// TODO(plugin)`
//! 随 M11 §2.4 测试插件的变体落地。
mod common;
use std::ffi::{c_char, c_void, CString};
use oakplugin::ffi::{
oakplugin_host_init, oakplugin_host_scan, oakplugin_host_shutdown, oakplugin_instance_create,
oakplugin_instance_free,
};
use oakplugin::handle::{get, CHandle};
const OK: i32 = 0;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
/// 扫描并创建实例;不可用返回空句柄。
fn create_instance() -> CHandle {
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
return CHandle::null();
}
let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap());
let dirs = [dir.as_ptr()];
unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) };
let id = cs(TEST_PLUGIN_ID);
unsafe { oakplugin_instance_create(id.as_ptr()) }
}
/// 句柄 → Instance 引用。
fn instance_of(h: &CHandle) -> Option<std::sync::Arc<oakplugin::handle::RefBox<oakplugin::instance::Instance>>> {
unsafe { get::<std::sync::Arc<oakplugin::handle::RefBox<oakplugin::instance::Instance>>>(h) }
.cloned()
}
/// 协商顺序:测试插件声明 RGBA/F32、帧率 24——协商结果必须
/// 原样采纳(HS: ofxhImageEffect.cpp:1686-1740 的回灌路径)。
/// 全组合矩阵(RGBA/RGB/Alpha × Float/Half/Byte)需要插件变体:
/// `// TODO(plugin)`。
#[test]
fn clip_preferences_component_depth_matrix() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let prefs = inst.value.get_clip_preferences().expect("协商应成功");
assert_eq!(prefs.output_components, "OfxImageComponentRGBA");
assert_eq!(prefs.output_bit_depth, "OfxBitDepthFloat");
assert_eq!(prefs.frame_rate, 24.0);
// 回灌:clip 实例属性带上了协商结果(clipGetPropertySet 读)。
let output = inst.value.clips.iter().find(|c| c.name == "Output").unwrap();
assert_eq!(
output
.props
.get("OfxImageEffectPropComponents", 0)
.map(|v| format!("{v:?}")),
Some("String(\"OfxImageComponentRGBA\")".into())
);
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// 位深回退链(Half→Byte):宿主当前只宣告 F32 支持、插件声明
/// F32——直通。回退链的插件侧变体 `// TODO(plugin)`。
#[test]
fn bit_depth_fallback_chain() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let prefs = inst.value.get_clip_preferences().unwrap();
assert_eq!(prefs.output_bit_depth, "OfxBitDepthFloat");
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// RoD:插件实现 getRegionOfDefinition → 用插件值(1920×1080
/// HS: ofxhImageEffect.cpp:1087-1130 的 out args 读取)。
/// "未实现时用输入 RoD 并集"的默认语义需要插件变体:`// TODO(plugin)`。
#[test]
fn region_of_definition_default_and_override() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let rod = inst
.value
.get_region_of_definition(0.0, oakplugin::instance::RenderScale { x: 1.0, y: 1.0 })
.expect("getRoD 应成功");
assert_eq!((rod.x1, rod.y1, rod.x2, rod.y2), (0.0, 0.0, 1920.0, 1080.0));
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// RoI:插件把 region 原样写回 Source 的 per-clip 属性
/// "OfxImageEffectPropRegionOfInterest_Source");返回值与
/// 请求 region 一致(HS: getRegionsOfInterest 的 per-clip 前缀)。
#[test]
fn regions_of_interest_writeback() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let region = oakplugin::instance::OfxRectD { x1: 10.0, y1: 20.0, x2: 100.0, y2: 200.0 };
let rois = inst
.value
.get_regions_of_interest(
0.0,
oakplugin::instance::RenderScale { x: 1.0, y: 1.0 },
region,
)
.expect("getRoI 应成功");
assert_eq!(rois.len(), 2, "clips 数与 RoI 数一致");
// RoI 只对输入 clip 有意义:Source 回写 regionOutput 是
// 宿主预定义的默认(零),渲染驱动只用输入条目。
let source = rois
.get(inst.value.clips.iter().position(|c| c.name == "Source").unwrap())
.unwrap();
assert_eq!((source.x1, source.y1, source.x2, source.y2), (10.0, 20.0, 100.0, 200.0));
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// isIdentity:测试插件恒非透传 → None(render 必须真正执行)。
/// 透传短路(Some)需要插件变体:`// TODO(plugin)`。
#[test]
fn is_identity_shortcircuit() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let identity = inst.value.is_identity(1.5).expect("isIdentity 应成功");
assert!(identity.is_none(), "测试插件恒非透传:{identity:?}");
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// field 透传:插件声明的 field orderOfxFieldNone)原样透传到
/// 协商结果(宿主只透传不处理)。多值矩阵 `// TODO(plugin)`。
#[test]
fn field_passthrough() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let prefs = inst.value.get_clip_preferences().unwrap();
assert_eq!(prefs.field, "OfxFieldNone", "插件声明的 field 应透传");
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// begin/endSequenceRender:配对调用成功;begin 后 timeline
/// getTimeBounds 返回该序列范围(sequence_range 接线)。
#[test]
fn sequence_render_brackets() {
common::with_host(|| {
use oakplugin::instance::OfxRangeD;
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
let inst = instance_of(&h).expect("句柄应可解析");
let range = OfxRangeD { min: 10.0, max: 200.0 };
assert!(inst.value.begin_sequence_render(range).is_ok());
assert!(inst.value.end_sequence_render(range).is_ok());
// begin → timeline 上下文带范围(经 render 设置;单测直达
// RenderCtx 的接线见 suites::timeline 测试)。
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
+195
View File
@@ -0,0 +1,195 @@
// 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/>.
//! property.rs 的契约测试:OFX 属性集语义。
//!
//! 参照:HS: ofxhProperty.cpp。语义要点:多维数组、越界/缺失/
/// 类型不符的行为、define 替换语义。
mod common;
use std::ffi::{c_void, CString};
use std::sync::Arc;
use oakplugin::error::Error;
use oakplugin::property::{PropertySet, Value};
/// `Value` 未实现 PartialEqPointer 无法比较),测试用 Debug 串
/// 比较做值相等断言(同进程内指针的 Debug 输出确定)。
fn val_eq(a: &Value, b: &Value) -> bool {
format!("{a:?}") == format!("{b:?}")
}
/// define 后 get 命中;同名 define 整体替换值数组(维度随之变化)。
#[test]
fn define_and_replace() {
let s = PropertySet::new();
s.define("a", vec![Value::Int(1), Value::Int(2)]);
assert_eq!(s.dimension("a"), 2);
assert!(val_eq(&s.get("a", 0).unwrap(), &Value::Int(1)));
// 同名整体替换:旧值数组连同维度一起被覆盖。
s.define("a", vec![Value::Int(9)]);
assert_eq!(s.dimension("a"), 1);
assert!(val_eq(&s.get("a", 0).unwrap(), &Value::Int(9)));
assert!(s.get("a", 1).is_none());
}
/// 未定义属性的 get 返回 Nonedimension 为 0。
#[test]
fn missing_property() {
let s = PropertySet::new();
assert!(s.get("nope", 0).is_none());
assert!(s.get("nope", 42).is_none());
assert_eq!(s.dimension("nope"), 0);
}
/// 越界读取(index >= dimension)返回 None。
#[test]
fn out_of_bounds_read() {
let s = PropertySet::new();
s.define("a", vec![Value::Int(1)]);
assert!(s.get("a", 1).is_none());
assert!(s.get("a", 100).is_none());
}
/// set_at 覆盖指定下标且不改维度;对缺失属性返回 NotFound 错误。
#[test]
fn set_at_semantics() {
let s = PropertySet::new();
s.define("a", vec![Value::Int(1), Value::Int(2)]);
// 覆盖命中下标,维度不变。
assert!(s.set_at("a", 0, Value::Int(9)).is_ok());
assert!(val_eq(&s.get("a", 0).unwrap(), &Value::Int(9)));
assert_eq!(s.dimension("a"), 2);
// 越界写:维度固定语义(不自动扩容)→ NotFound。
assert!(matches!(s.set_at("a", 2, Value::Int(3)), Err(Error::NotFound)));
assert_eq!(s.dimension("a"), 2);
// 缺失属性:NotFound。
assert!(matches!(s.set_at("b", 0, Value::Int(1)), Err(Error::NotFound)));
}
/// 四种值类型(Int/Double/String/Pointer)各自的存取 round-trip
/// 字符串含空串与 UTF-8 非 ASCII。
#[test]
fn typed_roundtrip() {
let s = PropertySet::new();
let non_ascii = CString::new("你好,wörld ✓").unwrap();
let raw = 0x1234usize as *mut c_void;
s.define("ints", vec![Value::Int(-7)]);
s.define("dbls", vec![Value::Double(1.5)]);
s.define("strs", vec![Value::String(non_ascii.clone())]);
s.define("ptrs", vec![Value::Pointer(raw)]);
assert!(val_eq(&s.get("ints", 0).unwrap(), &Value::Int(-7)));
assert!(val_eq(&s.get("dbls", 0).unwrap(), &Value::Double(1.5)));
assert!(val_eq(&s.get("strs", 0).unwrap(), &Value::String(non_ascii)));
assert!(val_eq(&s.get("ptrs", 0).unwrap(), &Value::Pointer(raw)));
// 空串也是合法属性值。
s.set_one("empty", Value::String(CString::new("").unwrap()));
assert!(val_eq(
&s.get("empty", 0).unwrap(),
&Value::String(CString::new("").unwrap())
));
}
/// remove 后属性消失;remove 缺失属性 no-op。
#[test]
fn remove_semantics() {
let s = PropertySet::new();
s.set_one("a", Value::Int(1));
s.remove("a");
assert!(s.get("a", 0).is_none());
assert_eq!(s.dimension("a"), 0);
// 缺失属性 removeno-op,不崩。
s.remove("a");
s.remove("never-existed");
}
/// snapshot 返回全量快照且与后续修改隔离(深拷贝)。
#[test]
fn snapshot_is_isolated() {
let s = PropertySet::new();
s.define("a", vec![Value::Int(1)]);
let snap = s.snapshot();
assert_eq!(snap.len(), 1);
assert_eq!(snap[0].name, "a");
// 后续对属性集的修改不影响快照。
s.set_at("a", 0, Value::Int(2)).unwrap();
s.set_one("b", Value::Int(3));
assert_eq!(snap.len(), 1);
assert!(val_eq(&snap[0].values[0], &Value::Int(1)));
// 反向隔离:改快照不回写属性集(深拷贝验证)。
let mut snap = snap;
snap[0].values[0] = Value::Int(99);
assert!(val_eq(&s.get("a", 0).unwrap(), &Value::Int(2)));
}
/// 并发读写(32 线程 define/get/set_at 交错)不崩、数据自洽
/// (属性集被所有 suite 共享,必须线程安全)。
#[test]
fn concurrent_access() {
// 线程要求 'static:经 Arc 共享属性集(对应插件线程经句柄共享)。
let s = Arc::new(PropertySet::new());
let names: Vec<&'static str> = (0..32).map(leak_name).collect();
for (i, name) in names.iter().enumerate() {
s.set_one(name, Value::Int(i as i32));
}
let threads: Vec<_> = (0..32usize)
.map(|i| {
let s = Arc::clone(&s);
// &'static str 是 Copy:拷贝进闭包,线程各自持名。
let name = names[i];
std::thread::spawn(move || {
for k in 0..500 {
// 读永远合法(属性始终存在、维度恒为 1)。
assert!(s.get(name, 0).is_some());
if i % 2 == 0 {
// 偶数线程:set_at 覆盖(不改维度)。
s.set_at(name, 0, Value::Int((i + k) as i32)).unwrap();
} else {
// 奇数线程:define 整体替换(维度同样恒为 1)。
s.define(name, vec![Value::Int((i as i32) - k as i32)]);
}
}
})
})
.collect();
for t in threads {
t.join().unwrap();
}
// 数据自洽:32 个属性全部存活、维度未被撑大。
for (i, name) in names.iter().enumerate() {
assert_eq!(s.dimension(name), 1);
assert!(s.get(name, 0).is_some());
}
}
/// 泄露一个 `&'static str` 属性名(并发测试用;测试进程结束即回收)。
fn leak_name(i: usize) -> &'static str {
Box::leak(format!("p{i}").into_boxed_str())
}
+248
View File
@@ -0,0 +1,248 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! render 驱动测试(M11 §4):pluginrenderer.cpp 语义收编的 CPU 路径
//! ——序列括号、render_job 多输入/参数覆盖、isIdentity 短路、输出
//! 装配像素断言。GL 路径见 gl_render_test.rs。
mod common;
use std::ffi::{c_char, c_int, c_void, CString};
use oakplugin::ffi::{
oakplugin_host_scan, oakplugin_instance_create, oakplugin_instance_free,
oakplugin_instance_get_param, oakplugin_instance_render_begin_sequence,
oakplugin_instance_render_end_sequence, oakplugin_instance_render_job,
OakPluginJobTexture, OakPluginJobValue, OakNodeValue, node_value_type,
};
use oakplugin::handle::CHandle;
const OK: i32 = 0;
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
const IDENTITY_PLUGIN_ID: &str = "org.oak.test-plugin.identity";
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
fn fake_texture(ctx: usize) -> CHandle {
CHandle {
ctx: ctx as *mut c_void,
addref: None,
release: None,
abi_version: 1,
}
}
fn scan_and_create(id: &str) -> CHandle {
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
return CHandle::null();
}
let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap());
let dirs = [dir.as_ptr()];
unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) };
let id = cs(id);
unsafe { oakplugin_instance_create(id.as_ptr()) }
}
/// 序列括号 + render_jobCPU):begin → job → end;输出像素为测试
/// 插件常量填充(0.5 RGBA F32alpha=1);RoI 计算成功。
#[cfg(feature = "test-stubs")]
#[test]
fn render_job_cpu_path_and_sequence_brackets() {
use oakplugin::bridge::render::stub;
common::with_host(|| {
stub::reset();
let mut h = scan_and_create(TEST_PLUGIN_ID);
if h.is_null() {
return;
}
stub::setup_dst(8, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let dst = fake_texture(0xA1);
// 未 begin 序列直接 job 也可用(单帧;序列括号是优化语义)。
assert_eq!(
unsafe { oakplugin_instance_render_job(h, dst, 0.0, 0, 0, std::ptr::null(), CHandle::null(), std::ptr::null(), 0, std::ptr::null(), 0, CHandle::null()) },
OK
);
// begin/end 括号配对。
assert_eq!(unsafe { oakplugin_instance_render_begin_sequence(h, 0.0, 10.0, 0) }, OK);
assert_eq!(unsafe { oakplugin_instance_render_end_sequence(h, 0.0, 10.0, 0) }, OK);
// 像素断言:8×4 常量 0.5 / alpha 1。
let pixels = stub::dst_pixels();
let n = pixels.len() / 16;
assert_eq!(n, 32);
for i in 0..n {
let f = |o: usize| f32::from_le_bytes(pixels[i * 16 + o * 4..i * 16 + o * 4 + 4].try_into().unwrap());
assert_eq!(f(0), 0.5, "pixel {i} r");
assert_eq!(f(3), 1.0, "pixel {i} a");
}
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 多输入 + 参数覆盖:inputs 表按 clip 名挂输入纹理;values 覆盖
/// gain 参数(驱动注入实例参数,插件经 param 读取——测试插件 render
/// 不读 gain,故断言回写成功即可)。
#[cfg(feature = "test-stubs")]
#[test]
fn render_job_multi_input_and_param_overrides() {
use oakplugin::bridge::render::stub;
common::with_host(|| {
stub::reset();
let mut h = scan_and_create(TEST_PLUGIN_ID);
if h.is_null() {
return;
}
stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
stub::setup_src(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32, vec![0u8; 4 * 4 * 16]);
let dst = fake_texture(0xA1);
let src = fake_texture(0xA2);
// 参数覆盖:gain = 1.5FLOAT)。
let mut value = OakNodeValue::default();
value.r#type = node_value_type::FLOAT;
value.f = [1.5, 0.0, 0.0, 0.0];
let gain = cs("gain");
let job_values = [OakPluginJobValue {
key: gain.as_ptr(),
value,
}];
// 多输入表:Source → src 纹理。
let source = cs("Source");
let job_inputs = [OakPluginJobTexture {
clip: source.as_ptr(),
texture: src,
}];
assert_eq!(
unsafe {
oakplugin_instance_render_job(
h,
dst,
0.0,
0,
0,
std::ptr::null(),
CHandle::null(),
job_inputs.as_ptr(),
1,
job_values.as_ptr(),
1,
CHandle::null(),
)
},
OK
);
// 覆盖已生效:经 C ABI 读回 gain。
let mut out = OakNodeValue::default();
let g = cs("gain");
assert_eq!(unsafe { oakplugin_instance_get_param(h, g.as_ptr(), &mut out) }, OK);
assert_eq!(out.f[0], 1.5, "gain 覆盖应已注入实例参数");
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// isIdentity 短路:identity 变体插件返回 "Source" → render_job 不调
/// render action,直接把 Source 的帧拷入输出(像素与输入一致)。
#[cfg(feature = "test-stubs")]
#[test]
fn render_job_is_identity_shortcircuit() {
use oakplugin::bridge::render::stub;
common::with_host(|| {
stub::reset();
let mut h = scan_and_create(IDENTITY_PLUGIN_ID);
if h.is_null() {
return;
}
// 输入帧:每像素 r=0.1,g=0.2,b=0.3,a=1.0。
let mut pixels = Vec::new();
for _ in 0..4 * 4 {
for v in [0.1f32, 0.2, 0.3, 1.0] {
pixels.extend_from_slice(&v.to_le_bytes());
}
}
stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
stub::setup_src(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32, pixels.clone());
let dst = fake_texture(0xA1);
let src = fake_texture(0xA2);
assert_eq!(
unsafe { oakplugin_instance_render_job(h, dst, 0.0, 0, 0, cs("Source").as_ptr(), src, std::ptr::null(), 0, std::ptr::null(), 0, CHandle::null()) },
OK
);
// 输出 == 输入(透传帧)。
let out_pixels = stub::dst_pixels();
assert_eq!(out_pixels, pixels, "isIdentity 透传应逐字节拷贝输入帧");
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 默认构建(无桩):render_job 缺桥符号 → 明确失败码而非崩溃
/// (优雅降级;GL 用例同)。
#[cfg(not(feature = "test-stubs"))]
#[test]
fn render_job_graceful_without_bridge() {
common::with_host(|| {
let mut h = scan_and_create(TEST_PLUGIN_ID);
if h.is_null() {
return;
}
let dst = fake_texture(0xA1);
let r = unsafe {
oakplugin_instance_render_job(h, dst, 0.0, 0, 0, std::ptr::null(), CHandle::null(), std::ptr::null(), 0, std::ptr::null(), 0, CHandle::null())
};
assert_eq!(r, -90003, "无 liboakrender 时输出纹理无 CPU 帧 → E_FAILED");
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 驱动错误路径:非 F32 输出 → 明确失败;取消标记 → 明确失败。
#[cfg(feature = "test-stubs")]
#[test]
fn render_job_error_paths() {
use oakplugin::bridge::render::stub;
use oakplugin::ffi::{oakplugin_instance_cancel, oakplugin_instance_render_job};
common::with_host(|| {
stub::reset();
let mut h = scan_and_create(TEST_PLUGIN_ID);
if h.is_null() {
return;
}
// 非 F32 输出(U8)→ read_dst 拒绝。
stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_U8);
let dst = fake_texture(0xA1);
let r = unsafe {
oakplugin_instance_render_job(h, dst, 0.0, 0, 0, std::ptr::null(), CHandle::null(), std::ptr::null(), 0, std::ptr::null(), 0, CHandle::null())
};
assert_eq!(r, -90003, "非 F32 输出 → E_FAILED");
// 取消标记 → 驱动入口短路。
stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
assert_eq!(unsafe { oakplugin_instance_cancel(h) }, OK);
let r = unsafe {
oakplugin_instance_render_job(h, dst, 0.0, 0, 0, std::ptr::null(), CHandle::null(), std::ptr::null(), 0, std::ptr::null(), 0, CHandle::null())
};
assert_eq!(r, -90003, "已取消 → E_FAILED");
unsafe { oakplugin_instance_free(&mut h) };
});
}
+684
View File
@@ -0,0 +1,684 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! suite 层 round-trip 测试:宿主 suite 表 ⇄ 最小测试插件。
//!
//! 声明文档原计划"每个用例让测试插件在 describe/render 期间真实
//! 回调对应 suite""插件视角"的 HostSupport 兼容性背书)。最小
//! 测试插件(tests/fixtures/oak-test-plugin.ofx.bundle)是第 0 期
//! 交付物,尚未构建——插件无关的断言直接执行;依赖插件的用例经
//! [`common::skip`] 门,插件落地后补全(用例内 `// TODO(plugin)`)。
mod common;
use std::ffi::{c_char, c_double, c_int, c_uint, c_void, CStr, CString};
use std::sync::atomic::{AtomicUsize, Ordering};
use oakplugin::descriptor::EffectDescriptor;
use oakplugin::instance::Instance;
use oakplugin::param::{ParamInstance, ParamSetInstance};
use oakplugin::property::{PropertySet, Value};
use oakplugin::suites::fetch_suite;
use oakplugin::suites::image_effect::suite_v1 as image_effect_suite;
use oakplugin::suites::memory::suite_v1 as memory_suite;
use oakplugin::suites::message::suite_v1 as message_suite_v1;
use oakplugin::suites::multithread::suite_v1 as multithread_suite;
use oakplugin::suites::param::suite_v1 as param_suite;
use oakplugin::suites::progress::suite_v1 as progress_suite;
use oakplugin::suites::property::suite_v1 as property_suite;
use oakplugin::suites::timeline::suite_v1 as timeline_suite;
use oakplugin::suites::tag;
/// OFX 状态码的本地别名(SDK ofxCore.h)。
const OK: c_int = 0;
const UNKNOWN: c_int = 3;
const UNSUPPORTED: c_int = 5;
const BAD_HANDLE: c_int = 9;
const BAD_INDEX: c_int = 10;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
/// 未打标属性集句柄(宿主内部直用路径)。
fn props_handle(s: &PropertySet) -> *mut c_void {
s as *const PropertySet as *mut c_void
}
/// 假插件(仅喂给 Instancedescribe 之外的字段不被触碰)。
fn dummy_plugin(descriptor: EffectDescriptor) -> std::sync::Arc<oakplugin::host::Plugin> {
unsafe extern "C" fn dummy_entry(
_: *const c_char,
_: *const c_void,
_: *mut c_void,
_: *mut c_void,
) -> c_int {
OK
}
std::sync::Arc::new(oakplugin::host::Plugin {
identifier: "test.plugin".into(),
version: (1, 0),
bundle_path: std::path::PathBuf::new(),
contexts: vec![],
descriptor,
lib: std::ptr::null_mut(),
entry: dummy_entry,
ofx_plugin: std::ptr::null_mut(),
})
}
/// 用 param suite 的 paramDefine 造一个含 6 参数的实例。
fn make_instance() -> (std::sync::Arc<Instance>, *mut c_void) {
let mut desc = EffectDescriptor::new();
let s = param_suite();
let dhandle = tag::make(&desc.props as *const PropertySet, tag::DESCRIPTOR);
unsafe {
for (t, n) in [
("OfxParamTypeInteger", "gain"),
("OfxParamTypeDouble", "opacity"),
("OfxParamTypeDouble2D", "pos"),
("OfxParamTypeRGB", "color"),
("OfxParamTypeBoolean", "enabled"),
("OfxParamTypeString", "label"),
] {
let t = cs(t);
let n = cs(n);
let mut ph: *mut c_void = std::ptr::null_mut();
assert_eq!((s.param_define)(dhandle, t.as_ptr(), n.as_ptr(), &mut ph), OK);
}
}
let params = ParamSetInstance {
params: desc
.params
.iter()
.map(|d| Box::new(ParamInstance::from_def((**d).clone())))
.collect(),
};
let plugin = dummy_plugin(desc);
let inst = std::sync::Arc::new(Instance {
props: PropertySet::new(),
plugin,
context: "OfxImageEffectContextFilter".into(),
params,
clips: vec![],
node_identity: std::sync::atomic::AtomicUsize::new(0),
destroyed: std::sync::atomic::AtomicBool::new(false),
sequence_range: std::sync::Mutex::new(None),
progress_cb: std::sync::Mutex::new(None),
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(oakplugin::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
});
let h = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
(inst, h)
}
/// fetch_suite:第 1 期承诺的八张表按名按版本可取(v 不符返回
/// 空);不认识的 suite 返回空。覆盖 M11 §3.1 清单逐个断言。
#[test]
fn fetch_suite_registry() {
for (name, version) in [
("OfxPropertySuite", 1),
("OfxMemorySuite", 1),
("OfxImageEffectSuite", 1),
("OfxParameterSuite", 1),
("OfxMessageSuite", 1),
("OfxMessageSuite", 2),
("OfxProgressSuite", 1),
("OfxProgressSuite", 2),
("OfxTimeLineSuite", 1),
("OfxMultiThreadSuite", 1),
] {
assert!(
fetch_suite(name, version).is_some(),
"{name} v{version} 应可取"
);
}
assert!(fetch_suite("OfxPropertySuite", 2).is_none());
assert!(fetch_suite("OfxMessageSuite", 3).is_none());
assert!(fetch_suite("OfxBogusSuite", 1).is_none());
}
/// property suitedefine→set→get→getDimension→reset 全链路;
/// 越界读返回 kOfxStatErrBadIndex;未定义属性 → ErrUnknown。
///
/// 声明原含"写只读宿主能力属性 → kOfxStatErrReadOnly"SDK 无此
/// 状态码、HostSupport 也不在 suite 层拦截写(见 suites/property.rs
/// 模块文档的偏差说明)——本测试随之改为断言 reset 的明确行为。
#[test]
fn property_suite_roundtrip() {
let set = PropertySet::new();
set.define("width", vec![Value::Int(0)]);
let h = props_handle(&set);
let s = property_suite();
let name = cs("width");
// set → get round-trip。
unsafe {
assert_eq!((s.set_int)(h, name.as_ptr(), 0, 1920), OK);
}
let mut v = 0;
unsafe {
assert_eq!((s.get_int)(h, name.as_ptr(), 0, &mut v), OK);
}
assert_eq!(v, 1920);
// getDimension。
let mut dim = 0;
unsafe {
assert_eq!((s.get_dimension)(h, name.as_ptr(), &mut dim), OK);
}
assert_eq!(dim, 1);
// 越界读 → BadIndex;未定义 → Unknown。
unsafe {
assert_eq!((s.get_int)(h, name.as_ptr(), 5, &mut v), BAD_INDEX);
}
let nope = cs("nope");
unsafe {
assert_eq!((s.get_int)(h, nope.as_ptr(), 0, &mut v), UNKNOWN);
}
// propReset:第 1 期明确不支持(无默认值快照)→ ErrUnsupported。
unsafe {
assert_eq!((s.reset)(h, name.as_ptr()), UNSUPPORTED);
}
// 空 handle → BadHandle。
unsafe {
assert_eq!((s.get_int)(std::ptr::null_mut(), name.as_ptr(), 0, &mut v), BAD_HANDLE);
}
}
/// memory suitealloc/free 正常;free(NULL) no-op;未知指针
/// free → BadHandle;零尺寸分配可往返。
///
/// 声明原含"实例销毁后账本无残留"sweep 是 pub(crate) 钩子
/// destroyInstance 路径),属 host 生命周期测试(lifecycle_test
/// ——此处只验 suite 契约。
#[test]
fn memory_suite_ledger() {
let s = memory_suite();
let mut a: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.alloc)(std::ptr::null_mut(), 256, &mut a), OK);
}
assert!(!a.is_null());
// 内存可用且对齐(f32 写读)。
unsafe {
let f = a as *mut f32;
*f = 1.5f32;
assert_eq!(*f, 1.5f32);
assert_eq!(a as usize % 16, 0);
}
// 释放;free(NULL) no-op;重复/未知 → BadHandle。
unsafe {
assert_eq!((s.free)(a), OK);
assert_eq!((s.free)(std::ptr::null_mut()), OK);
assert_eq!((s.free)(a), BAD_HANDLE);
}
// 零尺寸。
let mut z: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.alloc)(std::ptr::null_mut(), 0, &mut z), OK);
assert_eq!((s.free)(z), OK);
}
}
/// image effect suitedescribe 期 clipDefine/clipGetPropertySet 与
/// 属性读写;实例期 clipGetHandle。
///
/// clipGetImage/clipReleaseImage 配对依赖
/// [`oakplugin::clip::ClipInstance::fetch_image`]bridge::render 帧
/// 访问 C ABI 未冻结)——随最小测试插件落地补全(`// TODO(plugin)`)。
#[test]
fn image_effect_clip_image_pairing() {
let mut desc = EffectDescriptor::new();
let s = image_effect_suite();
let h = tag::make(&desc.props as *const PropertySet, tag::DESCRIPTOR);
// clipDefine + 属性读写(props 在偏移 0)。
let mut clip: *mut c_void = std::ptr::null_mut();
let name = cs("Source");
unsafe {
assert_eq!((s.clip_define)(h, name.as_ptr(), &mut clip), OK);
}
assert_eq!(tag::kind(clip), tag::CLIP);
let ps = property_suite();
let label_prop = cs("OfxPropLabel");
let optional_prop = cs("OfxImageClipPropOptional");
let label = cs("SourceLabel");
unsafe {
assert_eq!((ps.set_string)(clip, label_prop.as_ptr(), 0, label.as_ptr()), OK);
assert_eq!((ps.set_int)(clip, optional_prop.as_ptr(), 0, 1), OK);
}
let mut out: *mut c_char = std::ptr::null_mut();
unsafe {
assert_eq!((ps.get_string)(clip, label_prop.as_ptr(), 0, &mut out), OK);
assert_eq!(CStr::from_ptr(out).to_bytes(), b"SourceLabel");
}
// getPropertySet/getParamSet:返回 effect 本体。
let mut props: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.get_property_set)(h, &mut props), OK);
assert_eq!(props, h);
assert_eq!((s.get_param_set)(h, &mut props), OK);
assert_eq!(props, h);
}
// 实例期 clipGetHandle(手工构造实例 + clip 实例)。
let clip_desc = oakplugin::descriptor::ClipDescriptor {
props: PropertySet::new(),
name: "Source".into(),
};
let inst = std::sync::Arc::new(Instance {
props: PropertySet::new(),
plugin: dummy_plugin(EffectDescriptor::new()),
context: "OfxImageEffectContextFilter".into(),
params: ParamSetInstance { params: vec![] },
clips: vec![Box::new(oakplugin::clip::ClipInstance::from_descriptor(
&clip_desc,
))],
node_identity: std::sync::atomic::AtomicUsize::new(0),
destroyed: std::sync::atomic::AtomicBool::new(false),
sequence_range: std::sync::Mutex::new(None),
progress_cb: std::sync::Mutex::new(None),
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(oakplugin::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
});
let ih = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
let mut clip_h: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.clip_get_handle)(ih, name.as_ptr(), &mut clip_h, std::ptr::null_mut()), OK);
}
assert_eq!(tag::kind(clip_h), tag::CLIP);
// 未找到 → BadHandleHS:2067-2070)。
let nope = cs("Nope");
unsafe {
assert_eq!((s.clip_get_handle)(ih, nope.as_ptr(), &mut clip_h, std::ptr::null_mut()), BAD_HANDLE);
}
// clipGetImage/clipReleaseImage 配对:依赖 clip fetch_image
// bridge::render 未冻结)——插件落地后补全。
if common::test_plugin_dir().is_none() {
common::skip("clipGetImage 配对随最小测试插件落地(M11 §2.4)");
return;
}
// TODO(plugin):插件驱动 clipGetImage → clipReleaseImage 配对 +
// 不配对时的销毁记账断言。
}
/// param suitedescribe 期 define→getHandle→getValue(默认值);
/// 实例期 int/double/bool/choice/string/RGBA/2D/3D 的
/// setValue/getValue round-tripAtTime == 当前值。
///
/// 声明原含"paramSetValue 触发 instanceChanged":通知走
/// [`oakplugin::param::notify_instance_changed`]bridge 期实现)——
/// 随插件+桥落地补全(`// TODO(bridge)`)。
#[test]
fn param_suite_roundtrip_and_change_action() {
// describe 期。
let mut desc = EffectDescriptor::new();
let s = param_suite();
let dhandle = tag::make(&desc.props as *const PropertySet, tag::DESCRIPTOR);
let t = cs("OfxParamTypeDouble");
let n = cs("opacity");
let mut ph: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.param_define)(dhandle, t.as_ptr(), n.as_ptr(), &mut ph), OK);
}
assert_eq!(tag::kind(ph), tag::PARAM_DEF);
let mut v = 99.0;
unsafe {
assert_eq!((s.param_get_value)(ph, &mut v), OK);
}
assert_eq!(v, 0.0, "describe 期 getValue 应为默认值");
// describe 期 set → BadHandleHS verifyMagic 语义)。
unsafe {
assert_eq!((s.param_set_value)(ph, 42.0), BAD_HANDLE);
}
// paramGetHandle 按名取;未找到 → Unknown。
let mut ph2: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.param_get_handle)(dhandle, n.as_ptr(), &mut ph2, std::ptr::null_mut()), OK);
assert_eq!(ph, ph2);
}
let nope = cs("nope");
unsafe {
assert_eq!((s.param_get_handle)(dhandle, nope.as_ptr(), &mut ph2, std::ptr::null_mut()), UNKNOWN);
}
// 实例期 round-trip。
let (_inst, ih) = make_instance();
let mut gain: *mut c_void = std::ptr::null_mut();
let mut pos: *mut c_void = std::ptr::null_mut();
let mut color: *mut c_void = std::ptr::null_mut();
let mut enabled: *mut c_void = std::ptr::null_mut();
let mut label: *mut c_void = std::ptr::null_mut();
unsafe {
for (n, out) in [
("gain", &mut gain),
("pos", &mut pos),
("color", &mut color),
("enabled", &mut enabled),
("label", &mut label),
] {
let n = cs(n);
assert_eq!((s.param_get_handle)(ih, n.as_ptr(), out, std::ptr::null_mut()), OK);
assert_eq!(tag::kind(*out), tag::PARAM_INSTANCE);
}
// Integer。
assert_eq!((s.param_set_value)(gain, 7), OK);
let mut iv = 0;
assert_eq!((s.param_get_value)(gain, &mut iv), OK);
assert_eq!(iv, 7);
// Double2D。
assert_eq!((s.param_set_value)(pos, 1.5, -2.5), OK);
let (mut x, mut y) = (0.0, 0.0);
assert_eq!((s.param_get_value)(pos, &mut x, &mut y), OK);
assert_eq!((x, y), (1.5, -2.5));
// RGB。
assert_eq!((s.param_set_value)(color, 0.1, 0.2, 0.3), OK);
let (mut r, mut g, mut b) = (0.0, 0.0, 0.0);
assert_eq!((s.param_get_value)(color, &mut r, &mut g, &mut b), OK);
assert_eq!((r, g, b), (0.1, 0.2, 0.3));
// Boolean。
assert_eq!((s.param_set_value)(enabled, 1), OK);
let mut e = 0;
assert_eq!((s.param_get_value)(enabled, &mut e), OK);
assert_eq!(e, 1);
// Stringget 返回内驻指针)。
let hello = cs("hello");
assert_eq!((s.param_set_value)(label, hello.as_ptr()), OK);
let mut p: *mut c_char = std::ptr::null_mut();
assert_eq!((s.param_get_value)(label, &mut p), OK);
assert_eq!(CStr::from_ptr(p).to_bytes(), b"hello");
// AtTime == 当前值(无动画)。
let mut v2 = 0.0;
assert_eq!((s.param_get_value_at_time)(gain, 12.0, &mut iv), OK);
assert_eq!(iv, 7);
assert_eq!((s.param_set_value_at_time)(gain, 12.0, 9), OK);
let mut iv2 = 0;
assert_eq!((s.param_get_value)(gain, &mut iv2), OK);
assert_eq!(iv2, 9);
}
// TODO(bridge)paramSetValue → instanceChanged 断言随
// notify_instance_changedbridge 期)落地。
}
/// paramGetValueAtTime/paramSetValueAtTime 与关键帧族
/// GetNumKeys/GetKeyTime/GetKeyIndex/DeleteKey/DeleteAllKeys):
/// 第 1 期无动画 → keys 恒 0、key 查询 BadIndex、删除 no-op OK。
#[test]
fn param_keyframe_family() {
let (_inst, ih) = make_instance();
let s = param_suite();
let mut opacity: *mut c_void = std::ptr::null_mut();
let name = cs("opacity");
unsafe {
assert_eq!((s.param_get_handle)(ih, name.as_ptr(), &mut opacity, std::ptr::null_mut()), OK);
}
let mut nkeys = -1;
unsafe {
assert_eq!((s.param_get_num_keys)(opacity, &mut nkeys), OK);
}
assert_eq!(nkeys, 0, "无动画支持:恒 0 个关键帧");
let mut kt = 0.0;
unsafe {
assert_eq!((s.param_get_key_time)(opacity, 0, &mut kt), BAD_INDEX);
}
let mut ki = 0;
unsafe {
assert_eq!((s.param_get_key_index)(opacity, 1.0, 0, &mut ki), BAD_INDEX);
// 删除与拷贝:无关键帧 → no-op OK。
assert_eq!((s.param_delete_key)(opacity, 1.0), OK);
assert_eq!((s.param_delete_all_keys)(opacity), OK);
assert_eq!((s.param_copy)(opacity, opacity, 1.0, 1.0, std::ptr::null()), OK);
// editBegin/End 括号。
assert_eq!((s.param_edit_begin)(opacity), OK);
assert_eq!((s.param_edit_end)(opacity), OK);
}
}
/// message suitev1 变长参数经 C shim 格式化后落到注册的捕获器
/// (%d/%s/%f 三种格式逐字一致);headless 默认(question 答"否")。
///
/// v2 的 va_list 入口无法从 Rust 构造调用(platform ABI),格式化
/// 路径与 v1 共用同一 C 函数(forward)——由测试插件从 C 侧覆盖
/// `// TODO(plugin)`)。
#[test]
fn message_suite_v1_v2() {
use oakplugin::suites::message::set_handler;
unsafe extern "C" fn capture(
type_: *const c_char,
message: *const c_char,
userdata: *mut c_void,
) -> c_int {
let v = unsafe { &mut *(userdata as *mut Vec<(String, String)>) };
v.push((
unsafe { CStr::from_ptr(type_) }.to_string_lossy().into_owned(),
unsafe { CStr::from_ptr(message) }.to_string_lossy().into_owned(),
));
1
}
let mut captured: Vec<(String, String)> = Vec::new();
set_handler(Some(capture), &mut captured as *mut _ as *mut c_void);
let s = message_suite_v1();
let t = cs("OfxMessageError");
let id = cs("test-id");
let fmt = cs("v=%d s=%s f=%f");
let str_arg = cs("oak");
unsafe {
// 答复 YES → kOfxStatReplyYes12)。
assert_eq!(
(s.message)(std::ptr::null_mut(), t.as_ptr(), id.as_ptr(), fmt.as_ptr(), 42, str_arg.as_ptr(), 1.5f64),
12
);
}
assert_eq!(captured.len(), 1);
assert_eq!(captured[0].0, "OfxMessageError");
assert!(captured[0].1.starts_with("v=42 s=oak f="), "got {:?}", captured[0].1);
// NULL format → FailedC++ olivehost.cpp:267)。
unsafe {
assert_eq!((s.message)(std::ptr::null_mut(), t.as_ptr(), id.as_ptr(), std::ptr::null()), 1);
}
// 注销出口 → headless 默认:普通消息 OK。
set_handler(None, std::ptr::null_mut());
let msg = cs("ask");
unsafe {
assert_eq!(
(s.message)(std::ptr::null_mut(), t.as_ptr(), id.as_ptr(), msg.as_ptr()),
OK
);
}
// question 类型 + 无出口 → REPLY_NO13)。
let q = cs("OfxMessageQuestion");
unsafe {
assert_eq!(
(s.message)(std::ptr::null_mut(), q.as_ptr(), id.as_ptr(), fmt.as_ptr(), 1),
13
);
}
}
/// progress suitestart/update/end 序列转发到 ProgressReporter
/// 回调返回 false 时 update 向插件返回取消状态。
#[test]
fn progress_suite_forwarding() {
use oakplugin::progress::ProgressReporter;
use oakplugin::suites::progress::set_current;
unsafe extern "C" fn capture_progress(p: f64, userdata: *mut c_void) -> c_int {
let v = unsafe { &mut *(userdata as *mut Vec<f64>) };
v.push(p);
0
}
unsafe extern "C" fn cancel_progress(_p: f64, _userdata: *mut c_void) -> c_int {
1
}
let s = progress_suite();
let mut seen: Vec<f64> = Vec::new();
set_current(Some(unsafe { ProgressReporter::new(capture_progress, &mut seen as *mut _ as *mut c_void) }));
unsafe {
assert_eq!((s.start)(std::ptr::null_mut(), std::ptr::null()), OK);
assert_eq!((s.update)(std::ptr::null_mut(), 0.25), OK);
assert_eq!((s.update)(std::ptr::null_mut(), 0.75), OK);
assert_eq!((s.end)(std::ptr::null_mut()), OK);
}
assert_eq!(seen, vec![0.25, 0.75]);
// 取消回调:update → REPLY_NO。
set_current(Some(unsafe { ProgressReporter::new(cancel_progress, std::ptr::null_mut()) }));
unsafe {
assert_eq!((s.update)(std::ptr::null_mut(), 0.5), 13);
}
set_current(None);
}
/// timeline suitegetTime/getTimeBounds 返回渲染上下文注入的值;
/// 无上下文 → 0 / (0,0) 的 headless 默认。
#[test]
fn timeline_suite_values() {
use oakplugin::instance::{OfxRangeD, RenderScale};
use oakplugin::suites::{RenderCtx, set_render_ctx};
let s = timeline_suite();
let handle = 0x10usize as *mut c_void;
let mut t = 0.0;
let mut min = 0.0;
let mut max = 0.0;
// 无上下文。
unsafe {
assert_eq!((s.get_time)(handle, &mut t), OK);
assert_eq!(t, 0.0);
assert_eq!((s.get_time_bounds)(handle, &mut min, &mut max), OK);
assert_eq!((min, max), (0.0, 0.0));
}
// 注入上下文。
set_render_ctx(Some(RenderCtx {
time: 42.5,
scale: RenderScale { x: 1.0, y: 1.0 },
range: OfxRangeD { min: 10.0, max: 200.0 },
}));
unsafe {
assert_eq!((s.get_time)(handle, &mut t), OK);
assert_eq!(t, 42.5);
assert_eq!((s.get_time_bounds)(handle, &mut min, &mut max), OK);
assert_eq!((min, max), (10.0, 200.0));
assert_eq!((s.goto_time)(handle, 99.0), OK);
}
set_render_ctx(None);
}
/// multithread suite:插件(此处以测试自身的工作线程模拟)起 8 线程
/// 各回调 property suite 一千次;无数据竞争、index/isSpawnedThread
/// 语义正确、join 完整。
#[test]
fn multithread_suite_spawned_callbacks() {
let s = multithread_suite();
let ps = property_suite();
// 共享属性集:每线程一个计数器属性,各写 1000 次。
// (测试自身即"插件":工作线程经 multiThread 起,回调 property
// suite——与插件场景同构。)
let set = std::sync::Arc::new(PropertySet::new());
for i in 0..8 {
let name = Box::leak(format!("ctr{i}").into_boxed_str());
set.set_one(name, Value::Int(0));
}
unsafe extern "C" fn counter_worker(index: c_uint, _count: c_uint, arg: *mut c_void) {
eprintln!("DBG worker {index} arg={:x}", arg as usize);
let set = unsafe { &*(arg as *const PropertySet) };
let name = Box::leak(format!("ctr{index}").into_boxed_str());
let mut v = 0;
unsafe {
let _ = (property_suite().get_int)(
set as *const _ as *mut c_void,
cs(name).as_ptr(),
0,
&mut v,
);
}
for _ in 0..1000 {
unsafe {
let _ = (property_suite().set_int)(
set as *const _ as *mut c_void,
cs(name).as_ptr(),
0,
v + 1,
);
let _ = (property_suite().get_int)(
set as *const _ as *mut c_void,
cs(name).as_ptr(),
0,
&mut v,
);
}
}
}
// 宿主线程身份。
let mut idx = 0;
let mut spawned = 1;
unsafe {
assert_eq!((s.index)(&mut idx), OK);
assert_eq!(idx, -1);
assert_eq!((s.is_spawned)(&mut spawned), OK);
assert_eq!(spawned, 0);
}
// 8 线程各 1000 次增量:最终值恰为 1000(无丢写 = 无竞争)。
// 注意传 `&*set`Arc 负载)而非 `&set`Arc 结构体)。
let raw: *const PropertySet = &*set;
unsafe {
assert_eq!((s.multi_thread)(counter_worker, 8, raw as *mut c_void), OK);
}
for i in 0..8 {
let name = Box::leak(format!("ctr{i}").into_boxed_str());
let mut v = 0;
// 注意 `&*set`Arc 负载)而非 `&set`Arc 结构体)。
unsafe {
assert_eq!((ps.get_int)(&*set as *const _ as *mut c_void, name.as_ptr() as *const c_char, 0, &mut v), OK);
}
assert_eq!(v, 1000, "线程 {i} 的计数应无丢失");
}
// 0 线程:no-op。
unsafe {
assert_eq!((s.multi_thread)(counter_worker, 0, std::ptr::null_mut()), OK);
}
}