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:
Generated
+32
@@ -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"
|
||||
@@ -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"
|
||||
@@ -0,0 +1,265 @@
|
||||
# oakplugin Rust crate(M11 第 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 PropertySet:OFX 属性集的存储与类型化读写
|
||||
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 OfxImageEffectOpenGLRenderSuiteV1(M11 §4 新增)
|
||||
host.rs Host 单例:bundle 扫描、插件缓存、action 分发
|
||||
descriptor.rs EffectDescriptor/ClipDescriptor(describe 的产物)
|
||||
instance.rs Instance:action 调用面(render/协商/RoD/RoI/isIdentity/
|
||||
render_gl/GetOutputColourspace)
|
||||
render_driver.rs pluginrenderer.cpp 渲染流程收编(M11 §4 新增)
|
||||
clip.rs ClipInstance:clip↔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 生效并并入 multi,editEnd 整体 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.h(vendored)与 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 → render(in 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。
|
||||
|
||||
### ofxColour(OFX 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_driver(pluginrenderer.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 渲染取消)。 |
|
||||
| nm:liboaknode/liboakrender→liboakplugin C++ 符号为 0;HostSupport 移出构建 | 由外层 CMake 接线验证(本 crate 侧:ffi.rs 仅导出 C ABI)。 |
|
||||
| cargo test(host 内部单测)+ ctest(C ABI 层)双绿 | **达成**(cargo test 两种模式全绿;ctest 属 standalone 树外层)。 |
|
||||
| M11 §4:GL 路径 + ofxColour + pluginrenderer 收编 | **达成**(本机验证):OpenGLRender suite v1 + render_gl(GL 上下文/格式协商规则按 ofxGPURender.h);ofxColour 属性族 + GetOutputColourspace(ACEScg 工作空间);render_driver 收编 + render_job C ABI(见上)。GL golden(EXR 容差)需 0 期基建 + 本机 GPU 人工确认(`OAK_GPU_TESTS` 门)。 |
|
||||
|
||||
## 实现纪律(实现方必读)
|
||||
|
||||
1. 所有 `extern "C"` 函数体必须包 `crate::handle::guard(..)`(
|
||||
catch_unwind + 错误码映射),禁止 panic 越过 FFI。
|
||||
2. 句柄全部经 `handle.rs` 的 `RefBox<T>`;`ctx` 永不裸指针外露含义。
|
||||
3. 共享状态(插件缓存、instance 注册表、线程表)一律 `Mutex`;
|
||||
插件可能在其自起线程回调任意 suite(MultiThread suite 存活期)。
|
||||
4. OFX 语义以 HostSupport 的行为为参照系;每个协商/时序实现点
|
||||
必须注释对应 HostSupport 文件行号。
|
||||
5. 错误码、句柄布局、字符串两段式与 `include/plugin/*.h` 逐字一致。
|
||||
6. 依赖政策:**优先使用成熟第三方 crate**(serde/quick-xml/thiserror
|
||||
等),不重复造轮子。选型要求:crates.io 有维护、许可
|
||||
MIT/Apache-2.0/BSD(GPL 兼容);新增依赖在 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.c,build.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)时 skip(README 已记录),兼容 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 约定。
|
||||
@@ -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 shim(cbits/)。动机见 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(), "测试插件编译失败");
|
||||
}
|
||||
@@ -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 上下文:
|
||||
* - 参数:gain(Double,默认 0)
|
||||
* - clip:Source(输入,RGBA)、Output
|
||||
* - describe/describeInContext 建参数与 clip;
|
||||
* - getClipPreferences:全链路 F32+RGBA、帧率 24;
|
||||
* - render:把输出图像填成常量 0.5(RGBA float),alpha=1;
|
||||
* - getRoD:project size;getRoI:原样返回 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);
|
||||
}
|
||||
|
||||
/* ---------- setHost(mandatory 第一个调用) ---------- */
|
||||
|
||||
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 });
|
||||
|
||||
/* ofxColour(M11 §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);
|
||||
}
|
||||
}
|
||||
|
||||
/* 参数族:gain(Double,带 display min/max)、mode(Choice,
|
||||
* 两选项)、debug(Double,secret)、label(String)。 */
|
||||
OfxPropertySetHandle paramProps = NULL;
|
||||
OfxStatus st = g_paramSuite->paramDefine((OfxParamSetHandle)handle, kOfxParamTypeDouble, "gain", ¶mProps);
|
||||
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", ¶mProps);
|
||||
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", ¶mProps);
|
||||
if (st != kOfxStatOK)
|
||||
return st;
|
||||
g_propSuite->propSetInt(paramProps, kOfxParamPropSecret, 0, 1);
|
||||
|
||||
st = g_paramSuite->paramDefine((OfxParamSetHandle)handle, kOfxParamTypeString, "label", ¶mProps);
|
||||
if (st != kOfxStatOK)
|
||||
return st;
|
||||
propSetString(paramProps, kOfxPropLabel, 0, "Label");
|
||||
|
||||
/* clip:Source + 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.5(RGBA 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;
|
||||
}
|
||||
|
||||
/* ---------- ofxColour(M11 §4):GetOutputColourspace ---------- */
|
||||
|
||||
static OfxStatus actionGetOutputColourspace(OfxPropertySetHandle inArgs,
|
||||
OfxPropertySetHandle outArgs)
|
||||
{
|
||||
/* 优先采纳宿主偏好的第一个色彩空间;否则交叉引用 Source clip
|
||||
* (ofxColour.h:cross-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);
|
||||
|
||||
/* 输入 clip:clipLoadTexture(请求 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;
|
||||
|
||||
/* 输出 clip:clipLoadTexture(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 suite;attach/
|
||||
* 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;
|
||||
}
|
||||
@@ -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.rs(cc 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 → kOfxStatFailed(olivehost.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);
|
||||
}
|
||||
@@ -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):
|
||||
* get:Integer/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);
|
||||
}
|
||||
@@ -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/>.
|
||||
|
||||
//! bridge:oak 其余模块的 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_DEFAULT(macOS: -2;Linux: 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))
|
||||
}
|
||||
}
|
||||
@@ -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_combo,node.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_value(include/node/node.h:93,字段逐字一致)。
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Value {
|
||||
/// 类型([`node_value_type`])。
|
||||
pub r#type: i32,
|
||||
/// INT/COMBO 值、BOOL 0/1、RATIONAL 分子。
|
||||
pub num: i64,
|
||||
/// RATIONAL 分母。
|
||||
pub den: i64,
|
||||
/// FLOAT f[0];VEC2/3/4 f[0..n-1];COLOR r,g,b,a。
|
||||
pub f: [f64; 4],
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// 类型化构造:整数 / choice 索引。
|
||||
pub const fn int(v: i64) -> Self {
|
||||
Self {
|
||||
r#type: node_value_type::INT,
|
||||
num: v,
|
||||
den: 0,
|
||||
f: [0.0; 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型化构造:浮点。
|
||||
pub const fn float(v: f64) -> Self {
|
||||
Self {
|
||||
r#type: node_value_type::FLOAT,
|
||||
num: 0,
|
||||
den: 0,
|
||||
f: [v, 0.0, 0.0, 0.0],
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型化构造:布尔。
|
||||
pub const fn bool_(v: bool) -> Self {
|
||||
Self {
|
||||
r#type: node_value_type::BOOL,
|
||||
num: v as i64,
|
||||
den: 0,
|
||||
f: [0.0; 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型化构造:choice(COMBO)。
|
||||
pub const fn combo(v: i64) -> Self {
|
||||
Self {
|
||||
r#type: node_value_type::COMBO,
|
||||
num: v,
|
||||
den: 0,
|
||||
f: [0.0; 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型化构造:颜色。
|
||||
pub const fn color(r: f64, g: f64, b: f64, a: f64) -> Self {
|
||||
Self {
|
||||
r#type: node_value_type::COLOR,
|
||||
num: 0,
|
||||
den: 0,
|
||||
f: [r, g, b, a],
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型化构造:vec2/3/4(长度按 f 数组尾部 0 判定)。
|
||||
pub const fn vec(v: &[f64]) -> Self {
|
||||
let t = match v.len() {
|
||||
2 => node_value_type::VEC2,
|
||||
3 => node_value_type::VEC3,
|
||||
_ => node_value_type::VEC4,
|
||||
};
|
||||
let mut f = [0.0; 4];
|
||||
let mut i = 0;
|
||||
while i < v.len() && i < 4 {
|
||||
f[i] = v[i];
|
||||
i += 1;
|
||||
}
|
||||
Self {
|
||||
r#type: t,
|
||||
num: 0,
|
||||
den: 0,
|
||||
f,
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型化构造:字符串族(POD 不携带数据;值经
|
||||
/// [`set_input_string_undoable`] 传递)。
|
||||
pub const fn string() -> Self {
|
||||
Self {
|
||||
r#type: node_value_type::STRING,
|
||||
num: 0,
|
||||
den: 0,
|
||||
f: [0.0; 4],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 桥调用面 ------------------------------------------------------------
|
||||
|
||||
/// 按身份取节点句柄(M9 身份注册表;`oaknode_node_from_identity`)。
|
||||
/// 身份未登记 / 符号缺失 → 空句柄。
|
||||
pub(crate) unsafe fn node_from_identity(id: usize) -> NodeHandle {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::node_from_identity_impl(id) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
/// 预乘 alpha(0/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 {
|
||||
/// 原生 id(texture_id 的返回值)。
|
||||
id: i32,
|
||||
/// CPU 镜像帧(数据 + 参数)。
|
||||
frame: StubFrame,
|
||||
}
|
||||
|
||||
fn bytes_per_pixel(format: i32) -> usize {
|
||||
match format {
|
||||
PIXEL_FORMAT_F32 => 4,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// 按参数分配紧凑像素缓冲(宽×高×4 通道×每分量字节)。
|
||||
fn tight_len(params: &VideoParams) -> usize {
|
||||
params.width as usize * params.height as usize * 4 * bytes_per_pixel(params.format)
|
||||
}
|
||||
|
||||
struct StubState {
|
||||
dst: StubFrame,
|
||||
src: StubFrame,
|
||||
/// 标记为占位(dummy)的纹理 ctx(texture_is_dummy 用)。
|
||||
dummy: std::collections::HashSet<usize>,
|
||||
/// GL 渲染器是否可用(renderer_is_open_gl 的返回值)。
|
||||
gl_available: bool,
|
||||
/// GL 纹理注册表(ctx = GL_TEX_BASE + id)。
|
||||
gl_textures: Vec<StubGlTexture>,
|
||||
/// 下一个 GL 纹理 id(从 1 起)。
|
||||
next_gl_id: i32,
|
||||
}
|
||||
|
||||
static STATE: std::sync::LazyLock<Mutex<StubState>> = std::sync::LazyLock::new(|| {
|
||||
Mutex::new(StubState {
|
||||
dst: StubFrame::new(0, 0, 0),
|
||||
src: StubFrame::new(0, 0, 0),
|
||||
dummy: std::collections::HashSet::new(),
|
||||
gl_available: false,
|
||||
gl_textures: Vec::new(),
|
||||
next_gl_id: 1,
|
||||
})
|
||||
});
|
||||
|
||||
fn lock() -> std::sync::MutexGuard<'static, StubState> {
|
||||
STATE.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// 重置全部桩状态(测试隔离)。
|
||||
pub fn reset() {
|
||||
let mut s = lock();
|
||||
s.dst = StubFrame::new(0, 0, 0);
|
||||
s.src = StubFrame::new(0, 0, 0);
|
||||
s.dummy.clear();
|
||||
s.gl_available = false;
|
||||
s.gl_textures.clear();
|
||||
s.next_gl_id = 1;
|
||||
}
|
||||
|
||||
/// 把某纹理 ctx 标记为占位(dummy);`dummy=false` 取消标记。
|
||||
/// clip 桥把 dummy 输入视作空输入(NotFound)。
|
||||
pub fn set_dummy(ctx: usize, dummy: bool) {
|
||||
let mut s = lock();
|
||||
if dummy {
|
||||
s.dummy.insert(ctx);
|
||||
} else {
|
||||
s.dummy.remove(&ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// 配置输出帧(宽/高/格式;F32 = 全链路主路径)。
|
||||
pub fn setup_dst(width: i32, height: i32, format: i32) {
|
||||
lock().dst = StubFrame::new(width, height, format);
|
||||
}
|
||||
|
||||
/// 配置输入帧并填充像素。
|
||||
pub fn setup_src(width: i32, height: i32, format: i32, pixels: Vec<u8>) {
|
||||
let mut s = lock();
|
||||
s.src = StubFrame::new(width, height, format);
|
||||
s.src.data = pixels;
|
||||
}
|
||||
|
||||
/// 输出帧像素(断言用)。
|
||||
pub fn dst_pixels() -> Vec<u8> {
|
||||
lock().dst.data.clone()
|
||||
}
|
||||
|
||||
/// 输入帧像素(断言用)。
|
||||
pub fn src_pixels() -> Vec<u8> {
|
||||
lock().src.data.clone()
|
||||
}
|
||||
|
||||
/// 输出帧参数。
|
||||
pub fn dst_params() -> VideoParams {
|
||||
lock().dst.params
|
||||
}
|
||||
|
||||
/// GL 渲染器可用标记(renderer_is_open_gl 的桩返回值)。
|
||||
pub fn set_gl_available(available: bool) {
|
||||
lock().gl_available = available;
|
||||
}
|
||||
|
||||
/// 构造桩 GL 渲染器句柄(ctx = 0xD1)。
|
||||
pub fn make_gl_renderer() -> RendererHandle {
|
||||
magic_handle(GL_RENDERER)
|
||||
}
|
||||
|
||||
/// 已注册的 GL 纹理 id 列表(断言用)。
|
||||
pub fn gl_texture_ids() -> Vec<i32> {
|
||||
lock().gl_textures.iter().map(|t| t.id).collect()
|
||||
}
|
||||
|
||||
/// 某 GL 纹理 id 的像素(断言用;未知 id 返回空)。
|
||||
pub fn gl_texture_data(id: i32) -> Vec<u8> {
|
||||
lock()
|
||||
.gl_textures
|
||||
.iter()
|
||||
.find(|t| t.id == id)
|
||||
.map(|t| t.frame.data.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 某 GL 纹理 id 的参数(断言用)。
|
||||
pub fn gl_texture_params(id: i32) -> Option<VideoParams> {
|
||||
lock()
|
||||
.gl_textures
|
||||
.iter()
|
||||
.find(|t| t.id == id)
|
||||
.map(|t| t.frame.params)
|
||||
}
|
||||
|
||||
/// 直接构造一个 GL 纹理(GL 测试的目标纹理/输入纹理模拟;
|
||||
/// oakrender 侧创建纹理的 C ABI 等价物)。返回句柄(ctx =
|
||||
/// GL_TEX_BASE + id)。
|
||||
pub fn make_gl_texture(width: i32, height: i32, format: i32) -> TextureHandle {
|
||||
let mut s = lock();
|
||||
let id = s.next_gl_id;
|
||||
s.next_gl_id += 1;
|
||||
s.gl_textures.push(StubGlTexture {
|
||||
id,
|
||||
frame: StubFrame::new(width, height, format),
|
||||
});
|
||||
magic_handle(GL_TEX_BASE + id as usize)
|
||||
}
|
||||
|
||||
fn frame_of(state: &mut StubState, ctx: usize) -> &mut StubFrame {
|
||||
if ctx == SRC_FRAME {
|
||||
&mut state.src
|
||||
} else if let Some(id) = gl_id_of_ctx(ctx) {
|
||||
match state.gl_textures.iter_mut().find(|t| t.id == id) {
|
||||
Some(t) => &mut t.frame,
|
||||
None => &mut state.dst,
|
||||
}
|
||||
} else {
|
||||
&mut state.dst
|
||||
}
|
||||
}
|
||||
|
||||
fn magic_handle(ctx: usize) -> CHandle {
|
||||
CHandle {
|
||||
ctx: ctx as *mut c_void,
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
const DST_TEX: usize = 0xA1;
|
||||
const SRC_TEX: usize = 0xA2;
|
||||
const DST_FRAME: usize = 0xB1;
|
||||
const SRC_FRAME: usize = 0xB2;
|
||||
/// 桩 GL 渲染器 ctx。
|
||||
const GL_RENDERER: usize = 0xD1;
|
||||
/// 桩 GL 纹理 ctx 基址(ctx = GL_TEX_BASE + id)。
|
||||
const GL_TEX_BASE: usize = 0xC0;
|
||||
|
||||
/// GL 纹理 ctx → id。
|
||||
fn gl_id_of_ctx(ctx: usize) -> Option<i32> {
|
||||
if ctx > GL_TEX_BASE {
|
||||
Some((ctx - GL_TEX_BASE) as i32)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 行拷贝:`src`(行跨度 src_linesize 字节)→ `dst`(行跨度
|
||||
/// dst_linesize 字节),共 `rows` 行、每行 `row_bytes` 字节。
|
||||
fn copy_rows(
|
||||
src: &[u8],
|
||||
src_linesize: usize,
|
||||
dst: &mut [u8],
|
||||
dst_linesize: usize,
|
||||
row_bytes: usize,
|
||||
rows: usize,
|
||||
) {
|
||||
for y in 0..rows {
|
||||
let s = y * src_linesize;
|
||||
let d = y * dst_linesize;
|
||||
if row_bytes == src_linesize && src_linesize == dst_linesize {
|
||||
dst[d..d + row_bytes].copy_from_slice(&src[s..s + row_bytes]);
|
||||
} else {
|
||||
dst[d..d + row_bytes].copy_from_slice(&src[s..s + row_bytes.min(src.len().saturating_sub(s))]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_get_frame(texture: TextureHandle, out: *mut FrameHandle) -> i32 {
|
||||
if out.is_null() {
|
||||
return -1;
|
||||
}
|
||||
// GL 纹理:帧句柄即纹理自身 ctx(frame_of 按 ctx 反查镜像帧)。
|
||||
let frame_ctx = if texture.ctx as usize == SRC_TEX {
|
||||
SRC_FRAME
|
||||
} else if gl_id_of_ctx(texture.ctx as usize).is_some() {
|
||||
texture.ctx as usize
|
||||
} else {
|
||||
DST_FRAME
|
||||
};
|
||||
unsafe { *out = magic_handle(frame_ctx) };
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_is_dummy(texture: TextureHandle) -> i32 {
|
||||
lock().dummy.contains(&(texture.ctx as usize)) as i32
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_width(frame: FrameHandle) -> i32 {
|
||||
let mut s = lock();
|
||||
frame_of(&mut s, frame.ctx as usize).params.width
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_height(frame: FrameHandle) -> i32 {
|
||||
let mut s = lock();
|
||||
frame_of(&mut s, frame.ctx as usize).params.height
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_data(frame: FrameHandle) -> *mut c_void {
|
||||
let mut s = lock();
|
||||
let f = frame_of(&mut s, frame.ctx as usize);
|
||||
f.data.as_mut_ptr() as *mut c_void
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_get_params(frame: FrameHandle, out: *mut VideoParams) -> i32 {
|
||||
if out.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let mut s = lock();
|
||||
unsafe { *out = frame_of(&mut s, frame.ctx as usize).params };
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_allocate(frame: FrameHandle) -> i32 {
|
||||
let mut s = lock();
|
||||
let f = frame_of(&mut s, frame.ctx as usize);
|
||||
let len = f.params.width as usize
|
||||
* f.params.height as usize
|
||||
* 4
|
||||
* bytes_per_pixel(f.params.format);
|
||||
f.data.resize(len, 0);
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_free(frame: *mut FrameHandle) {
|
||||
if !frame.is_null() {
|
||||
unsafe { (*frame).ctx = std::ptr::null_mut() };
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_linesize_bytes(frame: FrameHandle) -> i32 {
|
||||
let mut s = lock();
|
||||
let f = frame_of(&mut s, frame.ctx as usize);
|
||||
(f.params.width * 4 * bytes_per_pixel(f.params.format) as i32) as i32
|
||||
}
|
||||
|
||||
// ---- 渲染器族桩 ---------------------------------------------------------
|
||||
|
||||
pub(super) unsafe fn renderer_create_dynamic(backend: *const std::ffi::c_char) -> RendererHandle {
|
||||
if backend.is_null() {
|
||||
return RendererHandle::null();
|
||||
}
|
||||
let id = unsafe { std::ffi::CStr::from_ptr(backend) }.to_str().unwrap_or("");
|
||||
if id != "opengl" {
|
||||
return RendererHandle::null();
|
||||
}
|
||||
magic_handle(GL_RENDERER)
|
||||
}
|
||||
|
||||
pub(super) unsafe fn renderer_init(
|
||||
_renderer: RendererHandle,
|
||||
_gl_context: *mut std::ffi::c_void,
|
||||
) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn renderer_is_open_gl(renderer: RendererHandle) -> i32 {
|
||||
if renderer.ctx as usize != GL_RENDERER {
|
||||
return 0;
|
||||
}
|
||||
lock().gl_available as i32
|
||||
}
|
||||
|
||||
pub(super) unsafe fn renderer_destroy(renderer: *mut RendererHandle) {
|
||||
if !renderer.is_null() {
|
||||
unsafe { (*renderer).ctx = std::ptr::null_mut() };
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 纹理族桩(GL 纹理注册表)------------------------------------------
|
||||
|
||||
pub(super) unsafe fn texture_create(
|
||||
renderer: RendererHandle,
|
||||
params: *const VideoParams,
|
||||
pixels: *const std::ffi::c_void,
|
||||
linesize: i32,
|
||||
) -> TextureHandle {
|
||||
if renderer.ctx as usize != GL_RENDERER || params.is_null() {
|
||||
return TextureHandle::null();
|
||||
}
|
||||
let params = unsafe { *params };
|
||||
if params.width <= 0 || params.height <= 0 {
|
||||
return TextureHandle::null();
|
||||
}
|
||||
let mut s = lock();
|
||||
let id = s.next_gl_id;
|
||||
s.next_gl_id += 1;
|
||||
let mut data = vec![0u8; tight_len(¶ms)];
|
||||
if !pixels.is_null() {
|
||||
// linesize 字节/行(renderer.h 的 bytes 契约;0 → 紧凑行)。
|
||||
let bpp = bytes_per_pixel(params.format);
|
||||
let row_bytes = (params.width as usize) * 4 * bpp;
|
||||
let src_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
|
||||
let src = unsafe { std::slice::from_raw_parts(pixels as *const u8, src_linesize * params.height as usize) };
|
||||
copy_rows(src, src_linesize, &mut data, row_bytes, row_bytes.min(src_linesize), params.height as usize);
|
||||
}
|
||||
s.gl_textures.push(StubGlTexture {
|
||||
id,
|
||||
frame: StubFrame {
|
||||
params,
|
||||
data,
|
||||
},
|
||||
});
|
||||
magic_handle(GL_TEX_BASE + id as usize)
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_id(texture: TextureHandle) -> i32 {
|
||||
gl_id_of_ctx(texture.ctx as usize).unwrap_or(0)
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_get_params(texture: TextureHandle, out: *mut VideoParams) -> i32 {
|
||||
if out.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let mut s = lock();
|
||||
let Some(id) = gl_id_of_ctx(texture.ctx as usize) else {
|
||||
return -1;
|
||||
};
|
||||
let Some(t) = s.gl_textures.iter().find(|t| t.id == id) else {
|
||||
return -1;
|
||||
};
|
||||
unsafe { *out = t.frame.params };
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_download(
|
||||
texture: TextureHandle,
|
||||
pixels: *mut std::ffi::c_void,
|
||||
linesize: i32,
|
||||
) -> i32 {
|
||||
let mut s = lock();
|
||||
let Some(id) = gl_id_of_ctx(texture.ctx as usize) else {
|
||||
return -1;
|
||||
};
|
||||
let Some(t) = s.gl_textures.iter().find(|t| t.id == id) else {
|
||||
return -1;
|
||||
};
|
||||
if pixels.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let bpp = bytes_per_pixel(t.frame.params.format);
|
||||
let row_bytes = (t.frame.params.width as usize) * 4 * bpp;
|
||||
let dst_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
|
||||
let dst = unsafe { std::slice::from_raw_parts_mut(pixels as *mut u8, dst_linesize * t.frame.params.height as usize) };
|
||||
let src = t.frame.data.clone();
|
||||
copy_rows(&src, row_bytes, dst, dst_linesize, row_bytes, t.frame.params.height as usize);
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_upload(
|
||||
texture: TextureHandle,
|
||||
pixels: *const std::ffi::c_void,
|
||||
linesize: i32,
|
||||
) -> i32 {
|
||||
let mut s = lock();
|
||||
let Some(id) = gl_id_of_ctx(texture.ctx as usize) else {
|
||||
return -1;
|
||||
};
|
||||
let Some(t) = s.gl_textures.iter_mut().find(|t| t.id == id) else {
|
||||
return -1;
|
||||
};
|
||||
if pixels.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let bpp = bytes_per_pixel(t.frame.params.format);
|
||||
let row_bytes = (t.frame.params.width as usize) * 4 * bpp;
|
||||
let src_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
|
||||
let src = unsafe { std::slice::from_raw_parts(pixels as *const u8, src_linesize * t.frame.params.height as usize) };
|
||||
let mut data = vec![0u8; tight_len(&t.frame.params)];
|
||||
copy_rows(src, src_linesize, &mut data, row_bytes, row_bytes.min(src_linesize), t.frame.params.height as usize);
|
||||
t.frame.data = data;
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_retain(texture: TextureHandle) -> TextureHandle {
|
||||
texture
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_free(texture: *mut TextureHandle) {
|
||||
if texture.is_null() {
|
||||
return;
|
||||
}
|
||||
let ctx = unsafe { (*texture).ctx as usize };
|
||||
if let Some(id) = gl_id_of_ctx(ctx) {
|
||||
let mut s = lock();
|
||||
s.gl_textures.retain(|t| t.id != id);
|
||||
}
|
||||
unsafe { (*texture).ctx = std::ptr::null_mut() };
|
||||
}
|
||||
|
||||
pub(super) unsafe fn renderer_download_from_texture(
|
||||
renderer: RendererHandle,
|
||||
texture_id: i32,
|
||||
params: *const VideoParams,
|
||||
dst: *mut std::ffi::c_void,
|
||||
linesize: i32,
|
||||
) -> i32 {
|
||||
if renderer.ctx as usize != GL_RENDERER || params.is_null() || dst.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let req = unsafe { *params };
|
||||
let s = lock();
|
||||
let Some(t) = s.gl_textures.iter().find(|t| t.id == texture_id) else {
|
||||
return -1;
|
||||
};
|
||||
let bpp = bytes_per_pixel(req.format);
|
||||
let row_bytes = (req.width as usize) * 4 * bpp;
|
||||
let dst_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
|
||||
let out = unsafe { std::slice::from_raw_parts_mut(dst as *mut u8, dst_linesize * req.height as usize) };
|
||||
let src = t.frame.data.clone();
|
||||
copy_rows(&src, row_bytes, out, dst_linesize, row_bytes, req.height as usize);
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 默认模式单测(无桩;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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! clip 实例:clip ↔ oakrender 纹理桥。
|
||||
//!
|
||||
//! 对应 C++ 的 `OliveClipInstance`。纹理数据经
|
||||
//! [`crate::bridge::render`] 的 oakrender C ABI 流动;OFX 侧只看到
|
||||
//! [`crate::image::Image`](CPU 路径)。
|
||||
//!
|
||||
//! `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`];
|
||||
//! clip handle 即 `&props`)。
|
||||
//!
|
||||
//! `// TODO(bridge)`:`fetch_image`/`store_output_image` 依赖
|
||||
//! bridge::render 的帧访问 C ABI(声明未冻结),保留 todo!()。
|
||||
//! **已落地(M11 第 1 期)**:帧访问 C ABI 在 [`crate::bridge::render`]
|
||||
//! 冻结(`oakrender_display_texture_*`/`oakrender_codec_frame_*`),
|
||||
//! 两处桥实现完成。
|
||||
|
||||
use crate::instance::{OfxRangeD, OfxRectD, RenderScale};
|
||||
use crate::property::PropertySet;
|
||||
|
||||
/// clip 实例。
|
||||
#[repr(C)]
|
||||
pub struct ClipInstance {
|
||||
/// 实例级 clip 属性(当前分量/位深/像素比,协商结果写入;
|
||||
/// 偏移 0,句柄约定)。
|
||||
pub props: PropertySet,
|
||||
/// clip 名。
|
||||
pub name: String,
|
||||
/// 当前输入纹理(oakrender 句柄的借用拷贝;输出 clip 为 None)。
|
||||
input_texture: std::sync::Mutex<Option<crate::bridge::render::TextureHandle>>,
|
||||
/// 当前输出纹理(C++ `output_textures_` 的 phase 1 单槽;
|
||||
/// [`store_output_image`](Self::store_output_image) 的回写目标;
|
||||
/// 输入 clip 为 None)。
|
||||
output_texture: std::sync::Mutex<Option<crate::bridge::render::TextureHandle>>,
|
||||
}
|
||||
|
||||
/// 从 clip 属性读协商分量(getClipPreferences 写入)。
|
||||
fn components_from_props(props: &PropertySet) -> Option<crate::image::Components> {
|
||||
use crate::property::Value;
|
||||
match props.get(crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS, 0)? {
|
||||
Value::String(s) => match s.to_string_lossy().as_ref() {
|
||||
"OfxImageComponentRGBA" => Some(crate::image::Components::Rgba),
|
||||
"OfxImageComponentRGB" => Some(crate::image::Components::Rgb),
|
||||
"OfxImageComponentAlpha" => Some(crate::image::Components::Alpha),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl ClipInstance {
|
||||
/// 按描述符实例化(createInstance 路径调用;公开:宿主与测试
|
||||
/// 都需要构造 clip 实例)。实例 props 是描述符 props 的深拷贝
|
||||
/// (HS: ClipBase 的实例构造,ofxhClip.cpp:57-70——插件在实例期
|
||||
/// 读 supported components 等)。
|
||||
///
|
||||
/// ofxColour(M11 §4):输入 clip 的 kOfxImageClipPropColourspace
|
||||
/// 由宿主写为工作空间(ACEScg,ofxColour.h "Hosts should set this
|
||||
/// property to the colourspace of the input clip. Typically it will
|
||||
/// be set to the working colourspace");输出 clip 由
|
||||
/// GetOutputColourspace action 后写。
|
||||
pub fn from_descriptor(desc: &crate::descriptor::ClipDescriptor) -> Self {
|
||||
let props = desc.props.clone();
|
||||
let name = desc.name.clone();
|
||||
if name != "Output" {
|
||||
props.set_one(
|
||||
crate::host::PROP_CLIP_COLOURSPACE,
|
||||
crate::property::Value::String(
|
||||
std::ffi::CString::new(crate::host::WORKING_COLOURSPACE).unwrap(),
|
||||
),
|
||||
);
|
||||
}
|
||||
Self {
|
||||
props,
|
||||
name,
|
||||
input_texture: std::sync::Mutex::new(None),
|
||||
output_texture: std::sync::Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// 写协商后的像素格式(C++ `OliveClipInstance::setParams` 的 props
|
||||
/// 侧:setPixelDepth/setComponents,oliveclip.cpp:700-709)。
|
||||
/// `format` 为 olive::PixelFormat::Format(0=u8, 2=u16, 3=f16,
|
||||
/// 4=f32);`channels` 为分量数。
|
||||
pub fn set_video_params(&self, format: i32, channels: i32) {
|
||||
let depth = match format {
|
||||
0 => "OfxBitDepthByte",
|
||||
2 => "OfxBitDepthShort",
|
||||
3 => "OfxBitDepthHalf",
|
||||
_ => "OfxBitDepthFloat",
|
||||
};
|
||||
let comps = match channels {
|
||||
1 => "OfxImageComponentAlpha",
|
||||
3 => "OfxImageComponentRGB",
|
||||
_ => "OfxImageComponentRGBA",
|
||||
};
|
||||
self.props.set_one(
|
||||
crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
|
||||
crate::property::Value::String(std::ffi::CString::new(depth).unwrap()),
|
||||
);
|
||||
self.props.set_one(
|
||||
crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS,
|
||||
crate::property::Value::String(std::ffi::CString::new(comps).unwrap()),
|
||||
);
|
||||
}
|
||||
|
||||
/// 写协商 RoD(C++ `setRegionOfDefinition` 的单槽版,
|
||||
/// oliveclip.cpp:674-678——C++ 按 time 存 map,本驱动一帧一槽;
|
||||
/// 落点与 image effect suite 的 clipGetRegionOfDefinition 读取处
|
||||
/// 一致)。
|
||||
pub fn set_region_of_definition(&self, rod: OfxRectD, _time: f64) {
|
||||
use crate::property::Value;
|
||||
self.props.define(
|
||||
"OfxImageEffectPropRegionOfDefinition",
|
||||
vec![
|
||||
Value::Double(rod.x1),
|
||||
Value::Double(rod.y1),
|
||||
Value::Double(rod.x2),
|
||||
Value::Double(rod.y2),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 挂接输入纹理(oaknode 侧 clip 输入值变化时由 param/render 桥
|
||||
/// 调用)。`time` 用于多帧纹理选择。空句柄断开。
|
||||
pub fn set_input_texture(
|
||||
&self,
|
||||
texture: crate::bridge::render::TextureHandle,
|
||||
_time: f64,
|
||||
) {
|
||||
let mut slot = self.input_texture.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if texture.is_null() {
|
||||
*slot = None;
|
||||
} else {
|
||||
*slot = Some(texture);
|
||||
}
|
||||
}
|
||||
|
||||
/// 挂接输出纹理(render 驱动创建并经句柄传入;C++
|
||||
/// `setOutputTexture` 的 phase 1 单槽版)。`time` 用于多帧纹理
|
||||
/// 选择(`// [P2]`)。空句柄断开。
|
||||
pub fn set_output_texture(
|
||||
&self,
|
||||
texture: crate::bridge::render::TextureHandle,
|
||||
_time: f64,
|
||||
) {
|
||||
let mut slot = self
|
||||
.output_texture
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if texture.is_null() {
|
||||
*slot = None;
|
||||
} else {
|
||||
*slot = Some(texture);
|
||||
}
|
||||
}
|
||||
|
||||
/// 抓取本 clip 在 `time` 的图像(OFX clipGetImage 的宿主侧)。
|
||||
/// CPU 路径:把 oakrender 纹理 readback 成 [`crate::image::Image`]
|
||||
/// (像素格式按协商结果,全链路 F32)。
|
||||
/// `// [P2]` GL 路径:clipLoadTexture 语义在此扩展。
|
||||
///
|
||||
/// 第 1 期约束:帧必须是 f32 格式(全链路 F32);`region` 只支持
|
||||
/// None(整帧)——转换(u8→f32 等)与子区域随 renderer 桥落地。
|
||||
pub fn fetch_image(
|
||||
&self,
|
||||
time: f64,
|
||||
scale: RenderScale,
|
||||
region: Option<OfxRectD>,
|
||||
) -> crate::error::Result<crate::image::Image> {
|
||||
use crate::bridge::render::*;
|
||||
use crate::error::Error;
|
||||
|
||||
let _ = (time, scale);
|
||||
if region.is_some() {
|
||||
return Err(Error::Failed("fetch_image 子区域第 1 期不支持".into()));
|
||||
}
|
||||
let texture = self
|
||||
.input_texture
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone()
|
||||
.ok_or(Error::NotFound)?;
|
||||
// 占位纹理(dummy):视作无输入。
|
||||
if unsafe { crate::bridge::render::texture_is_dummy(texture) } != 0 {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
let mut frame = FrameHandle::null();
|
||||
let r = unsafe { crate::bridge::render::texture_get_frame(texture, &mut frame) };
|
||||
if r != 0 || frame.is_null() {
|
||||
return Err(Error::Failed("纹理无 CPU 帧".into()));
|
||||
}
|
||||
let mut params = VideoParams::default();
|
||||
unsafe { crate::bridge::render::frame_get_params(frame, &mut params) };
|
||||
if params.format != PIXEL_FORMAT_F32 {
|
||||
unsafe { crate::bridge::render::frame_free(&mut frame) };
|
||||
return Err(Error::Failed(format!(
|
||||
"输入帧格式 {} 非 F32(第 1 期约束)",
|
||||
params.format
|
||||
)));
|
||||
}
|
||||
let (w, h) = (params.width as f64, params.height as f64);
|
||||
// 分量按协商结果(getClipPreferences 已写入 clip.props)。
|
||||
let components = components_from_props(&self.props).unwrap_or(crate::image::Components::Rgba);
|
||||
|
||||
let mut image = crate::image::Image::allocate(
|
||||
crate::image::BitDepth::Float,
|
||||
components,
|
||||
OfxRectD { x1: 0.0, y1: 0.0, x2: w, y2: h },
|
||||
);
|
||||
let src = unsafe { crate::bridge::render::frame_data(frame) };
|
||||
if src.is_null() {
|
||||
unsafe { crate::bridge::render::frame_free(&mut frame) };
|
||||
return Err(Error::Failed("帧无数据".into()));
|
||||
}
|
||||
// 行优先拷贝(帧行跨度经 linesize 读取——真实 oakrender 帧可
|
||||
// 有行填充;目标 Image 恒紧凑。M11 §4 修复:phase 1 假设紧凑
|
||||
// 行,对真实 oakrender 的填充帧会写错列)。
|
||||
let channels = components.channel_count();
|
||||
let tight = (w as usize) * channels * 4;
|
||||
let row = unsafe { crate::bridge::render::frame_linesize_bytes(frame) } as usize;
|
||||
let row = if row > 0 { row } else { tight };
|
||||
let src_bytes = unsafe { std::slice::from_raw_parts(src as *const u8, row * h as usize) };
|
||||
let dst = image.pixels_mut();
|
||||
for y in 0..h as usize {
|
||||
let s = y * row;
|
||||
let d = y * tight;
|
||||
dst[d..d + tight].copy_from_slice(&src_bytes[s..s + tight]);
|
||||
}
|
||||
unsafe { crate::bridge::render::frame_free(&mut frame) };
|
||||
Ok(image)
|
||||
}
|
||||
|
||||
/// 把输出图像回写为 oakrender 纹理(render 完成后由
|
||||
/// [`crate::instance::Instance::render`] 的调用方使用)。
|
||||
///
|
||||
/// 输出纹理由 oakrender 侧创建并经 [`Self::set_output_texture`]
|
||||
/// 挂入——本函数取该纹理的 CPU 帧(`texture_get_frame`),按帧
|
||||
/// 参数校验 F32 与尺寸后整帧拷贝图像像素(全链路 F32;C++
|
||||
/// pluginrenderer 的 `readback/wrap` 路径第 1 期以 CPU 拷贝表达,
|
||||
/// GL 走 [`crate::bridge::render`] 的 `// [P2]`)。未挂输出纹理
|
||||
/// 或纹理为占位(dummy)→ [`crate::error::Error::NotFound`]。
|
||||
/// 成功返回纹理句柄(借用拷贝,调用方负责其生命周期)。
|
||||
pub fn store_output_image(
|
||||
&self,
|
||||
image: &crate::image::Image,
|
||||
) -> crate::error::Result<crate::bridge::render::TextureHandle> {
|
||||
use crate::bridge::render::*;
|
||||
use crate::error::Error;
|
||||
|
||||
let texture = self
|
||||
.output_texture
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone()
|
||||
.ok_or(Error::NotFound)?;
|
||||
if unsafe { texture_is_dummy(texture) } != 0 {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
let mut frame = FrameHandle::null();
|
||||
let r = unsafe { texture_get_frame(texture, &mut frame) };
|
||||
if r != 0 || frame.is_null() {
|
||||
return Err(Error::Failed("输出纹理无 CPU 帧".into()));
|
||||
}
|
||||
let mut params = VideoParams::default();
|
||||
unsafe { frame_get_params(frame, &mut params) };
|
||||
if params.format != PIXEL_FORMAT_F32 {
|
||||
unsafe { frame_free(&mut frame) };
|
||||
return Err(Error::Failed(format!(
|
||||
"输出帧格式 {} 非 F32(第 1 期约束)",
|
||||
params.format
|
||||
)));
|
||||
}
|
||||
let (w, h) = (params.width as usize, params.height as usize);
|
||||
// 图像与帧必须同尺寸(全链路 F32;宽高/行宽/总长逐项校验)。
|
||||
let tight = w * image.components().channel_count() * 4;
|
||||
if tight != image.row_bytes() || tight * h != image.pixels().len() {
|
||||
unsafe { frame_free(&mut frame) };
|
||||
return Err(Error::Failed("图像尺寸与输出帧不一致".into()));
|
||||
}
|
||||
let dst = unsafe { frame_data(frame) };
|
||||
if dst.is_null() {
|
||||
unsafe { frame_free(&mut frame) };
|
||||
return Err(Error::Failed("输出帧无数据".into()));
|
||||
}
|
||||
// 行优先拷贝(目标帧行跨度经 linesize 读取——真实 oakrender
|
||||
// 帧可有行填充;M11 §4 修复同 fetch_image)。
|
||||
let row = unsafe { frame_linesize_bytes(frame) } as usize;
|
||||
let row = if row > 0 { row } else { tight };
|
||||
let dst_bytes = unsafe { std::slice::from_raw_parts_mut(dst as *mut u8, row * h) };
|
||||
let pixels = image.pixels();
|
||||
for y in 0..h {
|
||||
let d = y * row;
|
||||
let s = y * tight;
|
||||
dst_bytes[d..d + tight].copy_from_slice(&pixels[s..s + tight]);
|
||||
}
|
||||
unsafe { frame_free(&mut frame) };
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
/// 本 clip 的时间域(clipGetFrameRange)。
|
||||
///
|
||||
/// `// TODO(bridge)`:输入范围经 oakrender 帧的时间基推导
|
||||
/// (time_base)——随 renderer 桥落地。
|
||||
pub fn frame_range(&self) -> crate::error::Result<OfxRangeD> {
|
||||
let _ = OfxRangeD::default();
|
||||
Err(crate::error::Error::Failed("frame_range 待 renderer 桥".into()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! describe 的产物:效果描述符与 clip 描述符。
|
||||
//!
|
||||
//! 参照:HS: ofxhImageEffect.cpp `Descriptor`(describe action 期间
|
||||
//! 插件可读写的属性容器)。
|
||||
//!
|
||||
//! 句柄约定([`crate::suites::tag`]):`props` 在偏移 0 且
|
||||
//! `#[repr(C)]`;`params`/`clips` 元素装箱(Vec 重分配不移动对象)。
|
||||
|
||||
use std::ffi::CString;
|
||||
|
||||
use crate::param::ParamDef;
|
||||
use crate::property::{PropertySet, Value};
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
// ---- clip 描述符属性(ofxImageEffect.h / ofxCore.h)----
|
||||
|
||||
/// kOfxImageClipPropOptional。
|
||||
pub(crate) const CLIP_OPTIONAL: &str = "OfxImageClipPropOptional";
|
||||
/// kOfxImageClipPropIsMask。
|
||||
pub(crate) const CLIP_IS_MASK: &str = "OfxImageClipPropIsMask";
|
||||
/// kOfxImageClipPropFieldExtraction。
|
||||
pub(crate) const CLIP_FIELD_EXTRACTION: &str = "OfxImageClipPropFieldExtraction";
|
||||
/// kOfxImageEffectPropSupportedComponents。
|
||||
pub(crate) const CLIP_SUPPORTED_COMPONENTS: &str = "OfxImageEffectPropSupportedComponents";
|
||||
/// kOfxImageEffectPropTemporalClipAccess。
|
||||
pub(crate) const CLIP_TEMPORAL_ACCESS: &str = "OfxImageEffectPropTemporalClipAccess";
|
||||
/// kOfxImageEffectPropSupportsTiles。
|
||||
pub(crate) const CLIP_SUPPORTS_TILES: &str = "OfxImageEffectPropSupportsTiles";
|
||||
/// kOfxImageFieldDoubled(FieldExtraction 默认值)。
|
||||
pub(crate) const CLIP_FIELD_DOUBLED: &str = "OfxImageFieldDoubled";
|
||||
/// kOfxTypeClip。
|
||||
pub(crate) const CLIP_TYPE: &str = "OfxTypeClip";
|
||||
|
||||
/// clip 描述符(describe 期间由插件定义)。
|
||||
/// `#[repr(C)]` + props 在偏移 0(句柄约定;describe 期 clip handle
|
||||
/// 即 &props,与实例期 [`crate::clip::ClipInstance`] 共用 CLIP 标签)。
|
||||
#[repr(C)]
|
||||
pub struct ClipDescriptor {
|
||||
/// 描述属性:label、可选性(kOfxImageClipPropOptional)、支持分量
|
||||
/// (kOfxImageEffectPropSupportedComponents)、field 支持等。
|
||||
pub props: PropertySet,
|
||||
/// clip 名(如 "Source"/"Output")。
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl ClipDescriptor {
|
||||
/// 按名构建(镜像 HS ofxhClip.cpp `ClipDescriptor::ClipDescriptor`
|
||||
/// + clipDescriptorStuffs 属性表,ofxhClip.cpp:22-38)。公开:
|
||||
/// 测试构造 clip 实例(GL suite 往返)。
|
||||
pub fn new(name: &str) -> Self {
|
||||
let props = PropertySet::new();
|
||||
props.set_one(crate::param::PROP_TYPE, Value::String(cs(CLIP_TYPE)));
|
||||
props.set_one(crate::param::PROP_NAME, Value::String(cs(name)));
|
||||
props.set_one(crate::param::PROP_LABEL, Value::String(cs(name)));
|
||||
props.set_one(crate::param::PROP_SHORT_LABEL, Value::String(cs("")));
|
||||
props.set_one(crate::param::PROP_LONG_LABEL, Value::String(cs("")));
|
||||
props.define(CLIP_SUPPORTED_COMPONENTS, vec![]);
|
||||
props.set_one(CLIP_TEMPORAL_ACCESS, Value::Int(0));
|
||||
props.set_one(CLIP_OPTIONAL, Value::Int(0));
|
||||
props.set_one(CLIP_IS_MASK, Value::Int(0));
|
||||
props.set_one(CLIP_FIELD_EXTRACTION, Value::String(cs(CLIP_FIELD_DOUBLED)));
|
||||
props.set_one(CLIP_SUPPORTS_TILES, Value::Int(1));
|
||||
// ofxColour(M11 §4):clip 色彩空间属性族。Colourspace 由宿主
|
||||
// 在实例化时写入(输入 clip = 工作空间 ACEScg);Preferred 由
|
||||
// 插件在 GetClipPreferences 写(宿主侧预置空数组)。
|
||||
props.set_one(
|
||||
crate::host::PROP_CLIP_COLOURSPACE,
|
||||
Value::String(cs("")),
|
||||
);
|
||||
props.define(crate::host::PROP_CLIP_PREFERRED_COLOURSPACES, vec![]);
|
||||
Self {
|
||||
props,
|
||||
name: name.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 效果描述符。
|
||||
/// `#[repr(C)]` + props 在偏移 0(句柄约定:describe 期 effect/
|
||||
/// param-set handle 即 &props)。
|
||||
#[repr(C)]
|
||||
pub struct EffectDescriptor {
|
||||
/// 效果级属性:label、描述、分组、单帧/时间域标记等。
|
||||
pub props: PropertySet,
|
||||
/// 参数定义集(describe 期间插件经 Param suite 填充;
|
||||
/// 值类型见 [`crate::param::ParamDef`];Box 保证句柄地址稳定)。
|
||||
pub params: Vec<Box<ParamDef>>,
|
||||
/// clip 定义集(含 "Output";Box 保证句柄地址稳定)。
|
||||
pub clips: Vec<Box<ClipDescriptor>>,
|
||||
}
|
||||
|
||||
impl EffectDescriptor {
|
||||
/// 空描述符(describe action 前由 host 创建并预置根属性)。
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
props: PropertySet::new(),
|
||||
params: Vec::new(),
|
||||
clips: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 按名找 clip。
|
||||
pub fn clip(&self, name: &str) -> Option<&ClipDescriptor> {
|
||||
self.clips.iter().find(|c| c.name == name).map(|b| b.as_ref())
|
||||
}
|
||||
|
||||
/// 按名找参数定义。
|
||||
pub fn param(&self, name: &str) -> Option<&ParamDef> {
|
||||
self.params.iter().find(|p| p.name == name).map(|b| b.as_ref())
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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/Copy:C 侧按值传句柄,复制后再 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
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,968 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! 插件实例:action 调用面。
|
||||
//!
|
||||
//! 对应 C++ 的 `OlivePluginInstance`。语义参照:
|
||||
//! HS: ofxhImageEffect.cpp `Instance`(action 调用序列与参数集组装)。
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::clip::ClipInstance;
|
||||
use crate::host::Plugin;
|
||||
use crate::param::ParamSetInstance;
|
||||
use crate::property::PropertySet;
|
||||
|
||||
/// paramEditBegin/End 的编辑事务状态(undo 分组)。对应 C++
|
||||
/// oliveplugininstance.cpp 的 `edit_depth_`/`edit_command_`:
|
||||
/// 事务内的多次参数回写合并为一条 multi 命令,editEnd 时整体
|
||||
/// redo 后释放。
|
||||
#[repr(C)]
|
||||
pub struct EditTransaction {
|
||||
/// 嵌套深度(editBegin/End 必须配对)。
|
||||
depth: i32,
|
||||
/// 事务累积的 multi 命令(空句柄 = 尚无子命令)。
|
||||
multi: crate::bridge::undo::CommandHandle,
|
||||
/// 事务内回写次数(标签计数)。
|
||||
param_count: i32,
|
||||
/// 第一条回写的标签。
|
||||
first_label: String,
|
||||
}
|
||||
|
||||
impl EditTransaction {
|
||||
/// 空事务(无嵌套、无累积命令)。
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
depth: 0,
|
||||
multi: crate::bridge::undo::CommandHandle::null(),
|
||||
param_count: 0,
|
||||
first_label: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 时间范围(OFX 规范双精度秒)。
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct OfxRangeD {
|
||||
/// 起始(含)。
|
||||
pub min: f64,
|
||||
/// 结束(含)。
|
||||
pub max: f64,
|
||||
}
|
||||
|
||||
/// 矩形(规范坐标,double)。
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct OfxRectD {
|
||||
/// 左。
|
||||
pub x1: f64,
|
||||
/// 上。
|
||||
pub y1: f64,
|
||||
/// 右。
|
||||
pub x2: f64,
|
||||
/// 下。
|
||||
pub y2: f64,
|
||||
}
|
||||
|
||||
/// 像素比/场序等渲染参量(render action 的 in_args 子集)。
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct RenderScale {
|
||||
/// x 方向渲染比例。
|
||||
pub x: f64,
|
||||
/// y 方向渲染比例。
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
/// 插件实例。`Arc<RefBox<Instance>>` 管理生命周期;身份注册见
|
||||
/// [`crate::handle::Registry`](param 桥按身份反查)。
|
||||
///
|
||||
/// `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`];
|
||||
/// 实例期 effect/param-set handle 即 `&props`)。
|
||||
#[repr(C)]
|
||||
pub struct Instance {
|
||||
/// 实例级属性集(偏移 0,句柄约定)。
|
||||
pub props: PropertySet,
|
||||
/// 所属插件。
|
||||
pub plugin: Arc<Plugin>,
|
||||
/// 上下文(kOfxImageEffectContext*)。
|
||||
pub context: String,
|
||||
/// 参数实例集(createInstance 后由 describe 产物实例化)。
|
||||
pub params: ParamSetInstance,
|
||||
/// clip 实例集(含 "Output";Box 保证句柄地址稳定)。
|
||||
pub clips: Vec<Box<ClipInstance>>,
|
||||
/// 绑定的 oaknode 节点身份(param 桥写入;0 = 未绑定)。
|
||||
pub node_identity: std::sync::atomic::AtomicUsize,
|
||||
/// destroyInstance action 是否已通知(析构幂等门;Drop 驱动;
|
||||
/// 公开:宿主 shutdown 与测试都要置位)。
|
||||
pub destroyed: std::sync::atomic::AtomicBool,
|
||||
/// beginSequenceRender 的时间域(timeline getTimeBounds 用;
|
||||
/// endSequenceRender 清除)。
|
||||
pub sequence_range: std::sync::Mutex<Option<OfxRangeD>>,
|
||||
/// facade 注册的进度回调(oakplugin_instance_set_progress_cb)。
|
||||
pub progress_cb: std::sync::Mutex<Option<(crate::progress::ProgressFn, usize)>>,
|
||||
/// facade 取消标记(oakplugin_instance_cancel)。
|
||||
pub cancel: std::sync::atomic::AtomicBool,
|
||||
/// paramEditBegin/End 的编辑事务(undo 分组)。
|
||||
pub edit: std::sync::Mutex<EditTransaction>,
|
||||
/// 实例级渲染串行化(对应 C++ `OlivePluginInstance::mutex`;
|
||||
/// pluginrenderer.cpp:1436-1444 的 instance_lock——渲染路径非
|
||||
/// 线程安全,并发 render 必须互斥)。
|
||||
pub render_lock: std::sync::Mutex<()>,
|
||||
}
|
||||
|
||||
/// 实例销毁路径:先通知 destroyInstance action,再摘除身份登记。
|
||||
/// (RefBox 归零时 Drop 触发——action 通知必须在对象析构前发出。)
|
||||
impl Drop for Instance {
|
||||
fn drop(&mut self) {
|
||||
if !self.destroyed.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
||||
self.notify_destroy();
|
||||
}
|
||||
crate::suites::param::unregister_params_of(&self.props as *const _ as usize);
|
||||
crate::host::instance_registry().unregister(
|
||||
&self.props as *const crate::property::PropertySet as usize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Instance {
|
||||
/// 绑定 oaknode 节点(C++ `set_node_handle` 的 Rust 侧;装配期由
|
||||
/// facade/测试调用)。`identity` 为 [`crate::host::instance_registry`]
|
||||
/// 无关的 oaknode 节点身份(`oaknode_node_identity` 的地址语义);
|
||||
/// 0 解除绑定。
|
||||
pub fn bind_node(&self, identity: usize) {
|
||||
self.node_identity
|
||||
.store(identity, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// paramEditBegin:进入编辑事务(可嵌套;首次进入重置事务状态)。
|
||||
pub fn edit_begin(&self) {
|
||||
let mut e = self.edit.lock().unwrap_or_else(|e| e.into_inner());
|
||||
e.depth += 1;
|
||||
if e.depth == 1 {
|
||||
e.multi = crate::bridge::undo::CommandHandle::null();
|
||||
e.param_count = 0;
|
||||
e.first_label.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// paramEditEnd:退出编辑事务;最外层结束时把累积的 multi 命令
|
||||
/// redo 生效并释放(本 crate 无 undo 栈——命令在 oakundo 侧
|
||||
/// 提交,见 C++ `submit_undo_command` 的 fallback 分支)。
|
||||
pub fn edit_end(&self) {
|
||||
let mut e = self.edit.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if e.depth > 0 {
|
||||
e.depth -= 1;
|
||||
}
|
||||
if e.depth == 0 && !e.multi.is_null() {
|
||||
unsafe { crate::bridge::undo::command_redo_now(e.multi) };
|
||||
unsafe { crate::bridge::undo::command_free(&mut e.multi) };
|
||||
e.param_count = 0;
|
||||
e.first_label.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否处于编辑事务内(param 桥回写据此决定打包成 multi)。
|
||||
pub fn in_edit(&self) -> bool {
|
||||
self.edit
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.depth
|
||||
> 0
|
||||
}
|
||||
|
||||
/// 事务感知的 undo 提交(param 桥回写用;对应 C++
|
||||
/// oliveplugininstance.cpp:390-414 `submit_undo_command`):
|
||||
/// 编辑事务内并入 multi(子命令立即 redo 生效,值即时可见),
|
||||
/// 否则单命令 redo 后释放。
|
||||
pub(crate) fn submit_undo_command(
|
||||
&self,
|
||||
mut cmd: crate::bridge::undo::CommandHandle,
|
||||
label: &str,
|
||||
) {
|
||||
if cmd.is_null() {
|
||||
return;
|
||||
}
|
||||
if self.in_edit() {
|
||||
let mut e = self.edit.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if e.multi.is_null() {
|
||||
e.multi = unsafe { crate::bridge::undo::command_init_multi() };
|
||||
}
|
||||
e.param_count += 1;
|
||||
if e.first_label.is_empty() {
|
||||
e.first_label = label.to_string();
|
||||
}
|
||||
unsafe { crate::bridge::undo::command_redo_now(cmd) };
|
||||
unsafe { crate::bridge::undo::command_multi_add_child(e.multi, cmd) };
|
||||
} else {
|
||||
unsafe { crate::bridge::undo::command_redo_now(cmd) };
|
||||
unsafe { crate::bridge::undo::command_free(&mut cmd) };
|
||||
}
|
||||
}
|
||||
|
||||
/// getClipPreferences action,返回协商结果(输出 clip 的分量/
|
||||
/// 位深/像素比/field 与帧率)。协商顺序逐行对照
|
||||
/// HS: ofxhImageEffect.cpp `Instance::getClipPreferences`——
|
||||
/// 当年调试重灾区,实现时必须附行号注释。
|
||||
///
|
||||
/// 协商流程(HS: ofxhImageEffect.cpp:1686-1740):
|
||||
/// 1. out args 预置 clipPrefsStuffs(frameRate=1、premult、
|
||||
/// fieldOrder、continuousSamples、frameVarying,HS:1697-1710);
|
||||
/// 2. 预置 per-clip 的 "OfxImageClipPropComponents_<name>" 等
|
||||
/// (默认 RGBA/Float/PAR=1,HS:1717-1727);
|
||||
/// 3. 调 action(out args 即协商产物,HS:1708-1718);
|
||||
/// 4. 回灌:per-clip 分量/位深/像素比写回 clip 实例属性,
|
||||
/// 输出帧率/fielding/premult 记入实例(HS:1721-1740)。
|
||||
pub fn get_clip_preferences(&self) -> crate::error::Result<ClipPreferences> {
|
||||
use crate::property::Value;
|
||||
use crate::host::{
|
||||
ACTION_GET_CLIP_PREFERENCES, CLIP_PREF_COMPONENTS, CLIP_PREF_DEPTH, CLIP_PREF_PAR,
|
||||
PROP_CONTINUOUS_SAMPLES, PROP_FIELD_ORDER, PROP_FRAME_RATE, PROP_FRAME_VARYING,
|
||||
PROP_PREMULT,
|
||||
};
|
||||
|
||||
// 1. clipPrefsStuffs(HS:1697-1706 的默认值)。
|
||||
let out = PropertySet::new();
|
||||
out.set_one(PROP_FRAME_RATE, Value::Double(1.0));
|
||||
out.set_one(PROP_PREMULT, Value::String(CString::new("").unwrap()));
|
||||
out.set_one(PROP_FIELD_ORDER, Value::String(CString::new("").unwrap()));
|
||||
out.set_one(PROP_CONTINUOUS_SAMPLES, Value::Int(0));
|
||||
out.set_one(PROP_FRAME_VARYING, Value::Int(0));
|
||||
|
||||
// 2. per-clip 预置(HS:1717-1727;phase 1 全链路 F32+RGBA)。
|
||||
for clip in &self.clips {
|
||||
out.set_one(&crate::host::clip_pref_prop(CLIP_PREF_COMPONENTS, &clip.name), Value::String(CString::new("OfxImageComponentRGBA").unwrap()));
|
||||
out.set_one(&crate::host::clip_pref_prop(CLIP_PREF_DEPTH, &clip.name), Value::String(CString::new("OfxBitDepthFloat").unwrap()));
|
||||
out.set_one(&crate::host::clip_pref_prop(CLIP_PREF_PAR, &clip.name), Value::Double(1.0));
|
||||
}
|
||||
|
||||
// 3. action。
|
||||
let inst_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let empty = PropertySet::new();
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
.call_action(ACTION_GET_CLIP_PREFERENCES, inst_handle, &empty, &out)
|
||||
};
|
||||
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
|
||||
return Err(crate::error::Error::Failed(format!(
|
||||
"getClipPreferences 失败:{stat}"
|
||||
)));
|
||||
}
|
||||
|
||||
// 4. 回灌(HS:1721-1740:每 clip 的协商结果写回实例属性)。
|
||||
let mut output_components = "OfxImageComponentRGBA".to_string();
|
||||
let mut output_bit_depth = "OfxBitDepthFloat".to_string();
|
||||
let mut output_par = 1.0;
|
||||
for clip in &self.clips {
|
||||
let comp_name = crate::host::clip_pref_prop(CLIP_PREF_COMPONENTS, &clip.name);
|
||||
let depth_name = crate::host::clip_pref_prop(CLIP_PREF_DEPTH, &clip.name);
|
||||
let par_name = crate::host::clip_pref_prop(CLIP_PREF_PAR, &clip.name);
|
||||
let comps = out
|
||||
.get(&comp_name, 0)
|
||||
.map(|v| match v {
|
||||
Value::String(s) => s.to_string_lossy().into_owned(),
|
||||
_ => "OfxImageComponentRGBA".to_string(),
|
||||
})
|
||||
.unwrap_or_else(|| "OfxImageComponentRGBA".to_string());
|
||||
let depth = out
|
||||
.get(&depth_name, 0)
|
||||
.map(|v| match v {
|
||||
Value::String(s) => s.to_string_lossy().into_owned(),
|
||||
_ => "OfxBitDepthFloat".to_string(),
|
||||
})
|
||||
.unwrap_or_else(|| "OfxBitDepthFloat".to_string());
|
||||
let par = out
|
||||
.get(&par_name, 0)
|
||||
.and_then(|v| match v {
|
||||
Value::Double(d) => Some(d),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(1.0);
|
||||
clip.props.set_one(
|
||||
crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS,
|
||||
Value::String(CString::new(comps.clone()).unwrap()),
|
||||
);
|
||||
clip.props.set_one(
|
||||
crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
|
||||
Value::String(CString::new(depth.clone()).unwrap()),
|
||||
);
|
||||
clip.props.set_one(
|
||||
"OfxImagePropPixelAspectRatio",
|
||||
Value::Double(par),
|
||||
);
|
||||
if clip.name == "Output" {
|
||||
output_components = comps;
|
||||
output_bit_depth = depth;
|
||||
output_par = par;
|
||||
}
|
||||
}
|
||||
|
||||
let frame_rate = out
|
||||
.get(PROP_FRAME_RATE, 0)
|
||||
.and_then(|v| match v {
|
||||
Value::Double(d) => Some(d),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(24.0);
|
||||
let field = out
|
||||
.get(PROP_FIELD_ORDER, 0)
|
||||
.map(|v| match v {
|
||||
Value::String(s) => s.to_string_lossy().into_owned(),
|
||||
_ => String::new(),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(ClipPreferences {
|
||||
output_components,
|
||||
output_bit_depth,
|
||||
pixel_aspect_ratio: output_par,
|
||||
frame_rate,
|
||||
field,
|
||||
})
|
||||
}
|
||||
|
||||
/// getRegionOfDefinition action(HS: ofxhImageEffect.cpp:1087-1130:
|
||||
/// in args = time + renderScale;out args = RegionOfDefinition)。
|
||||
pub fn get_region_of_definition(
|
||||
&self,
|
||||
time: f64,
|
||||
scale: RenderScale,
|
||||
) -> crate::error::Result<OfxRectD> {
|
||||
use crate::property::Value;
|
||||
use crate::host::{ACTION_GET_ROD, PROP_RENDER_SCALE, PROP_ROD, PROP_TIME};
|
||||
|
||||
let in_args = PropertySet::new();
|
||||
in_args.set_one(PROP_TIME, Value::Double(time));
|
||||
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(scale.x), Value::Double(scale.y)]);
|
||||
// out args 预定义(HS 行为:宿主先建属性表,插件只管写——
|
||||
// 缺失属性上插件的 propSet 会失败被忽略)。
|
||||
let out = PropertySet::new();
|
||||
out.define(
|
||||
PROP_ROD,
|
||||
vec![
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
],
|
||||
);
|
||||
|
||||
let inst_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
.call_action(ACTION_GET_ROD, inst_handle, &in_args, &out)
|
||||
};
|
||||
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
|
||||
return Err(crate::error::Error::Failed(format!("getRoD 失败:{stat}")));
|
||||
}
|
||||
read_rect(&out, PROP_ROD).ok_or(crate::error::Error::Failed("getRoD 无输出".into()))
|
||||
}
|
||||
|
||||
/// getRegionsOfInterest action:输入 clip 的 RoI 写入 out_args
|
||||
/// 属性集,返回值即各 clip 的 RoI 列表(与 `clips` 顺序一致)。
|
||||
///
|
||||
/// 约定名 "OfxImageEffectPropRegionOfInterest_<clipname>"(HS:
|
||||
/// ofxhImageEffect.cpp getRegionsOfInterest 的 per-clip 前缀)。
|
||||
pub fn get_regions_of_interest(
|
||||
&self,
|
||||
time: f64,
|
||||
scale: RenderScale,
|
||||
region: OfxRectD,
|
||||
) -> crate::error::Result<Vec<OfxRectD>> {
|
||||
use crate::property::Value;
|
||||
use crate::host::{ACTION_GET_ROI, PROP_RENDER_SCALE, PROP_ROI, PROP_TIME};
|
||||
|
||||
let in_args = PropertySet::new();
|
||||
in_args.set_one(PROP_TIME, Value::Double(time));
|
||||
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(scale.x), Value::Double(scale.y)]);
|
||||
in_args.define(
|
||||
PROP_ROI,
|
||||
vec![
|
||||
Value::Double(region.x1),
|
||||
Value::Double(region.y1),
|
||||
Value::Double(region.x2),
|
||||
Value::Double(region.y2),
|
||||
],
|
||||
);
|
||||
// out args 预定义 per-clip ROI 属性(HS 约定名前缀)。
|
||||
let out = PropertySet::new();
|
||||
for clip in &self.clips {
|
||||
let name = format!("OfxImageEffectPropRegionOfInterest_{}", clip.name);
|
||||
out.define(
|
||||
&name,
|
||||
vec![
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
let inst_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
.call_action(ACTION_GET_ROI, inst_handle, &in_args, &out)
|
||||
};
|
||||
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
|
||||
return Err(crate::error::Error::Failed(format!("getRoI 失败:{stat}")));
|
||||
}
|
||||
let mut rois = Vec::with_capacity(self.clips.len());
|
||||
for clip in &self.clips {
|
||||
let name = format!("OfxImageEffectPropRegionOfInterest_{}", clip.name);
|
||||
let rect = read_rect(&out, &name)
|
||||
// 未设置 → 默认整个 region(OFX 语义:插件可不写)。
|
||||
.unwrap_or(region);
|
||||
rois.push(rect);
|
||||
}
|
||||
Ok(rois)
|
||||
}
|
||||
|
||||
/// isIdentity action:返回 Some((time, input_clip_name)) 表示本帧
|
||||
/// 直接透传该输入 clip;None 表示需要真正 render。
|
||||
///
|
||||
/// 参照 HS: ofxhImageEffect.cpp:1378-1450:in args = time + scale +
|
||||
/// renderWindow + fieldToRender;out args = kOfxImageEffectPropIsIdentity
|
||||
/// (输入 clip 名)+ kOfxPropTime(透传时间,可改)。
|
||||
pub fn is_identity(&self, time: f64) -> crate::error::Result<Option<(f64, String)>> {
|
||||
use crate::property::Value;
|
||||
use crate::host::{
|
||||
ACTION_IS_IDENTITY, PROP_FIELD_TO_RENDER, PROP_IS_IDENTITY, PROP_RENDER_SCALE,
|
||||
PROP_RENDER_WINDOW, PROP_TIME,
|
||||
};
|
||||
|
||||
let in_args = PropertySet::new();
|
||||
in_args.set_one(PROP_TIME, Value::Double(time));
|
||||
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(1.0), Value::Double(1.0)]);
|
||||
in_args.define(
|
||||
PROP_RENDER_WINDOW,
|
||||
vec![
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
],
|
||||
);
|
||||
in_args.set_one(PROP_FIELD_TO_RENDER, Value::String(CString::new("OfxImageFieldBoth").unwrap()));
|
||||
// out args 预定义:IsIdentity(String)+ Time(Double)。
|
||||
let out = PropertySet::new();
|
||||
out.set_one(PROP_IS_IDENTITY, Value::String(CString::new("").unwrap()));
|
||||
out.set_one(PROP_TIME, Value::Double(time));
|
||||
|
||||
let inst_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
.call_action(ACTION_IS_IDENTITY, inst_handle, &in_args, &out)
|
||||
};
|
||||
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
|
||||
return Err(crate::error::Error::Failed(format!("isIdentity 失败:{stat}")));
|
||||
}
|
||||
let identity = out.get(PROP_IS_IDENTITY, 0);
|
||||
match identity {
|
||||
Some(Value::String(s)) if !s.is_empty() => {
|
||||
let clip_name = s.to_string_lossy().into_owned();
|
||||
let t = out
|
||||
.get(PROP_TIME, 0)
|
||||
.and_then(|v| match v {
|
||||
Value::Double(d) => Some(d),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(time);
|
||||
Ok(Some((t, clip_name)))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// render action 的 in args 组装(CPU/GL 共用;GL 模式加
|
||||
/// kOfxImageEffectPropOpenGLEnabled=1,ofxGPURender.h:117)。
|
||||
fn render_in_args(time: f64, scale: RenderScale, window: OfxRectD, gl_enabled: bool) -> PropertySet {
|
||||
use crate::property::Value;
|
||||
use crate::host::{
|
||||
PROP_FIELD_TO_RENDER, PROP_INTERACTIVE_RENDER, PROP_NO_SPATIAL_AWARENESS,
|
||||
PROP_RENDER_QUALITY_DRAFT, PROP_RENDER_SCALE, PROP_RENDER_WINDOW,
|
||||
PROP_SEQUENTIAL_RENDER, PROP_TIME,
|
||||
};
|
||||
|
||||
let in_args = PropertySet::new();
|
||||
in_args.set_one(PROP_TIME, Value::Double(time));
|
||||
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(scale.x), Value::Double(scale.y)]);
|
||||
in_args.define(
|
||||
PROP_RENDER_WINDOW,
|
||||
vec![
|
||||
Value::Double(window.x1),
|
||||
Value::Double(window.y1),
|
||||
Value::Double(window.x2),
|
||||
Value::Double(window.y2),
|
||||
],
|
||||
);
|
||||
in_args.set_one(PROP_FIELD_TO_RENDER, Value::String(CString::new("OfxImageFieldBoth").unwrap()));
|
||||
in_args.set_one(PROP_SEQUENTIAL_RENDER, Value::Int(0));
|
||||
in_args.set_one(PROP_INTERACTIVE_RENDER, Value::Int(0));
|
||||
in_args.set_one(PROP_RENDER_QUALITY_DRAFT, Value::Int(0));
|
||||
in_args.set_one(PROP_NO_SPATIAL_AWARENESS, Value::Int(0));
|
||||
if gl_enabled {
|
||||
in_args.set_one(crate::host::PROP_GL_ENABLED, Value::Int(1));
|
||||
}
|
||||
in_args
|
||||
}
|
||||
|
||||
/// render action(CPU 路径)。`output` 为已按协商格式分配的输出
|
||||
/// 图像(Arc:插件经 clipGetImage(Output) 取用,render 驱动与
|
||||
/// suite 各自持强引用);输入 clip 的图像由调用方经
|
||||
/// [`ClipInstance::fetch_image`] 备好。GL 路径见
|
||||
/// [`Instance::render_gl`]。
|
||||
///
|
||||
/// 声明原为 `&mut Image`:输出图像需与 suite 共享强引用(TLS +
|
||||
/// LIVE_IMAGES 表),&mut 无法表达——改为 `Arc<Image>`。
|
||||
pub fn render(
|
||||
&self,
|
||||
time: f64,
|
||||
scale: RenderScale,
|
||||
window: OfxRectD,
|
||||
output: std::sync::Arc<crate::image::Image>,
|
||||
) -> crate::error::Result<()> {
|
||||
use crate::host::ACTION_RENDER;
|
||||
|
||||
let in_args = Self::render_in_args(time, scale, window, false);
|
||||
|
||||
// facade 取消标记:render 入口即短路(多帧循环的帧间取消)。
|
||||
if self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return Err(crate::error::Error::Failed("已取消".into()));
|
||||
}
|
||||
|
||||
// 渲染上下文(TLS):timeline/progress/clipGetImage 读取。
|
||||
let range = self
|
||||
.sequence_range
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.unwrap_or(OfxRangeD { min: 0.0, max: 0.0 });
|
||||
crate::suites::set_render_ctx(Some(crate::suites::RenderCtx {
|
||||
time,
|
||||
scale,
|
||||
range,
|
||||
}));
|
||||
crate::suites::set_current_output(Some(output.clone()));
|
||||
|
||||
// 进度报告器(facade 回调 → Progress suite)。
|
||||
if let Some((cb, userdata)) = self
|
||||
.progress_cb
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone()
|
||||
{
|
||||
crate::suites::progress::set_current(Some(unsafe {
|
||||
crate::progress::ProgressReporter::new(cb, userdata as *mut std::ffi::c_void)
|
||||
}));
|
||||
}
|
||||
|
||||
let inst_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let out = PropertySet::new();
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
.call_action(ACTION_RENDER, inst_handle, &in_args, &out)
|
||||
};
|
||||
|
||||
// 清理 TLS(action 返回后渲染上下文必须消失)。
|
||||
crate::suites::set_current_output(None);
|
||||
crate::suites::set_render_ctx(None);
|
||||
crate::suites::progress::set_current(None);
|
||||
|
||||
if stat != crate::suites::status::OK {
|
||||
return Err(crate::error::Error::Failed(format!("render 失败:{stat}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// GL render action(M11 §4;ofxGPURender.h 的 Render 动作 GL
|
||||
/// 模式)。与 [`Instance::render`](CPU)并存。
|
||||
///
|
||||
/// 前置契约(render 驱动遵守):调用方已把 `renderer` 的 GL
|
||||
/// 上下文置为 current(ofxGPURender.h "OpenGL Current Context":
|
||||
/// 宿主只在 Render/Begin/EndSequenceRender/Attach/Detach 期间
|
||||
/// 要求上下文 current——本实现的约定是 oakrender 的 PluginJob
|
||||
/// 路径在进入前做好),且 `output_texture` 已附着为渲染器输出
|
||||
/// 目标(等价 C++ `PluginRenderer::attach_output_texture`)。
|
||||
///
|
||||
/// action 序列:kOfxActionOpenGLContextAttached → render(in args
|
||||
/// 带 kOfxImageEffectPropOpenGLEnabled=1)→
|
||||
/// kOfxActionOpenGLContextDetached(ofxGPURender.h:345-371;
|
||||
/// attach/detach 必须配对)。渲染结果留在 GL 输出纹理上(插件
|
||||
/// 直接画进附着目标),宿主不做 CPU 回读;GL 模式下
|
||||
/// clipGetImage(Output) 不可用(插件按规范走 OpenGL suite——
|
||||
/// ofxGPURender.h "the effect SHOULD access all its images through
|
||||
/// the OpenGL suite")。render 返回前对未释放的输入 GL 纹理做
|
||||
/// 兜底清理([`crate::suites::gl_render::purge_leftovers`])。
|
||||
pub fn render_gl(
|
||||
&self,
|
||||
time: f64,
|
||||
scale: RenderScale,
|
||||
window: OfxRectD,
|
||||
renderer: crate::bridge::render::RendererHandle,
|
||||
output_texture: crate::bridge::render::TextureHandle,
|
||||
) -> crate::error::Result<()> {
|
||||
use crate::host::ACTION_RENDER;
|
||||
|
||||
let in_args = Self::render_in_args(time, scale, window, true);
|
||||
|
||||
if self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return Err(crate::error::Error::Failed("已取消".into()));
|
||||
}
|
||||
|
||||
let range = self
|
||||
.sequence_range
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.unwrap_or(OfxRangeD { min: 0.0, max: 0.0 });
|
||||
crate::suites::set_render_ctx(Some(crate::suites::RenderCtx {
|
||||
time,
|
||||
scale,
|
||||
range,
|
||||
}));
|
||||
// GL 纹理位深协商(插件 kOfxOpenGLPropPixelDepth → 管线 F32;
|
||||
// 调用方已按协商门(render 驱动)决定走 GL)。
|
||||
let gl_pixel_depth =
|
||||
crate::suites::gl_render::pick_gl_pixel_depth(&self.plugin.descriptor.props)
|
||||
.unwrap_or("OfxBitDepthFloat");
|
||||
crate::suites::set_gl_ctx(Some(crate::suites::GlCtx {
|
||||
renderer,
|
||||
output_texture,
|
||||
gl_pixel_depth,
|
||||
}));
|
||||
// GL 模式无 CPU 输出图像(current_output 保持 None)。
|
||||
|
||||
if let Some((cb, userdata)) = self
|
||||
.progress_cb
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone()
|
||||
{
|
||||
crate::suites::progress::set_current(Some(unsafe {
|
||||
crate::progress::ProgressReporter::new(cb, userdata as *mut std::ffi::c_void)
|
||||
}));
|
||||
}
|
||||
|
||||
let inst_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let empty = PropertySet::new();
|
||||
// attach(失败仍继续——插件可忽略;规范允许 ReplyDefault)。
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
.call_action(crate::host::ACTION_GL_CONTEXT_ATTACHED, inst_handle, &empty, &empty)
|
||||
};
|
||||
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
|
||||
crate::suites::set_gl_ctx(None);
|
||||
crate::suites::set_render_ctx(None);
|
||||
crate::suites::progress::set_current(None);
|
||||
return Err(crate::error::Error::Failed(format!(
|
||||
"OpenGLContextAttached 失败:{stat}"
|
||||
)));
|
||||
}
|
||||
|
||||
let out = PropertySet::new();
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
.call_action(ACTION_RENDER, inst_handle, &in_args, &out)
|
||||
};
|
||||
|
||||
// detach(必须与 attach 配对;ofxGPURender.h:345-346)。
|
||||
unsafe {
|
||||
self.plugin
|
||||
.call_action(crate::host::ACTION_GL_CONTEXT_DETACHED, inst_handle, &empty, &empty)
|
||||
};
|
||||
|
||||
// 兜底:插件遗漏 clipFreeTexture 的输入纹理在此释放。
|
||||
crate::suites::gl_render::purge_leftovers();
|
||||
crate::suites::set_gl_ctx(None);
|
||||
crate::suites::set_render_ctx(None);
|
||||
crate::suites::progress::set_current(None);
|
||||
|
||||
if stat != crate::suites::status::OK {
|
||||
return Err(crate::error::Error::Failed(format!("GL render 失败:{stat}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// GetOutputColourspace action(M11 §4;ofxColour.h:243-283):
|
||||
/// 宿主给插件一份偏好色彩空间列表,插件回写输出 clip 的色彩
|
||||
/// 空间(可为 "OfxColourspace_<clip>" 交叉引用)。
|
||||
///
|
||||
/// 返回解析后的输出色彩空间:交叉引用解析为所引 clip 的实际
|
||||
/// 色彩空间(ofxColour.h:142-141 的跨 clip 引用约定);
|
||||
/// REPLY_DEFAULT → 第一个输入 clip 的色彩空间(ofxColour.h:278-279)。
|
||||
/// 调用方(render 驱动)随后把结果写回输出 clip(
|
||||
/// [`Instance::set_output_colourspace`])。
|
||||
pub fn get_output_colourspace(&self, preferred: &[String]) -> crate::error::Result<String> {
|
||||
use crate::property::Value;
|
||||
use crate::host::{ACTION_GET_OUTPUT_COLOURSPACE, PROP_CLIP_COLOURSPACE, PROP_CLIP_PREFERRED_COLOURSPACES};
|
||||
|
||||
let in_args = PropertySet::new();
|
||||
let values: Vec<Value> = preferred
|
||||
.iter()
|
||||
.map(|s| Value::String(CString::new(s.as_str()).unwrap()))
|
||||
.collect();
|
||||
in_args.define(PROP_CLIP_PREFERRED_COLOURSPACES, values);
|
||||
let out = PropertySet::new();
|
||||
out.set_one(PROP_CLIP_COLOURSPACE, Value::String(CString::new("").unwrap()));
|
||||
|
||||
let inst_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
.call_action(ACTION_GET_OUTPUT_COLOURSPACE, inst_handle, &in_args, &out)
|
||||
};
|
||||
if stat == crate::suites::status::OK {
|
||||
let value = out
|
||||
.get(PROP_CLIP_COLOURSPACE, 0)
|
||||
.map(|v| match v {
|
||||
Value::String(s) => s.to_string_lossy().into_owned(),
|
||||
_ => String::new(),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if value.is_empty() {
|
||||
return Err(crate::error::Error::Failed(
|
||||
"GetOutputColourspace 未回写色彩空间".into(),
|
||||
));
|
||||
}
|
||||
Ok(self.resolve_colourspace(value))
|
||||
} else if stat == crate::suites::status::REPLY_DEFAULT {
|
||||
// 插件未实现 → 用第一个输入 clip 的色彩空间(规范默认)。
|
||||
let first_input = self
|
||||
.clips
|
||||
.iter()
|
||||
.find(|c| c.name != "Output")
|
||||
.map(|c| {
|
||||
c.props
|
||||
.get(PROP_CLIP_COLOURSPACE, 0)
|
||||
.map(|v| match v {
|
||||
Value::String(s) => s.to_string_lossy().into_owned(),
|
||||
_ => String::new(),
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Ok(first_input)
|
||||
} else {
|
||||
Err(crate::error::Error::Failed(format!(
|
||||
"GetOutputColourspace 失败:{stat}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// 把输出 clip 的色彩空间写回(GetOutputColourspace 后的落点;
|
||||
/// ofxColour.h:262-264 "the host must set kOfxImageClipPropColourspace
|
||||
/// on the instance's output clip to the value from outArgs")。
|
||||
pub fn set_output_colourspace(&self, colourspace: &str) {
|
||||
use crate::property::Value;
|
||||
if let Some(output) = self.clips.iter().find(|c| c.name == "Output") {
|
||||
output.props.set_one(
|
||||
crate::host::PROP_CLIP_COLOURSPACE,
|
||||
Value::String(CString::new(colourspace).unwrap()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析 "OfxColourspace_<clip>" 交叉引用为所引 clip 的实际色彩
|
||||
/// 空间;非交叉引用原样返回。
|
||||
pub fn resolve_colourspace(&self, value: String) -> String {
|
||||
const PREFIX: &str = "OfxColourspace_";
|
||||
if let Some(clip_name) = value.strip_prefix(PREFIX) {
|
||||
self.clips
|
||||
.iter()
|
||||
.find(|c| c.name == clip_name)
|
||||
.map(|c| {
|
||||
c.props
|
||||
.get(crate::host::PROP_CLIP_COLOURSPACE, 0)
|
||||
.map(|v| match v {
|
||||
crate::property::Value::String(s) => s.to_string_lossy().into_owned(),
|
||||
_ => value.clone(),
|
||||
})
|
||||
.unwrap_or(value.clone())
|
||||
})
|
||||
.unwrap_or(value)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
/// begin/endSequenceRender 配对(帧序列渲染的前后括号;
|
||||
/// HS: ofxhImageEffect.cpp:473-513 的 in args:frameRange +
|
||||
/// frameStep + renderScale + sequentialRenderStatus)。GL 模式
|
||||
/// 变体 [`Instance::begin_sequence_render_gl`] 额外带
|
||||
/// kOfxImageEffectPropOpenGLEnabled(ofxGPURender.h:117)。
|
||||
pub fn begin_sequence_render(&self, range: OfxRangeD) -> crate::error::Result<()> {
|
||||
self.begin_sequence_inner(range, false)
|
||||
}
|
||||
|
||||
/// GL 模式的 beginSequenceRender(in args 带 OpenGLEnabled=1)。
|
||||
pub fn begin_sequence_render_gl(&self, range: OfxRangeD) -> crate::error::Result<()> {
|
||||
self.begin_sequence_inner(range, true)
|
||||
}
|
||||
|
||||
fn begin_sequence_inner(&self, range: OfxRangeD, gl_enabled: bool) -> crate::error::Result<()> {
|
||||
use crate::host::ACTION_BEGIN_SEQUENCE;
|
||||
|
||||
let in_args = Self::sequence_in_args(range, gl_enabled);
|
||||
self.sequence_range
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.replace(range);
|
||||
|
||||
let inst_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let out = PropertySet::new();
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
.call_action(ACTION_BEGIN_SEQUENCE, inst_handle, &in_args, &out)
|
||||
};
|
||||
if stat != crate::suites::status::OK {
|
||||
return Err(crate::error::Error::Failed(format!(
|
||||
"beginSequenceRender 失败:{stat}"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 序列 action 的 in args 组装(begin/end 共用;GL 模式加
|
||||
/// OpenGLEnabled=1)。
|
||||
fn sequence_in_args(range: OfxRangeD, gl_enabled: bool) -> PropertySet {
|
||||
use crate::property::Value;
|
||||
use crate::host::{PROP_FRAME_RANGE, PROP_FRAME_STEP, PROP_RENDER_SCALE, PROP_SEQUENTIAL_RENDER};
|
||||
|
||||
let in_args = PropertySet::new();
|
||||
in_args.define(PROP_FRAME_RANGE, vec![Value::Double(range.min), Value::Double(range.max)]);
|
||||
in_args.set_one(PROP_FRAME_STEP, Value::Double(1.0));
|
||||
in_args.define(PROP_RENDER_SCALE, vec![Value::Double(1.0), Value::Double(1.0)]);
|
||||
in_args.set_one(PROP_SEQUENTIAL_RENDER, Value::Int(1));
|
||||
if gl_enabled {
|
||||
in_args.set_one(crate::host::PROP_GL_ENABLED, Value::Int(1));
|
||||
}
|
||||
in_args
|
||||
}
|
||||
|
||||
/// 见 [`Instance::begin_sequence_render`]。
|
||||
pub fn end_sequence_render(&self, range: OfxRangeD) -> crate::error::Result<()> {
|
||||
self.end_sequence_inner(range, false)
|
||||
}
|
||||
|
||||
/// GL 模式的 endSequenceRender。
|
||||
pub fn end_sequence_render_gl(&self, range: OfxRangeD) -> crate::error::Result<()> {
|
||||
self.end_sequence_inner(range, true)
|
||||
}
|
||||
|
||||
fn end_sequence_inner(&self, range: OfxRangeD, gl_enabled: bool) -> crate::error::Result<()> {
|
||||
use crate::host::ACTION_END_SEQUENCE;
|
||||
|
||||
let in_args = Self::sequence_in_args(range, gl_enabled);
|
||||
|
||||
let inst_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let out = PropertySet::new();
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
.call_action(ACTION_END_SEQUENCE, inst_handle, &in_args, &out)
|
||||
};
|
||||
self.sequence_range
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.take();
|
||||
if stat != crate::suites::status::OK {
|
||||
return Err(crate::error::Error::Failed(format!(
|
||||
"endSequenceRender 失败:{stat}"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 销毁(destroyInstance action)。析构由 RefBox 驱动;
|
||||
/// 此处只做 action 通知,幂等([`Instance::drop`] 的
|
||||
/// `destroyed` 门保证只发一次)。
|
||||
pub(crate) fn notify_destroy(&self) {
|
||||
use crate::host::ACTION_DESTROY_INSTANCE;
|
||||
let inst_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let empty = PropertySet::new();
|
||||
// 通知失败只记日志(销毁路径不可回滚)。
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
.call_action(ACTION_DESTROY_INSTANCE, inst_handle, &empty, &empty)
|
||||
};
|
||||
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
|
||||
eprintln!("destroyInstance 通知失败:{stat}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 从属性集读矩形(Double×4)。
|
||||
fn read_rect(props: &PropertySet, name: &str) -> Option<OfxRectD> {
|
||||
use crate::property::Value;
|
||||
let x1 = match props.get(name, 0)? {
|
||||
Value::Double(d) => d,
|
||||
_ => return None,
|
||||
};
|
||||
let y1 = match props.get(name, 1)? {
|
||||
Value::Double(d) => d,
|
||||
_ => return None,
|
||||
};
|
||||
let x2 = match props.get(name, 2)? {
|
||||
Value::Double(d) => d,
|
||||
_ => return None,
|
||||
};
|
||||
let y2 = match props.get(name, 3)? {
|
||||
Value::Double(d) => d,
|
||||
_ => return None,
|
||||
};
|
||||
Some(OfxRectD { x1, y1, x2, y2 })
|
||||
}
|
||||
|
||||
/// getClipPreferences 的协商结果。
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ClipPreferences {
|
||||
/// 输出分量(kOfxImageComponentRGBA 等)。
|
||||
pub output_components: String,
|
||||
/// 输出位深(kOfxBitDepthFloat 等;全链路 F32 下恒为 float)。
|
||||
pub output_bit_depth: String,
|
||||
/// 像素比。
|
||||
pub pixel_aspect_ratio: f64,
|
||||
/// 帧率。
|
||||
pub frame_rate: f64,
|
||||
/// field 处理模式(kOfxImageField*)。
|
||||
pub field: String,
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! # oakplugin — Oak 的插件模块(自研 OFX 宿主)
|
||||
//!
|
||||
//! 本 crate 是 oakplugin 模块的全部实现(M11):
|
||||
//!
|
||||
//! - **OFX 宿主**:扫描 bundle、加载插件、实现八张 suite
|
||||
//! (property/memory/image_effect/param/message/progress/timeline/
|
||||
//! multithread),驱动 describe/createInstance/render 等 action。
|
||||
//! - **桥**:把 OFX 实例的参数接到 oaknode([`bridge::node`])、把
|
||||
//! clip 的输入输出接到 oakrender 纹理([`bridge::render`])、把
|
||||
//! 参数修改包成 undo 命令([`bridge::undo`])。
|
||||
//! - **C ABI 出口**([`ffi`]):逐字实现 `include/plugin/*.h`。
|
||||
//!
|
||||
//! ## FFI 纪律(全 crate 最高优先级约定)
|
||||
//!
|
||||
//! 1. 每个 `extern "C"` 导出函数体必须包
|
||||
//! [`handle::guard`]/[`handle::guard_ptr`](catch_unwind + 错误码
|
||||
//! 映射)。panic 越过 FFI 边界是 release 阻断级缺陷。
|
||||
//! 2. 句柄一律 [`handle::RefBox`];`ctx` 是不透明指针,含义只在本
|
||||
//! crate 内解释。
|
||||
//! 3. 插件可在任意自起线程回调 suite(multithread suite 存活期
|
||||
//! 内);一切共享状态走 `Mutex`,句柄注册表见 [`handle::Registry`]。
|
||||
//! 4. OFX 语义以 openfx HostSupport 为参照系;协商与时序实现点必须
|
||||
//! 注释对应 HostSupport 文件与行号(格式:`// HS: ofxhImageEffect.cpp:2776`)。
|
||||
//!
|
||||
//! ## 第 1 期范围
|
||||
//!
|
||||
//! filter/generator/transition 上下文;CPU 渲染路径。GL 纹理 suite
|
||||
//! 与 ofxColour 为第 2 期([`clip`]/[`image`] 内以 `// [P2]` 标记
|
||||
//! 预留点)。
|
||||
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod bridge;
|
||||
pub mod clip;
|
||||
pub mod descriptor;
|
||||
pub mod error;
|
||||
pub mod ffi;
|
||||
pub mod handle;
|
||||
pub mod host;
|
||||
pub mod image;
|
||||
pub mod instance;
|
||||
pub mod param;
|
||||
pub mod progress;
|
||||
pub mod property;
|
||||
pub mod render_driver;
|
||||
pub mod suites;
|
||||
@@ -0,0 +1,812 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! 参数体系:12 种参数实例 + param ↔ oaknode 桥。
|
||||
//!
|
||||
//! 对应 C++ 的 `ParamInstance`/`OliveParamInstance`。桥的语义
|
||||
//! (M9 已定):节点输入值变化 → 写回 OFX 参数;OFX 参数被插件
|
||||
//! 改动 → 经 oaknode C ABI 回写节点(undoable 经
|
||||
//! [`crate::bridge::undo`])。
|
||||
//!
|
||||
//! 句柄约定([`crate::suites::tag`]):`ParamDef`/`ParamInstance`
|
||||
//! `#[repr(C)]` 且 `props` 在偏移 0;元素装箱(`Vec<Box<..>>`)保证
|
||||
//! Vec 重分配不移动对象、句柄不悬垂。
|
||||
//!
|
||||
//! `// TODO(bridge)`:`set_from_node`/`notify_instance_changed` 依赖
|
||||
//! bridge::node 的 `Value` 布局(声明尚未冻结),保留 todo!()。
|
||||
//! **已落地(M11 第 1 期)**:Value 布局随 [`crate::bridge::node`]
|
||||
//! 冻结(`include/node/node.h` 的 `oaknode_value` POD),两处 todo
|
||||
//! 已实现。
|
||||
|
||||
use std::ffi::CString;
|
||||
|
||||
use crate::property::{PropertySet, Value};
|
||||
|
||||
// ---- OFX 参数类型字符串(ofxParam.h)----
|
||||
|
||||
/// kOfxParamTypeInteger。
|
||||
pub const TYPE_INTEGER: &str = "OfxParamTypeInteger";
|
||||
/// kOfxParamTypeInteger2D。
|
||||
pub const TYPE_INTEGER2D: &str = "OfxParamTypeInteger2D";
|
||||
/// kOfxParamTypeInteger3D。
|
||||
pub const TYPE_INTEGER3D: &str = "OfxParamTypeInteger3D";
|
||||
/// kOfxParamTypeDouble。
|
||||
pub const TYPE_DOUBLE: &str = "OfxParamTypeDouble";
|
||||
/// kOfxParamTypeDouble2D。
|
||||
pub const TYPE_DOUBLE2D: &str = "OfxParamTypeDouble2D";
|
||||
/// kOfxParamTypeDouble3D。
|
||||
pub const TYPE_DOUBLE3D: &str = "OfxParamTypeDouble3D";
|
||||
/// kOfxParamTypeBoolean。
|
||||
pub const TYPE_BOOLEAN: &str = "OfxParamTypeBoolean";
|
||||
/// kOfxParamTypeChoice。
|
||||
pub const TYPE_CHOICE: &str = "OfxParamTypeChoice";
|
||||
/// kOfxParamTypeString。
|
||||
pub const TYPE_STRING: &str = "OfxParamTypeString";
|
||||
/// kOfxParamTypeStrChoice。
|
||||
pub const TYPE_STRCHOICE: &str = "OfxParamTypeStrChoice";
|
||||
/// kOfxParamTypeRGB。
|
||||
pub const TYPE_RGB: &str = "OfxParamTypeRGB";
|
||||
/// kOfxParamTypeRGBA。
|
||||
pub const TYPE_RGBA: &str = "OfxParamTypeRGBA";
|
||||
/// kOfxParamTypeBytes。
|
||||
pub const TYPE_BYTES: &str = "OfxParamTypeBytes";
|
||||
/// kOfxParamTypeCustom。
|
||||
pub const TYPE_CUSTOM: &str = "OfxParamTypeCustom";
|
||||
/// kOfxParamTypePushButton。
|
||||
pub const TYPE_PUSHBUTTON: &str = "OfxParamTypePushButton";
|
||||
/// kOfxParamTypeGroup。
|
||||
pub const TYPE_GROUP: &str = "OfxParamTypeGroup";
|
||||
/// kOfxParamTypePage。
|
||||
pub const TYPE_PAGE: &str = "OfxParamTypePage";
|
||||
/// kOfxParamTypeParametric(第 1 期不支持,paramDefine 拒绝)。
|
||||
pub const TYPE_PARAMETRIC: &str = "OfxParamTypeParametric";
|
||||
|
||||
// ---- 属性名(ofxParam.h / ofxCore.h)----
|
||||
|
||||
/// kOfxParamPropSecret。
|
||||
pub(crate) const P_SECRET: &str = "OfxParamPropSecret";
|
||||
/// kOfxParamPropHint。
|
||||
pub(crate) const P_HINT: &str = "OfxParamPropHint";
|
||||
/// kOfxParamPropScriptName。
|
||||
pub(crate) const P_SCRIPT_NAME: &str = "OfxParamPropScriptName";
|
||||
/// kOfxParamPropParent。
|
||||
pub(crate) const P_PARENT: &str = "OfxParamPropParent";
|
||||
/// kOfxParamPropEnabled。
|
||||
pub(crate) const P_ENABLED: &str = "OfxParamPropEnabled";
|
||||
/// kOfxParamPropDataPtr。
|
||||
pub(crate) const P_DATA_PTR: &str = "OfxParamPropDataPtr";
|
||||
/// kOfxParamPropType。
|
||||
pub(crate) const P_TYPE: &str = "OfxParamPropType";
|
||||
/// kOfxParamPropIsAnimating。
|
||||
pub(crate) const P_IS_ANIMATING: &str = "OfxParamPropIsAnimating";
|
||||
/// kOfxParamPropIsAutoKeying。
|
||||
pub(crate) const P_IS_AUTO_KEYING: &str = "OfxParamPropIsAutoKeying";
|
||||
/// kOfxParamPropPersistant。
|
||||
pub(crate) const P_PERSISTANT: &str = "OfxParamPropPersistant";
|
||||
/// kOfxParamPropEvaluateOnChange。
|
||||
pub(crate) const P_EVALUATE_ON_CHANGE: &str = "OfxParamPropEvaluateOnChange";
|
||||
/// kOfxParamPropCanUndo。
|
||||
pub(crate) const P_CAN_UNDO: &str = "OfxParamPropCanUndo";
|
||||
/// kOfxParamPropCacheInvalidation。
|
||||
pub(crate) const P_CACHE_INVALIDATION: &str = "OfxParamPropCacheInvalidation";
|
||||
/// kOfxParamInvalidateValueChange(P_CACHE_INVALIDATION 的默认值)。
|
||||
pub(crate) const V_INVALIDATE_VALUE_CHANGE: &str = "OfxParamInvalidateValueChange";
|
||||
/// kOfxParamPropAnimates。
|
||||
pub(crate) const P_ANIMATES: &str = "OfxParamPropAnimates";
|
||||
/// kOfxParamPropDefault。
|
||||
pub(crate) const P_DEFAULT: &str = "OfxParamPropDefault";
|
||||
/// kOfxParamPropDisplayMin。
|
||||
pub(crate) const P_DISPLAY_MIN: &str = "OfxParamPropDisplayMin";
|
||||
/// kOfxParamPropDisplayMax。
|
||||
pub(crate) const P_DISPLAY_MAX: &str = "OfxParamPropDisplayMax";
|
||||
/// kOfxParamPropMin。
|
||||
pub(crate) const P_MIN: &str = "OfxParamPropMin";
|
||||
/// kOfxParamPropMax。
|
||||
pub(crate) const P_MAX: &str = "OfxParamPropMax";
|
||||
/// kOfxParamPropIncrement。
|
||||
pub(crate) const P_INCREMENT: &str = "OfxParamPropIncrement";
|
||||
/// kOfxParamPropDigits。
|
||||
pub(crate) const P_DIGITS: &str = "OfxParamPropDigits";
|
||||
/// kOfxParamPropDoubleType。
|
||||
pub(crate) const P_DOUBLE_TYPE: &str = "OfxParamPropDoubleType";
|
||||
/// kOfxParamDoubleTypePlain。
|
||||
pub(crate) const V_DOUBLE_TYPE_PLAIN: &str = "OfxParamDoubleTypePlain";
|
||||
/// kOfxParamPropDefaultCoordinateSystem。
|
||||
pub(crate) const P_DEFAULT_COORD_SYS: &str = "OfxParamPropDefaultCoordinateSystem";
|
||||
/// kOfxParamCoordinatesCanonical。
|
||||
pub(crate) const V_COORD_CANONICAL: &str = "OfxParamCoordinatesCanonical";
|
||||
/// kOfxParamPropShowTimeMarker。
|
||||
pub(crate) const P_SHOW_TIME_MARKER: &str = "OfxParamPropShowTimeMarker";
|
||||
/// kOfxParamPropDimensionLabel。
|
||||
pub(crate) const P_DIMENSION_LABEL: &str = "OfxParamPropDimensionLabel";
|
||||
/// kOfxParamPropStringMode。
|
||||
pub(crate) const P_STRING_MODE: &str = "OfxParamPropStringMode";
|
||||
/// kOfxParamStringIsSingleLine。
|
||||
pub(crate) const V_STRING_SINGLE_LINE: &str = "OfxParamStringIsSingleLine";
|
||||
/// kOfxParamPropStringFilePathExists。
|
||||
pub(crate) const P_STRING_FILE_EXISTS: &str = "OfxParamPropStringFilePathExists";
|
||||
/// kOfxParamPropChoiceOption。
|
||||
pub(crate) const P_CHOICE_OPTION: &str = "OfxParamPropChoiceOption";
|
||||
/// kOfxParamPropCustomInterpCallbackV1。
|
||||
pub(crate) const P_CUSTOM_INTERP: &str = "OfxParamPropCustomCallbackV1";
|
||||
/// kOfxParamPropPageChild。
|
||||
pub(crate) const P_PAGE_CHILD: &str = "OfxParamPropPageChild";
|
||||
/// kOfxParamPropGroupOpen。
|
||||
pub(crate) const P_GROUP_OPEN: &str = "OfxParamPropGroupOpen";
|
||||
/// kOfxParamPropInteractV1。
|
||||
pub(crate) const P_INTERACT_V1: &str = "OfxParamPropInteractV1";
|
||||
/// kOfxParamPropInteractSize。
|
||||
pub(crate) const P_INTERACT_SIZE: &str = "OfxParamPropInteractSize";
|
||||
/// kOfxParamPropInteractSizeAspect。
|
||||
pub(crate) const P_INTERACT_ASPECT: &str = "OfxParamPropInteractSizeAspect";
|
||||
/// kOfxParamPropInteractMinimumSize。
|
||||
pub(crate) const P_INTERACT_MIN_SIZE: &str = "OfxParamPropInteractMinimumSize";
|
||||
/// kOfxParamPropInteractPreferedSize。
|
||||
pub(crate) const P_INTERACT_PREF_SIZE: &str = "OfxParamPropInteractPreferedSize";
|
||||
/// kOfxPropType。
|
||||
pub(crate) const PROP_TYPE: &str = "OfxPropType";
|
||||
/// kOfxPropName。
|
||||
pub(crate) const PROP_NAME: &str = "OfxPropName";
|
||||
/// kOfxPropLabel。
|
||||
pub(crate) const PROP_LABEL: &str = "OfxPropLabel";
|
||||
/// kOfxPropShortLabel。
|
||||
pub(crate) const PROP_SHORT_LABEL: &str = "OfxPropShortLabel";
|
||||
/// kOfxPropLongLabel。
|
||||
pub(crate) const PROP_LONG_LABEL: &str = "OfxPropLongLabel";
|
||||
/// kOfxPropIcon。
|
||||
pub(crate) const PROP_ICON: &str = "OfxPropIcon";
|
||||
/// kOfxTypeParameter。
|
||||
pub(crate) const TYPE_PARAMETER: &str = "OfxTypeParameter";
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
// 静态 ASCII 常量,无内嵌 NUL。
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
/// 参数值类别(param shim 的变长参数分发;与
|
||||
/// cbits/ofx_param_shim.c 的 KIND_* 枚举逐字对应)。
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ParamKind {
|
||||
/// Integer。
|
||||
Int = 1,
|
||||
/// Integer2D。
|
||||
Int2 = 2,
|
||||
/// Integer3D。
|
||||
Int3 = 3,
|
||||
/// Double。
|
||||
Double = 4,
|
||||
/// Double2D。
|
||||
Double2 = 5,
|
||||
/// Double3D。
|
||||
Double3 = 6,
|
||||
/// Boolean。
|
||||
Bool = 7,
|
||||
/// Choice。
|
||||
Choice = 8,
|
||||
/// RGB。
|
||||
Rgb = 9,
|
||||
/// RGBA。
|
||||
Rgba = 10,
|
||||
/// String。
|
||||
Str = 11,
|
||||
/// StrChoice。
|
||||
StrChoice = 12,
|
||||
}
|
||||
|
||||
/// OFX 类型字符串 → 值类别 + 维度(HS: ofxhParam.cpp `findType`)。
|
||||
/// 无值类(PushButton/Group/Page/Unknown)返回 None。
|
||||
pub(crate) fn kind_of_type(ofx_type: &str) -> Option<(ParamKind, usize)> {
|
||||
match ofx_type {
|
||||
TYPE_INTEGER => Some((ParamKind::Int, 1)),
|
||||
TYPE_INTEGER2D => Some((ParamKind::Int2, 2)),
|
||||
TYPE_INTEGER3D => Some((ParamKind::Int3, 3)),
|
||||
TYPE_DOUBLE => Some((ParamKind::Double, 1)),
|
||||
TYPE_DOUBLE2D => Some((ParamKind::Double2, 2)),
|
||||
TYPE_DOUBLE3D => Some((ParamKind::Double3, 3)),
|
||||
TYPE_BOOLEAN => Some((ParamKind::Bool, 1)),
|
||||
TYPE_CHOICE => Some((ParamKind::Choice, 1)),
|
||||
TYPE_STRING => Some((ParamKind::Str, 1)),
|
||||
TYPE_STRCHOICE => Some((ParamKind::StrChoice, 1)),
|
||||
TYPE_RGB => Some((ParamKind::Rgb, 3)),
|
||||
TYPE_RGBA => Some((ParamKind::Rgba, 4)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 数值类参数(Min/Max/DisplayMin/Max 表格适用;HS
|
||||
/// `addNumericParamProps` 的 isDoubleParam || isIntParam ||
|
||||
/// isColourParam)。
|
||||
fn is_numeric_type(ofx_type: &str) -> bool {
|
||||
matches!(
|
||||
ofx_type,
|
||||
TYPE_INTEGER
|
||||
| TYPE_INTEGER2D
|
||||
| TYPE_INTEGER3D
|
||||
| TYPE_DOUBLE
|
||||
| TYPE_DOUBLE2D
|
||||
| TYPE_DOUBLE3D
|
||||
| TYPE_RGB
|
||||
| TYPE_RGBA
|
||||
)
|
||||
}
|
||||
|
||||
/// 该类型的默认 ParamValue(describe 期基线;HS 的属性默认在 props
|
||||
/// 的 kOfxParamPropDefault,值语义等价)。
|
||||
fn type_default(ofx_type: &str) -> ParamValue {
|
||||
match kind_of_type(ofx_type) {
|
||||
Some((ParamKind::Int, d)) | Some((ParamKind::Int2, d)) | Some((ParamKind::Int3, d)) => {
|
||||
ParamValue::Int([0; 3], d)
|
||||
}
|
||||
Some((ParamKind::Double, d))
|
||||
| Some((ParamKind::Double2, d))
|
||||
| Some((ParamKind::Double3, d)) => ParamValue::Double([0.0; 3], d),
|
||||
Some((ParamKind::Rgb, d)) | Some((ParamKind::Rgba, d)) => ParamValue::Color([0.0; 4], d),
|
||||
Some((ParamKind::Bool, _)) => ParamValue::Bool(false),
|
||||
Some((ParamKind::Choice, _)) => ParamValue::Choice(0),
|
||||
Some((ParamKind::Str, _)) => ParamValue::String(cs("")),
|
||||
Some((ParamKind::StrChoice, _)) => ParamValue::StrChoice(cs("")),
|
||||
None => match ofx_type {
|
||||
TYPE_BYTES | TYPE_CUSTOM => ParamValue::Bytes(Vec::new()),
|
||||
TYPE_PUSHBUTTON => ParamValue::PushButton,
|
||||
TYPE_GROUP | TYPE_PAGE => ParamValue::Container,
|
||||
_ => ParamValue::Container, // 未知类型占位(paramDefine 已拒绝)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 数值属性表的值(Min/Max/Display 族;HS ofxhParam.cpp:420-449)。
|
||||
/// `is_max` 选 ±f64::MAX / ±i32::MAX;colour 参数恒 0/1。
|
||||
fn numeric_value(ofx_type: &str, kind: ParamKind, dim: usize, is_max: bool) -> Vec<Value> {
|
||||
if ofx_type == TYPE_RGB || ofx_type == TYPE_RGBA {
|
||||
// colour:display 范围 0..1,min/max 同。
|
||||
let v = Value::Double(if is_max { 1.0 } else { 0.0 });
|
||||
return vec![v; dim];
|
||||
}
|
||||
match kind {
|
||||
ParamKind::Double | ParamKind::Double2 | ParamKind::Double3 => {
|
||||
let v = Value::Double(if is_max { f64::MAX } else { f64::MIN });
|
||||
vec![v; dim]
|
||||
}
|
||||
_ => {
|
||||
let v = Value::Int(if is_max { i32::MAX } else { i32::MIN });
|
||||
vec![v; dim]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 参数值(与 OFX 参数类型一一对应)。
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ParamValue {
|
||||
/// kOfxParamTypeInteger / Integer2D / Integer3D。
|
||||
Int([i32; 3], usize),
|
||||
/// kOfxParamTypeDouble / Double2D / Double3D。
|
||||
Double([f64; 3], usize),
|
||||
/// kOfxParamTypeBoolean。
|
||||
Bool(bool),
|
||||
/// kOfxParamTypeChoice(选项索引)。
|
||||
Choice(i32),
|
||||
/// kOfxParamTypeString。
|
||||
String(std::ffi::CString),
|
||||
/// kOfxParamTypeRGB / RGBA。
|
||||
Color([f64; 4], usize),
|
||||
/// kOfxParamTypeStrChoice。
|
||||
StrChoice(std::ffi::CString),
|
||||
/// kOfxParamTypeBytes / Custom(不透明字节串)。
|
||||
Bytes(Vec<u8>),
|
||||
/// kOfxParamTypePushButton(无值,仅触发)。
|
||||
PushButton,
|
||||
/// kOfxParamTypeGroup / Page(容器,无值)。
|
||||
Container,
|
||||
}
|
||||
|
||||
/// 参数定义(describe 产物,见 [`crate::descriptor::EffectDescriptor`])。
|
||||
/// `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`])。
|
||||
#[derive(Clone)]
|
||||
#[repr(C)]
|
||||
pub struct ParamDef {
|
||||
/// 定义属性(label/hint/parent/coordinate-system/secret/
|
||||
/// display min-max/choice 选项与排序等;describe 期预置
|
||||
/// HostSupport 同款表格)。
|
||||
pub props: PropertySet,
|
||||
/// 参数名(OFX 标识)。
|
||||
pub name: String,
|
||||
/// OFX 类型字符串(kOfxParamType*)。
|
||||
pub ofx_type: String,
|
||||
/// 默认值(describe 后读取)。
|
||||
pub default: ParamValue,
|
||||
}
|
||||
|
||||
impl ParamDef {
|
||||
/// 按 OFX 类型构建定义(镜像 HS ofxhParam.cpp:224-352:
|
||||
/// universalProps + addStandardParamProps 的分类型属性表)。
|
||||
pub(crate) fn new(name: &str, ofx_type: &str) -> Self {
|
||||
let mut props = PropertySet::new();
|
||||
let uname = name.to_string();
|
||||
let utype = ofx_type.to_string();
|
||||
|
||||
// universalProps(HS ofxhParam.cpp:229-246)。
|
||||
props.set_one(PROP_TYPE, Value::String(cs(TYPE_PARAMETER)));
|
||||
props.set_one(P_SECRET, Value::Int(0));
|
||||
props.set_one(P_HINT, Value::String(cs("")));
|
||||
props.set_one(P_SCRIPT_NAME, Value::String(cs(&uname)));
|
||||
props.set_one(P_PARENT, Value::String(cs("")));
|
||||
props.set_one(P_ENABLED, Value::Int(1));
|
||||
props.set_one(P_DATA_PTR, Value::Pointer(std::ptr::null_mut()));
|
||||
props.set_one(P_TYPE, Value::String(cs(&utype)));
|
||||
props.set_one(PROP_NAME, Value::String(cs(&uname)));
|
||||
props.set_one(PROP_LABEL, Value::String(cs(&uname)));
|
||||
props.set_one(PROP_SHORT_LABEL, Value::String(cs(&uname)));
|
||||
props.set_one(PROP_LONG_LABEL, Value::String(cs(&uname)));
|
||||
props.define(PROP_ICON, vec![Value::String(cs("")), Value::String(cs(""))]);
|
||||
|
||||
// 值类参数(HS addValueParamProps,ofxhParam.cpp:352-380)。
|
||||
if let Some((kind, dim)) = kind_of_type(ofx_type) {
|
||||
props.set_one(P_IS_ANIMATING, Value::Int(0));
|
||||
props.set_one(P_IS_AUTO_KEYING, Value::Int(0));
|
||||
props.set_one(P_PERSISTANT, Value::Int(1));
|
||||
props.set_one(P_EVALUATE_ON_CHANGE, Value::Int(1));
|
||||
props.set_one(P_CAN_UNDO, Value::Int(1));
|
||||
props.set_one(P_CACHE_INVALIDATION, Value::String(cs(V_INVALIDATE_VALUE_CHANGE)));
|
||||
// 可动画性(HS ofxhParam.cpp:367-372:custom/string/
|
||||
// boolean/choice 默认不可动画)。
|
||||
let animates = ofx_type != TYPE_CUSTOM
|
||||
&& ofx_type != TYPE_STRING
|
||||
&& ofx_type != TYPE_BOOLEAN
|
||||
&& ofx_type != TYPE_CHOICE;
|
||||
props.set_one(P_ANIMATES, Value::Int(animates as i32));
|
||||
props.define(P_DEFAULT, default_values(kind, dim));
|
||||
|
||||
// 数值类(HS addNumericParamProps,ofxhParam.cpp:391-455)。
|
||||
if is_numeric_type(ofx_type) {
|
||||
props.define(P_DISPLAY_MIN, numeric_value(ofx_type, kind, dim, false));
|
||||
props.define(P_DISPLAY_MAX, numeric_value(ofx_type, kind, dim, true));
|
||||
props.define(P_MIN, numeric_value(ofx_type, kind, dim, false));
|
||||
props.define(P_MAX, numeric_value(ofx_type, kind, dim, true));
|
||||
if matches!(kind, ParamKind::Double | ParamKind::Double2 | ParamKind::Double3) {
|
||||
props.set_one(P_INCREMENT, Value::Double(1.0));
|
||||
props.set_one(P_DIGITS, Value::Int(2));
|
||||
}
|
||||
if ofx_type == TYPE_DOUBLE || ofx_type == TYPE_DOUBLE2D || ofx_type == TYPE_DOUBLE3D {
|
||||
props.set_one(P_DOUBLE_TYPE, Value::String(cs(V_DOUBLE_TYPE_PLAIN)));
|
||||
props.set_one(P_DEFAULT_COORD_SYS, Value::String(cs(V_COORD_CANONICAL)));
|
||||
if dim == 1 {
|
||||
props.set_one(P_SHOW_TIME_MARKER, Value::Int(0));
|
||||
}
|
||||
}
|
||||
if dim == 2 || dim == 3 {
|
||||
let labels: Vec<Value> = ["x", "y", "z"][..dim]
|
||||
.iter()
|
||||
.map(|l| Value::String(cs(l)))
|
||||
.collect();
|
||||
props.define(P_DIMENSION_LABEL, labels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分类型附加(HS addStandardParamProps,ofxhParam.cpp:248-301)。
|
||||
match ofx_type {
|
||||
TYPE_STRING => {
|
||||
props.set_one(P_STRING_MODE, Value::String(cs(V_STRING_SINGLE_LINE)));
|
||||
props.set_one(P_STRING_FILE_EXISTS, Value::Int(1));
|
||||
}
|
||||
TYPE_CHOICE => {
|
||||
// 维度 0:选项数由插件 SetN 决定。
|
||||
props.define(P_CHOICE_OPTION, vec![]);
|
||||
}
|
||||
TYPE_CUSTOM => {
|
||||
props.set_one(P_CUSTOM_INTERP, Value::Pointer(std::ptr::null_mut()));
|
||||
}
|
||||
TYPE_PAGE => {
|
||||
props.define(P_PAGE_CHILD, vec![]);
|
||||
}
|
||||
TYPE_GROUP => {
|
||||
props.set_one(P_GROUP_OPEN, Value::Int(1));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// 交互属性(HS addInteractParamProps,ofxhParam.cpp:305-317;
|
||||
// group/page 除外)。
|
||||
if ofx_type != TYPE_GROUP && ofx_type != TYPE_PAGE {
|
||||
props.set_one(P_INTERACT_V1, Value::Pointer(std::ptr::null_mut()));
|
||||
props.define(P_INTERACT_SIZE, vec![Value::Double(0.0), Value::Double(0.0)]);
|
||||
props.set_one(P_INTERACT_ASPECT, Value::Double(1.0));
|
||||
props.define(
|
||||
P_INTERACT_MIN_SIZE,
|
||||
vec![Value::Double(10.0), Value::Double(10.0)],
|
||||
);
|
||||
props.define(
|
||||
P_INTERACT_PREF_SIZE,
|
||||
vec![Value::Int(10), Value::Int(10)],
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
props,
|
||||
name: uname,
|
||||
ofx_type: utype,
|
||||
default: type_default(ofx_type),
|
||||
}
|
||||
}
|
||||
|
||||
/// 值类别(变长参数分发用;无值类返回 None)。
|
||||
pub(crate) fn kind(&self) -> Option<ParamKind> {
|
||||
kind_of_type(&self.ofx_type).map(|(k, _)| k)
|
||||
}
|
||||
}
|
||||
|
||||
/// kOfxParamPropDefault 的属性值(HS ofxhParam.cpp:376-377)。
|
||||
fn default_values(kind: ParamKind, dim: usize) -> Vec<Value> {
|
||||
match kind {
|
||||
ParamKind::Int | ParamKind::Int2 | ParamKind::Int3 | ParamKind::Bool | ParamKind::Choice => {
|
||||
vec![Value::Int(0); dim.max(1)]
|
||||
}
|
||||
ParamKind::Double
|
||||
| ParamKind::Double2
|
||||
| ParamKind::Double3
|
||||
| ParamKind::Rgb
|
||||
| ParamKind::Rgba => vec![Value::Double(0.0); dim.max(1)],
|
||||
ParamKind::Str | ParamKind::StrChoice => vec![Value::String(cs(""))],
|
||||
}
|
||||
}
|
||||
|
||||
/// 参数实例(createInstance 后由 [`ParamDef`] 实例化)。
|
||||
/// `#[repr(C)]` + props 在偏移 0(句柄约定)。
|
||||
#[repr(C)]
|
||||
pub struct ParamInstance {
|
||||
/// 实例级属性(当前值镜像等)。
|
||||
pub props: PropertySet,
|
||||
/// 对应定义。
|
||||
pub def: ParamDef,
|
||||
/// 当前值(节点桥与 OFX 侧的同步点)。
|
||||
value: std::sync::Mutex<ParamValue>,
|
||||
}
|
||||
|
||||
impl ParamInstance {
|
||||
/// 从定义实例化(createInstance 路径调用)。实例 props 是定义
|
||||
/// props 的深拷贝(HS: SetInstance::makeParam 的 descriptor 属性
|
||||
/// 复制语义——插件在实例期读 label/min/max 等)。
|
||||
pub fn from_def(def: ParamDef) -> Self {
|
||||
let value = def.default.clone();
|
||||
let props = def.props.clone();
|
||||
Self {
|
||||
props,
|
||||
def,
|
||||
value: std::sync::Mutex::new(value),
|
||||
}
|
||||
}
|
||||
|
||||
/// 读当前值。
|
||||
pub fn get(&self) -> ParamValue {
|
||||
self.value.lock().unwrap_or_else(|e| e.into_inner()).clone()
|
||||
}
|
||||
|
||||
/// 写当前值(OFX 语义:paramSetValue action 语义,不打节点桥)。
|
||||
pub fn set_ofx(&self, value: ParamValue) {
|
||||
*self.value.lock().unwrap_or_else(|e| e.into_inner()) = value;
|
||||
}
|
||||
|
||||
/// 字符串值的内驻指针(param suite 的 get_string 用;指向实例
|
||||
/// 存储,下次 set 前有效——克隆的 CString 指针会悬垂,不能给)。
|
||||
pub(crate) fn string_ptr(&self) -> Option<*const std::ffi::c_char> {
|
||||
let v = self.value.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match &*v {
|
||||
ParamValue::String(s) | ParamValue::StrChoice(s) => Some(s.as_ptr()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 oaknode 输入值写入(节点→插件方向)。类型映射表逐行对照
|
||||
/// C++ 的 `valueconvert`/`paraminstance.h`(`node_get` 的镜像):
|
||||
/// INT→Integer、FLOAT→Double、BOOL→Boolean、COMBO→Choice、
|
||||
/// COLOR→RGB/RGBA、VEC2/VEC3→Double(2D/3D)/Integer(2D/3D)。
|
||||
/// 维度不齐按 OFX 语义截断/补零(缺失元素按 0)。字符串族
|
||||
/// (OAKNODE_VALUE_STRING)的 POD 不携带数据——此路径不改值
|
||||
/// (走 facade 的字符串 API,见 `include/plugin/instance.h`)。
|
||||
/// 类型不匹配 → 忽略(保持现值;C++ `node_get` 失败时参数不回写)。
|
||||
pub fn set_from_node(&self, node_value: &crate::bridge::node::Value) {
|
||||
use crate::bridge::node::node_value_type as T;
|
||||
let v = node_value;
|
||||
let mapped: Option<ParamValue> = match self.def.ofx_type.as_str() {
|
||||
TYPE_DOUBLE => (v.r#type == T::FLOAT).then(|| ParamValue::Double([v.f[0], 0.0, 0.0], 1)),
|
||||
TYPE_DOUBLE2D => {
|
||||
(v.r#type == T::VEC2).then(|| ParamValue::Double([v.f[0], v.f[1], 0.0], 2))
|
||||
}
|
||||
TYPE_DOUBLE3D => (v.r#type == T::VEC3)
|
||||
.then(|| ParamValue::Double([v.f[0], v.f[1], v.f[2]], 3)),
|
||||
TYPE_INTEGER => (v.r#type == T::INT).then(|| ParamValue::Int([v.num as i32, 0, 0], 1)),
|
||||
TYPE_INTEGER2D | TYPE_INTEGER3D => {
|
||||
let dim = if self.def.ofx_type == TYPE_INTEGER2D { 2 } else { 3 };
|
||||
Some(ParamValue::Int(
|
||||
[v.f[0] as i32, v.f[1] as i32, v.f[2] as i32],
|
||||
dim,
|
||||
))
|
||||
}
|
||||
TYPE_BOOLEAN => (v.r#type == T::BOOL).then(|| ParamValue::Bool(v.num != 0)),
|
||||
TYPE_CHOICE => (v.r#type == T::COMBO).then(|| ParamValue::Choice(v.num as i32)),
|
||||
TYPE_RGB => (v.r#type == T::COLOR)
|
||||
.then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], 0.0], 3)),
|
||||
TYPE_RGBA => (v.r#type == T::COLOR)
|
||||
.then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], v.f[3]], 4)),
|
||||
_ => None, // 字符串/无值类:POD 无数据,不改值
|
||||
};
|
||||
if let Some(pv) = mapped {
|
||||
self.set_ofx(pv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ParamValue → [`crate::bridge::node::Value`](插件→节点方向;
|
||||
/// 字符串族经 [`crate::bridge::node::set_input_string_undoable`])。
|
||||
/// 镜像 C++ `paraminstance.h` 的 `value_int`/`value_double`/
|
||||
/// `value_vec`/`value_color` 构造:RGB 颜色补 alpha=1(C++
|
||||
/// `RGBInstance::set` 的 `value_color(r,g,b,1.0)`)。无值类与 Bytes
|
||||
/// 无节点对应 → None。
|
||||
pub(crate) fn to_node_value(v: &ParamValue) -> Option<crate::bridge::node::Value> {
|
||||
use crate::bridge::node::node_value_type as T;
|
||||
let mut out = crate::bridge::node::Value::default();
|
||||
match v {
|
||||
ParamValue::Double(d, 1) => {
|
||||
out.r#type = T::FLOAT;
|
||||
out.f = [d[0], 0.0, 0.0, 0.0];
|
||||
}
|
||||
ParamValue::Double(d, 2) => {
|
||||
out.r#type = T::VEC2;
|
||||
out.f = [d[0], d[1], 0.0, 0.0];
|
||||
}
|
||||
ParamValue::Double(d, 3) => {
|
||||
out.r#type = T::VEC3;
|
||||
out.f = [d[0], d[1], d[2], 0.0];
|
||||
}
|
||||
ParamValue::Double(d, _) => {
|
||||
out.r#type = T::FLOAT;
|
||||
out.f = [d[0], 0.0, 0.0, 0.0];
|
||||
}
|
||||
ParamValue::Int(a, 1) => {
|
||||
out.r#type = T::INT;
|
||||
out.num = a[0] as i64;
|
||||
}
|
||||
ParamValue::Int(a, 2) => {
|
||||
out.r#type = T::VEC2;
|
||||
out.f = [a[0] as f64, a[1] as f64, 0.0, 0.0];
|
||||
}
|
||||
ParamValue::Int(a, 3) => {
|
||||
out.r#type = T::VEC3;
|
||||
out.f = [a[0] as f64, a[1] as f64, a[2] as f64, 0.0];
|
||||
}
|
||||
ParamValue::Bool(b) => {
|
||||
out.r#type = T::BOOL;
|
||||
out.num = *b as i64;
|
||||
}
|
||||
ParamValue::Choice(c) => {
|
||||
out.r#type = T::COMBO;
|
||||
out.num = *c as i64;
|
||||
}
|
||||
ParamValue::Color(c, 3) => {
|
||||
out.r#type = T::COLOR;
|
||||
out.f = [c[0], c[1], c[2], 1.0];
|
||||
}
|
||||
ParamValue::Color(c, 4) => {
|
||||
out.r#type = T::COLOR;
|
||||
out.f = *c;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// 参数实例集(实例级)。
|
||||
pub struct ParamSetInstance {
|
||||
/// 全部参数(定义顺序稳定;Box 保证句柄地址稳定)。
|
||||
pub params: Vec<Box<ParamInstance>>,
|
||||
}
|
||||
|
||||
impl ParamSetInstance {
|
||||
/// 按名查找。
|
||||
pub fn find(&self, name: &str) -> Option<&ParamInstance> {
|
||||
self.params.iter().find(|p| p.def.name == name).map(|b| b.as_ref())
|
||||
}
|
||||
|
||||
/// 快照(内省 C ABI 与快照测试用)。
|
||||
///
|
||||
/// 声明原为 `Vec<(&str, &ParamValue)>`:值在 Mutex 内,无法返回
|
||||
/// 活引用——改为拥有型 `(String, ParamValue)`(内省语义不变)。
|
||||
pub fn snapshot(&self) -> Vec<(String, ParamValue)> {
|
||||
self.params
|
||||
.iter()
|
||||
.map(|p| (p.def.name.clone(), p.get()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件 → 节点方向的回写入口(instanceChanged action 触发)。
|
||||
/// 经 [`crate::bridge::node`] 定位绑定节点(身份注册表),再经
|
||||
/// [`crate::bridge::undo`] 包成 undoable 修改。未绑定节点时 no-op。
|
||||
///
|
||||
/// 语义对照 C++ `paraminstance.h` 的 `set()` 路径(`detail::node_set`/
|
||||
/// `node_set_at`):
|
||||
/// - 只回写 [`ChangeReason::PluginEdited`](插件自改)。UserEdited/
|
||||
/// TimeChanged 是宿主侧变更,值已由 [`ParamInstance::set_from_node`]
|
||||
/// 同步,不重复写回;
|
||||
/// - 字符串族经 `oaknode_node_set_input_string_undoable`
|
||||
/// (POD 不携带字符串数据);
|
||||
/// - 无值类(PushButton/Group/Page)与 Bytes 无节点对应 → no-op;
|
||||
/// - 编辑事务内([`crate::instance::Instance::in_edit`])并入 multi
|
||||
/// 命令,否则单命令立即 redo 生效(C++ `submit_undo_command`)。
|
||||
pub(crate) fn notify_instance_changed(
|
||||
instance: &crate::instance::Instance,
|
||||
param_name: &str,
|
||||
reason: ChangeReason,
|
||||
) {
|
||||
use crate::bridge::{node, undo};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
if !matches!(reason, ChangeReason::PluginEdited) {
|
||||
return;
|
||||
}
|
||||
// 未绑定节点 → no-op(C++:!node_.ctx 时 set 只写本地值)。
|
||||
let node_id = instance.node_identity.load(Ordering::Relaxed);
|
||||
if node_id == 0 {
|
||||
return;
|
||||
}
|
||||
// 身份查无(注册表无此项 / 桥符号缺失)→ no-op。
|
||||
let node_handle = unsafe { node::node_from_identity(node_id) };
|
||||
if node_handle.is_null() {
|
||||
return;
|
||||
}
|
||||
let Some(param) = instance.params.find(param_name) else {
|
||||
return;
|
||||
};
|
||||
let value = param.get();
|
||||
let label = format!("Change {param_name}");
|
||||
let Some(cname) = CString::new(param_name).ok() else {
|
||||
return;
|
||||
};
|
||||
match to_node_value(&value) {
|
||||
Some(nv) => {
|
||||
let mut cmd = undo::CommandHandle::null();
|
||||
let r =
|
||||
unsafe { node::set_input_undoable(node_handle, cname.as_ptr(), &nv, &mut cmd) };
|
||||
if r == 0 && !cmd.is_null() {
|
||||
instance.submit_undo_command(cmd, &label);
|
||||
}
|
||||
}
|
||||
None => match &value {
|
||||
ParamValue::String(s) | ParamValue::StrChoice(s) => {
|
||||
let Some(cval) = CString::new(s.to_bytes()).ok() else {
|
||||
return;
|
||||
};
|
||||
let mut cmd = undo::CommandHandle::null();
|
||||
let r = unsafe {
|
||||
node::set_input_string_undoable(
|
||||
node_handle,
|
||||
cname.as_ptr(),
|
||||
cval.as_ptr(),
|
||||
&mut cmd,
|
||||
)
|
||||
};
|
||||
if r == 0 && !cmd.is_null() {
|
||||
instance.submit_undo_command(cmd, &label);
|
||||
}
|
||||
}
|
||||
_ => {} // 无值类 / Bytes:无节点对应
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// instanceChanged 的原因(OFX kOfxChange*)。
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ChangeReason {
|
||||
/// 用户编辑。
|
||||
UserEdited,
|
||||
/// 插件自改。
|
||||
PluginEdited,
|
||||
/// 时间变化。
|
||||
TimeChanged,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bridge::node::node_value_type as T;
|
||||
|
||||
fn param(ofx_type: &str) -> ParamInstance {
|
||||
ParamInstance::from_def(ParamDef::new("p", ofx_type))
|
||||
}
|
||||
|
||||
/// ParamValue → oaknode Value 的类型映射表(镜像 C++
|
||||
/// paraminstance.h 的 value_* 构造)。
|
||||
#[test]
|
||||
fn to_node_value_mapping() {
|
||||
use crate::bridge::node::Value;
|
||||
let c = |t: i32| Value {
|
||||
r#type: t,
|
||||
num: 0,
|
||||
den: 0,
|
||||
f: [0.0; 4],
|
||||
};
|
||||
// Double 族。
|
||||
let v = to_node_value(&ParamValue::Double([1.5, 0.0, 0.0], 1)).unwrap();
|
||||
assert_eq!(v, Value::float(1.5));
|
||||
let v = to_node_value(&ParamValue::Double([1.0, 2.0, 0.0], 2)).unwrap();
|
||||
assert_eq!(v, Value::vec(&[1.0, 2.0]));
|
||||
let v = to_node_value(&ParamValue::Double([1.0, 2.0, 3.0], 3)).unwrap();
|
||||
assert_eq!(v, Value::vec(&[1.0, 2.0, 3.0]));
|
||||
// Int 族(2D/3D 升格为 VEC2/VEC3 浮点载荷)。
|
||||
let v = to_node_value(&ParamValue::Int([7, 0, 0], 1)).unwrap();
|
||||
assert_eq!(v, Value::int(7));
|
||||
let v = to_node_value(&ParamValue::Int([1, 2, 0], 2)).unwrap();
|
||||
assert_eq!(v, Value::vec(&[1.0, 2.0]));
|
||||
// Bool / Choice。
|
||||
assert_eq!(to_node_value(&ParamValue::Bool(true)).unwrap(), Value::bool_(true));
|
||||
assert_eq!(to_node_value(&ParamValue::Choice(3)).unwrap(), Value::combo(3));
|
||||
// Color:RGB 补 alpha=1(C++ oracle),RGBA 全透传。
|
||||
let v = to_node_value(&ParamValue::Color([0.1, 0.2, 0.3, 0.0], 3)).unwrap();
|
||||
assert_eq!(v, Value::color(0.1, 0.2, 0.3, 1.0));
|
||||
let v = to_node_value(&ParamValue::Color([0.1, 0.2, 0.3, 0.9], 4)).unwrap();
|
||||
assert_eq!(v, Value::color(0.1, 0.2, 0.3, 0.9));
|
||||
// 无值类 / Bytes → None(无节点对应)。
|
||||
assert_eq!(to_node_value(&ParamValue::PushButton), None);
|
||||
assert_eq!(to_node_value(&ParamValue::Container), None);
|
||||
assert_eq!(to_node_value(&ParamValue::Bytes(vec![1])), None);
|
||||
// 字符串族走专用桥(POD 路径 None)。
|
||||
assert_eq!(to_node_value(&ParamValue::String(cs(""))), None);
|
||||
let _ = c(T::NONE); // 哨兵:类型常量可达
|
||||
}
|
||||
|
||||
/// type_default 的无值类/字节族基线(paramDefine 拒绝前的兜底)。
|
||||
#[test]
|
||||
fn type_default_edge_cases() {
|
||||
assert!(matches!(
|
||||
type_default(TYPE_BYTES),
|
||||
ParamValue::Bytes(_)
|
||||
));
|
||||
assert!(matches!(type_default(TYPE_CUSTOM), ParamValue::Bytes(_)));
|
||||
assert!(matches!(type_default(TYPE_PUSHBUTTON), ParamValue::PushButton));
|
||||
assert!(matches!(type_default(TYPE_GROUP), ParamValue::Container));
|
||||
assert!(matches!(type_default(TYPE_PAGE), ParamValue::Container));
|
||||
// 未知类型占位(paramDefine 已拒绝,此处兜底)。
|
||||
assert!(matches!(type_default("OfxParamTypeBogus"), ParamValue::Container));
|
||||
// 数值族的默认 0。
|
||||
assert!(matches!(type_default(TYPE_INTEGER), ParamValue::Int([0, 0, 0], 1)));
|
||||
assert!(matches!(
|
||||
type_default(TYPE_DOUBLE2D),
|
||||
ParamValue::Double([0.0, 0.0, 0.0], 2)
|
||||
));
|
||||
assert!(matches!(
|
||||
type_default(TYPE_RGBA),
|
||||
ParamValue::Color([0.0, 0.0, 0.0, 0.0], 4)
|
||||
));
|
||||
}
|
||||
|
||||
/// set_from_node 的维度截断/补零与字符串静默路径。
|
||||
#[test]
|
||||
fn set_from_node_dimension_rules() {
|
||||
// Integer2D 缺第三维补零;浮点截断。
|
||||
let p = param(TYPE_INTEGER2D);
|
||||
p.set_from_node(&crate::bridge::node::Value::vec(&[1.9, 2.9]));
|
||||
assert_eq!(p.get(), ParamValue::Int([1, 2, 0], 2));
|
||||
// StrChoice 参数遇 STRING 类型:POD 无数据 → 不改值。
|
||||
let p = param(TYPE_STRCHOICE);
|
||||
p.set_from_node(&crate::bridge::node::Value::string());
|
||||
assert!(matches!(p.get(), ParamValue::StrChoice(_)));
|
||||
// Bytes/Custom 参数:任意节点值都不改(无映射)。
|
||||
let p = param(TYPE_CUSTOM);
|
||||
p.set_from_node(&crate::bridge::node::Value::float(3.0));
|
||||
assert!(matches!(p.get(), ParamValue::Bytes(_)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! 进度上报(Progress suite 的宿主侧)。
|
||||
//!
|
||||
//! 对应 C++ 的 `PluginProgressReporter`。进度/取消经 facade 注册的
|
||||
//! 回调出 crate(M9 的 facade 回调模式,不设全局状态)。
|
||||
//! 取消是粘滞的:一旦回调答 false,本报告器的 [`is_cancelled`]
|
||||
//! (image effect suite 的 abort 查询)持续为真。
|
||||
|
||||
use std::ffi::c_int;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// 进度回调:签名与 `include/plugin/instance.h` 的
|
||||
/// `oakplugin_progress_fn` 逐字一致——`(progress, userdata)`,
|
||||
/// 返回非 0 表示应中止(骨架声明为 `(userdata, progress) -> bool`,
|
||||
/// 与头文件不符,以此为准)。
|
||||
pub type ProgressFn = unsafe extern "C" fn(progress: f64, userdata: *mut std::ffi::c_void) -> c_int;
|
||||
|
||||
/// 进度上报器。render 路径持有一份,Progress suite 的
|
||||
/// progressStart/Update/End 转发到这里。
|
||||
pub struct ProgressReporter {
|
||||
callback: Option<ProgressFn>,
|
||||
userdata: usize,
|
||||
/// 取消标志(粘滞;update 返回 false 时置位)。
|
||||
cancelled: AtomicBool,
|
||||
}
|
||||
|
||||
impl ProgressReporter {
|
||||
/// 无回调(渲染静默进行)。
|
||||
pub fn silent() -> Self {
|
||||
Self {
|
||||
callback: None,
|
||||
userdata: 0,
|
||||
cancelled: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// 带回调。
|
||||
///
|
||||
/// # Safety
|
||||
/// `userdata` 的生命周期由注册方保证。
|
||||
pub unsafe fn new(callback: ProgressFn, userdata: *mut std::ffi::c_void) -> Self {
|
||||
Self {
|
||||
callback: Some(callback),
|
||||
// usize 存(裸指针破坏 Send 推导;值语义不变)。
|
||||
userdata: userdata as usize,
|
||||
cancelled: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// 报告进度;返回 false 表示应取消(映射
|
||||
/// kOfxStatReplyNo/action 失败由调用点决定)。
|
||||
pub fn update(&self, progress: f64) -> bool {
|
||||
match self.callback {
|
||||
Some(cb) => {
|
||||
// 头文件契约:非 0 = 中止。
|
||||
let abort = unsafe { cb(progress, self.userdata as *mut std::ffi::c_void) } != 0;
|
||||
if abort {
|
||||
self.cancelled.store(true, Ordering::Relaxed);
|
||||
}
|
||||
!abort
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// 本次渲染是否已被取消(image effect suite 的 abort 透传)。
|
||||
pub(crate) fn is_cancelled(&self) -> bool {
|
||||
self.cancelled.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! OFX 属性集(Property Suite 的宿主侧存储)。
|
||||
//!
|
||||
//! 每个 OFX 对象(host/plugin/descriptor/instance/clip/image/param)
|
||||
//! 都挂一个 [`PropertySet`]。属性是多维数组,按类型存取。
|
||||
//! 参照:HS: ofxhProperty.cpp(GetSuite 的读写语义:越界返回
|
||||
//! kOfxStatErrBadIndex,类型不符返回 kOfxStatErrBadHandle)。
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// 属性值(单元素)。字符串以 OFX 的 `char*` 语义持有(host 拥有,
|
||||
/// 插件借用)。
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Value {
|
||||
/// 32 位整型。
|
||||
Int(i32),
|
||||
/// 双精度。
|
||||
Double(f64),
|
||||
/// UTF-8 字符串(OFX 侧为 NUL 结尾 char*)。
|
||||
String(std::ffi::CString),
|
||||
/// 不透明指针(如 OfxImageEffectHandle 互指)。
|
||||
Pointer(*mut std::ffi::c_void),
|
||||
}
|
||||
|
||||
// 裸指针默认禁 Send/Sync;但属性集语义把指针当作**不透明令牌**:
|
||||
// 跨线程只搬运/比较值,解引用永远是 suite 层的 unsafe 责任(OFX
|
||||
// multithread suite 本就要求插件线程可读写宿主图像缓冲——指针的
|
||||
// 跨线程传递是规范语义,而非逃逸)。Mutex 包裹后属性集整体可共享。
|
||||
// 与 Rust 安全模型不冲突:安全代码只能拿到 `&Value`,无法解引用指针。
|
||||
unsafe impl Send for Value {}
|
||||
unsafe impl Sync for Value {}
|
||||
|
||||
/// 一个属性:名字 + 多维值数组。
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Property {
|
||||
/// OFX 属性名(kOfxProp*;拥有型——协商期的动态名
|
||||
/// "OfxImageClipPropComponents_<clip>" 需要,`&'static str`
|
||||
/// 无法表达)。
|
||||
pub name: String,
|
||||
/// 元素数组;维度 = len。
|
||||
pub values: Vec<Value>,
|
||||
}
|
||||
|
||||
/// 属性集。线程安全(内部 Mutex);OFX 对象的 `*Handle` 即指向它的
|
||||
/// 包装。
|
||||
pub struct PropertySet {
|
||||
props: Mutex<Vec<Property>>,
|
||||
}
|
||||
|
||||
/// 深拷贝(持锁克隆内部数组;createInstance 时描述符属性 → 实例
|
||||
/// 属性需要)。
|
||||
impl Clone for PropertySet {
|
||||
fn clone(&self) -> Self {
|
||||
let props = lock(&self.props);
|
||||
Self {
|
||||
props: Mutex::new(props.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertySet {
|
||||
/// 空集。
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
props: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 定义(或整体替换)一个属性。已存在同名属性时替换其值数组。
|
||||
///
|
||||
/// 对应 C++ `Set::addProperty` 的替换语义(HS: ofxhPropertySuite.cpp:462)
|
||||
/// 与 `PropertyTemplate::setValueN` 的整体写数组语义;属性不存在时
|
||||
/// 新建(维度和值都取自 `values`)。
|
||||
pub fn define(&self, name: &str, values: Vec<Value>) {
|
||||
let mut props = lock(&self.props);
|
||||
if let Some(p) = props.iter_mut().find(|p| p.name == name) {
|
||||
p.values = values;
|
||||
} else {
|
||||
props.push(Property {
|
||||
name: name.to_string(),
|
||||
values,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 单元素便捷定义。
|
||||
pub fn set_one(&self, name: &str, value: Value) {
|
||||
self.define(name, vec![value]);
|
||||
}
|
||||
|
||||
/// 读取第 `index` 个元素;属性不存在或越界返回 `None`。
|
||||
///
|
||||
/// 越界对应 C++ 的 kOfxStatErrBadIndex
|
||||
/// (HS: ofxhPropertySuite.cpp:257 `getValueRaw`),此处以 Option 表达。
|
||||
pub fn get(&self, name: &str, index: usize) -> Option<Value> {
|
||||
let props = lock(&self.props);
|
||||
props
|
||||
.iter()
|
||||
.find(|p| p.name == name)
|
||||
.and_then(|p| p.values.get(index).cloned())
|
||||
}
|
||||
|
||||
/// 覆盖第 `index` 个元素的值(不改变维度);失败返回
|
||||
/// [`crate::error::Error::NotFound`]。
|
||||
///
|
||||
/// 维度固定是刻意为之:与 C++ `setValue` 的自动扩容不同,这里
|
||||
/// 扩容只能经 [`PropertySet::define`] 显式进行(维度语义由定义方
|
||||
/// 掌控,避免插件意外撑大数组)。
|
||||
pub fn set_at(&self, name: &str, index: usize, value: Value) -> Result<()> {
|
||||
let mut props = lock(&self.props);
|
||||
let p = props
|
||||
.iter_mut()
|
||||
.find(|p| p.name == name)
|
||||
.ok_or(Error::NotFound)?;
|
||||
let slot = p.values.get_mut(index).ok_or(Error::NotFound)?;
|
||||
*slot = value;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 维度(属性不存在为 0)。
|
||||
pub fn dimension(&self, name: &str) -> usize {
|
||||
lock(&self.props)
|
||||
.iter()
|
||||
.find(|p| p.name == name)
|
||||
.map_or(0, |p| p.values.len())
|
||||
}
|
||||
|
||||
/// 删除属性;不存在为 no-op。
|
||||
pub fn remove(&self, name: &str) {
|
||||
lock(&self.props).retain(|p| p.name != name);
|
||||
}
|
||||
|
||||
/// 遍历快照(dump/快照测试用)。
|
||||
pub fn snapshot(&self) -> Vec<Property> {
|
||||
lock(&self.props).clone()
|
||||
}
|
||||
|
||||
/// suite 层专用:持锁访问原始存储。
|
||||
///
|
||||
/// 属性 suite 需要"先类型检查再按索引读写、返回内驻字符串指针"
|
||||
/// 等跨多次读写的一致性语义(逐条对照 HS: ofxhProperty.cpp),
|
||||
/// 公开 API 无法在不重复加锁的前提下表达;此入口把 `Vec<Property>`
|
||||
/// 在单一临界区内交给 suite 实现。仅本 crate 可见。
|
||||
pub(crate) fn with_locked<R>(&self, f: impl FnOnce(&mut Vec<Property>) -> R) -> R {
|
||||
let mut props = lock(&self.props);
|
||||
f(&mut props)
|
||||
}
|
||||
}
|
||||
|
||||
/// 取锁。毒锁(本 crate 代码在持锁时 panic)时接管其内部状态继续
|
||||
/// 使用——属性集的数据本身总是完好的,宁可继续也不让一次 panic
|
||||
/// 级联成后续所有 FFI 调用失败。
|
||||
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
m.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! render 驱动:`src/render/src/plugin/pluginrenderer.cpp`
|
||||
//! `PluginRenderer::render_plugin`(1857 行 C++ 的一部分)的渲染流程
|
||||
//! 语义收编(M11 §4)。
|
||||
//!
|
||||
//! 目标:oakrender 的 PluginJob 退化为一次 C ABI 调用(
|
||||
//! [`crate::ffi::oakplugin_instance_render_job`]),本模块承载全部
|
||||
//! OFX 宿主渲染流程。逐段对照的 C++ 行号已注释。
|
||||
//!
|
||||
//! ## 流程(render_frame,对应 render_plugin)
|
||||
//!
|
||||
//! 1. 实例锁(pluginrenderer.cpp:1436-1444)+ 取消检查;
|
||||
//! 2. use_opengl 决策(pluginrenderer.cpp:1446-1457):插件描述符
|
||||
//! kOfxImageEffectPropOpenGLRenderSupported ∈ {true, needed} 且
|
||||
//! 渲染器是 OpenGL 且目标纹理有 GL id;
|
||||
//! 3. 输入纹理收集(pluginrenderer.cpp:1518-1552):effect_input_id
|
||||
//! 匹配 → job.src;否则按 clip 名的纹理表;SimpleSource 回退;
|
||||
//! 4. getClipPreferences(pluginrenderer.cpp:1554-1594);
|
||||
//! 5. RoI/RoD 设定(pluginrenderer.cpp:1596-1607):region_of_interest
|
||||
//! = 目标尺寸(规范坐标);输出 clip 的 RoD 与输出纹理挂接;
|
||||
//! 6. 输入 clip 的 RoD 与格式(pluginrenderer.cpp:1627-1665;Phase 2
|
||||
//! 全链路 F32 → 无转换);
|
||||
//! 7. getRegionOfInterest(pluginrenderer.cpp:1666-1680):RoI 为
|
||||
//! 建议值,任何失败按默认整帧继续(C++ 对 BadHandle 即如此;
|
||||
//! Phase 2 其余状态同样继续——RoI 仅影响上游渲染范围,本管线
|
||||
//! 输入由 oakrender 整帧提供);
|
||||
//! 8. 输出 clip 格式(pluginrenderer.cpp:1686-1697);
|
||||
//! 9. **isIdentity 短路**(ofxRendering "Identity Effects"):
|
||||
//! isIdentity 命中 → 直接把所引输入 clip 在透传时间的帧拷入
|
||||
//! 输出(不调 render action);
|
||||
//! 10. 参数覆盖(pluginrenderer.cpp:132-290 apply_param_overrides);
|
||||
//! 11. render action:CPU 路径经 [`crate::instance::Instance::render`]
|
||||
//! (输出装配:图像 → 目标纹理帧,行跨度感知);GL 路径经
|
||||
//! [`crate::instance::Instance::render_gl`](插件直接画进已附着
|
||||
//! 的输出纹理,无 CPU 回读)。
|
||||
//!
|
||||
//! ## begin/end 序列括号
|
||||
//!
|
||||
//! ofxRendering 文档:"All calls to the render action are bracketed by
|
||||
//! a pair of begin/end sequence render actions"。oakrender 对同一实例
|
||||
//! 的一批帧先 [`begin_sequence`] 后 [`end_sequence`],中间逐帧
|
||||
//! [`render_frame`]。
|
||||
|
||||
use crate::bridge::render::{self, FrameHandle, RendererHandle, TextureHandle};
|
||||
use crate::image::Image;
|
||||
use crate::instance::{Instance, OfxRangeD, OfxRectD, RenderScale};
|
||||
use crate::property::Value;
|
||||
|
||||
/// 一帧渲染任务的输入(oakrender PluginJob 的 C ABI 载体;
|
||||
/// [`crate::ffi::OakPluginJob`] 的 Rust 侧视图)。
|
||||
pub struct RenderJob {
|
||||
/// 帧时间(秒)。
|
||||
pub time: f64,
|
||||
/// 目标纹理(输出;oakrender 侧创建并经句柄传入)。
|
||||
pub dst: TextureHandle,
|
||||
/// 主输入纹理(effect_input_id / SimpleSource;可空)。
|
||||
pub src: TextureHandle,
|
||||
/// effect 输入 clip 名(job.src 的落点;C++ `node->get_effect_input_id()`)。
|
||||
pub effect_input_id: Option<String>,
|
||||
/// 其余输入 clip 的纹理表(clip 名 → 纹理)。
|
||||
pub inputs: Vec<(String, TextureHandle)>,
|
||||
/// 参数覆盖(参数名 → oaknode_value POD;对应 NodeValueRow)。
|
||||
pub values: Vec<(String, crate::ffi::OakNodeValue)>,
|
||||
/// GL 渲染器(None → CPU 路径;Some → 视 use_opengl 决策)。
|
||||
pub renderer: Option<RendererHandle>,
|
||||
/// 渲染前是否清空目标(信息性——C++ render_plugin 亦不处理,
|
||||
/// 由上层渲染器负责;见插件渲染器注释)。
|
||||
pub clear_destination: bool,
|
||||
/// 交互式渲染标记(信息性;Phase 2 的 render in args 恒 0,见
|
||||
/// [`crate::instance::Instance::render`])。
|
||||
pub interactive: bool,
|
||||
}
|
||||
|
||||
impl Default for RenderJob {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
time: 0.0,
|
||||
dst: TextureHandle::null(),
|
||||
src: TextureHandle::null(),
|
||||
effect_input_id: None,
|
||||
inputs: Vec::new(),
|
||||
values: Vec::new(),
|
||||
renderer: None,
|
||||
clear_destination: false,
|
||||
interactive: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// beginSequenceRender 括号(ofxRendering:"bracketed by a pair of
|
||||
/// begin/end sequence render actions";ofxGPURender.h 的 GL 模式下该
|
||||
/// action 的 in args 也带 kOfxImageEffectPropOpenGLEnabled)。
|
||||
/// `gl` 非空时按 GL 模式调用。
|
||||
pub fn begin_sequence(
|
||||
inst: &Instance,
|
||||
range: OfxRangeD,
|
||||
gl: Option<RendererHandle>,
|
||||
) -> crate::error::Result<()> {
|
||||
if gl.is_some() {
|
||||
inst.begin_sequence_render_gl(range)
|
||||
} else {
|
||||
inst.begin_sequence_render(range)
|
||||
}
|
||||
}
|
||||
|
||||
/// endSequenceRender 括号(与 [`begin_sequence`] 配对)。
|
||||
pub fn end_sequence(
|
||||
inst: &Instance,
|
||||
range: OfxRangeD,
|
||||
gl: Option<RendererHandle>,
|
||||
) -> crate::error::Result<()> {
|
||||
if gl.is_some() {
|
||||
inst.end_sequence_render_gl(range)
|
||||
} else {
|
||||
inst.end_sequence_render(range)
|
||||
}
|
||||
}
|
||||
|
||||
/// 渲染一帧(`render_plugin` 的 Rust 移植;逐段行号对照见模块文档)。
|
||||
///
|
||||
/// 返回各输入 clip 的 RoI(clip 名 → 矩形;Phase 2 供测试断言,
|
||||
/// 宿主不据此裁剪输入——输入由 oakrender 整帧提供,与 C++ 渲染器
|
||||
/// 行为一致)。
|
||||
pub fn render_frame(
|
||||
inst: &Instance,
|
||||
job: &RenderJob,
|
||||
) -> crate::error::Result<Vec<(String, OfxRectD)>> {
|
||||
use crate::error::Error;
|
||||
|
||||
// 1. 实例锁(pluginrenderer.cpp:1436-1444:OlivePluginInstance 非
|
||||
// 线程安全,并发 setInputTexture/renderAction 会毁内部状态)。
|
||||
let _lock = inst.render_lock.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// 取消检查必须在任何插件调用之前(shutdown 后入口已卸载)。
|
||||
if inst.cancel.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return Err(Error::Failed("已取消".into()));
|
||||
}
|
||||
|
||||
// 2. use_opengl(pluginrenderer.cpp:1446-1457):插件声明 GL 支持
|
||||
// 且渲染器是 OpenGL 且目标纹理有 GL id 且像素深度协商可行
|
||||
// (管线 F32 满足插件 kOfxOpenGLPropPixelDepth 声明)。
|
||||
let use_opengl = match job.renderer {
|
||||
Some(r) if unsafe { render::renderer_is_open_gl(r) } == 1 => {
|
||||
let plugin_gl = plugin_supports_opengl(inst);
|
||||
let depth_ok =
|
||||
crate::suites::gl_render::pick_gl_pixel_depth(&inst.plugin.descriptor.props).is_some();
|
||||
let dst_id = unsafe { render::texture_id(job.dst) };
|
||||
plugin_gl && depth_ok && dst_id != 0
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// 目标帧与参数(F32 校验;输出装配的依据)。
|
||||
let (dst_frame, dst_params, w, h) = read_dst(job.dst)?;
|
||||
let mut dst_frame = dst_frame;
|
||||
let par = pixel_aspect(&dst_params);
|
||||
// 规范坐标的 RoI/RoD(pluginrenderer.cpp:1595-1603:x2 = 宽 × PAR)。
|
||||
let region_of_interest = OfxRectD {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
x2: w * par,
|
||||
y2: h,
|
||||
};
|
||||
|
||||
// 3. 输入纹理收集(pluginrenderer.cpp:1518-1552)。
|
||||
for clip in &inst.clips {
|
||||
if clip.name == "Output" {
|
||||
continue;
|
||||
}
|
||||
let tex = pick_input(&clip.name, job);
|
||||
if usable(tex) {
|
||||
clip.set_input_texture(tex, job.time);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. getClipPreferences(pluginrenderer.cpp:1554-1594)。
|
||||
let prefs = inst.get_clip_preferences()?;
|
||||
let components = match prefs.output_components.as_str() {
|
||||
"OfxImageComponentRGBA" => crate::image::Components::Rgba,
|
||||
"OfxImageComponentRGB" => crate::image::Components::Rgb,
|
||||
"OfxImageComponentAlpha" => crate::image::Components::Alpha,
|
||||
_ => return Err(Error::Failed("协商分量未知".into())),
|
||||
};
|
||||
if prefs.output_bit_depth != "OfxBitDepthFloat" {
|
||||
return Err(Error::Failed("Phase 2 仅支持 F32 输出".into()));
|
||||
}
|
||||
|
||||
// 5. 输出 clip:RoD + 输出纹理挂接(pluginrenderer.cpp:1603-1606)。
|
||||
let output_clip = inst
|
||||
.clips
|
||||
.iter()
|
||||
.find(|c| c.name == "Output")
|
||||
.ok_or_else(|| Error::Failed("实例无 Output clip".into()))?;
|
||||
output_clip.set_region_of_definition(region_of_interest, job.time);
|
||||
output_clip.set_output_texture(job.dst, job.time);
|
||||
|
||||
// 6. 输入 clip:RoD 与格式(pluginrenderer.cpp:1627-1665;Phase 2
|
||||
// 全链路 F32 → 格式选择恒等,无转换路径)。
|
||||
for clip in &inst.clips {
|
||||
if clip.name == "Output" {
|
||||
continue;
|
||||
}
|
||||
if usable(pick_input(&clip.name, job)) {
|
||||
clip.set_region_of_definition(region_of_interest, job.time);
|
||||
clip.set_video_params(render::PIXEL_FORMAT_F32, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. getRegionOfInterest(pluginrenderer.cpp:1666-1680)。RoI 为
|
||||
// 建议值:失败按默认整帧继续(C++ 对 BadHandle 如此;本管线
|
||||
// 输入整帧提供,RoI 只作记录)。
|
||||
let rois = inst
|
||||
.get_regions_of_interest(job.time, RenderScale { x: 1.0, y: 1.0 }, region_of_interest)
|
||||
.unwrap_or_else(|_| {
|
||||
inst.clips
|
||||
.iter()
|
||||
.map(|_| region_of_interest)
|
||||
.collect()
|
||||
});
|
||||
|
||||
// 8. 输出 clip 格式(pluginrenderer.cpp:1686-1697)。
|
||||
output_clip.set_video_params(render::PIXEL_FORMAT_F32, components.channel_count() as i32);
|
||||
|
||||
// 渲染窗口(像素坐标;pluginrenderer.cpp:1699-1704)。
|
||||
let render_window = OfxRectD { x1: 0.0, y1: 0.0, x2: w, y2: h };
|
||||
|
||||
// 9. isIdentity 短路(ofxRendering "Identity Effects"):插件声明
|
||||
// 本帧等价于某输入 clip → 直接透传该 clip 在透传时间的帧。
|
||||
if let Some((t, clip_name)) = inst.is_identity(job.time)? {
|
||||
passthrough(inst, &clip_name, t, job.dst)?;
|
||||
unsafe { render::frame_free(&mut (dst_frame)) };
|
||||
return Ok(zip_rois(inst, &rois));
|
||||
}
|
||||
|
||||
// 10. 参数覆盖(pluginrenderer.cpp:1729-1731 + 132-290)。
|
||||
apply_param_overrides(inst, &job.values);
|
||||
|
||||
// 11. render action。
|
||||
if !use_opengl {
|
||||
let output = std::sync::Arc::new(Image::allocate(
|
||||
crate::image::BitDepth::Float,
|
||||
components,
|
||||
OfxRectD { x1: 0.0, y1: 0.0, x2: w, y2: h },
|
||||
));
|
||||
inst.render(job.time, RenderScale { x: 1.0, y: 1.0 }, render_window, output.clone())?;
|
||||
// 输出装配(pluginrenderer.cpp:1762-1834 的 CPU 路径)。
|
||||
write_output_frame(job.dst, &output)?;
|
||||
} else {
|
||||
// GL 路径:插件直接画进已附着的输出纹理(
|
||||
// pluginrenderer.cpp:1784-1834 的 GL 分支);无 CPU 回读。
|
||||
inst.render_gl(job.time, RenderScale { x: 1.0, y: 1.0 }, render_window, job.renderer.unwrap(), job.dst)?;
|
||||
}
|
||||
|
||||
unsafe { render::frame_free(&mut (dst_frame)) };
|
||||
Ok(zip_rois(inst, &rois))
|
||||
}
|
||||
|
||||
/// 把输入 clip 名与 RoI 列表配对(与 `clips` 顺序一致)。
|
||||
fn zip_rois(inst: &Instance, rois: &[OfxRectD]) -> Vec<(String, OfxRectD)> {
|
||||
inst.clips
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| c.name != "Output")
|
||||
.map(|(i, c)| (c.name.clone(), rois.get(i).copied().unwrap_or_default()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 读目标纹理的帧与参数(F32 校验)。返回 (帧句柄, 参数, 宽, 高)。
|
||||
fn read_dst(dst: TextureHandle) -> crate::error::Result<(FrameHandle, render::VideoParams, f64, f64)> {
|
||||
use crate::error::Error;
|
||||
let mut frame = FrameHandle::null();
|
||||
if unsafe { render::texture_get_frame(dst, &mut frame) } != 0 || frame.is_null() {
|
||||
return Err(Error::Failed("输出纹理无 CPU 帧".into()));
|
||||
}
|
||||
let mut params = render::VideoParams::default();
|
||||
if unsafe { render::frame_get_params(frame, &mut params) } != 0 {
|
||||
unsafe { render::frame_free(&mut frame) };
|
||||
return Err(Error::Failed("输出帧无参数".into()));
|
||||
}
|
||||
if params.format != render::PIXEL_FORMAT_F32 {
|
||||
unsafe { render::frame_free(&mut frame) };
|
||||
return Err(Error::Failed(format!(
|
||||
"输出帧格式 {} 非 F32(Phase 2 约束)",
|
||||
params.format
|
||||
)));
|
||||
}
|
||||
let (w, h) = (params.width as f64, params.height as f64);
|
||||
if w <= 0.0 || h <= 0.0 {
|
||||
unsafe { render::frame_free(&mut frame) };
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
Ok((frame, params, w, h))
|
||||
}
|
||||
|
||||
/// 目标参数的像素比(缺失 1.0)。
|
||||
fn pixel_aspect(params: &render::VideoParams) -> f64 {
|
||||
if params.pixel_aspect_den != 0 {
|
||||
params.pixel_aspect_num as f64 / params.pixel_aspect_den as f64
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件描述符是否声明 GL 渲染支持(ofxGPURender.h:397-408:
|
||||
/// "true"/"needed")。
|
||||
fn plugin_supports_opengl(inst: &Instance) -> bool {
|
||||
inst.plugin
|
||||
.descriptor
|
||||
.props
|
||||
.get(crate::host::PROP_GL_RENDER_SUPPORTED, 0)
|
||||
.map(|v| match v {
|
||||
Value::String(s) => {
|
||||
let s = s.to_string_lossy();
|
||||
s == "true" || s == "needed"
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 输入纹理是否可用(非空且非占位;pluginrenderer.cpp:1504-1513 的
|
||||
/// is_usable_input——Phase 2 只看非 dummy,帧/Renderer 由 oakrender
|
||||
/// 保证)。
|
||||
fn usable(tex: TextureHandle) -> bool {
|
||||
!tex.is_null() && unsafe { render::texture_is_dummy(tex) } == 0
|
||||
}
|
||||
|
||||
/// 按 C++ pluginrenderer.cpp:1527-1543 的规则选输入纹理。
|
||||
fn pick_input(clip_name: &str, job: &RenderJob) -> TextureHandle {
|
||||
if job.effect_input_id.as_deref() == Some(clip_name) && !job.src.is_null() {
|
||||
return job.src;
|
||||
}
|
||||
for (name, tex) in &job.inputs {
|
||||
if name == clip_name {
|
||||
return *tex;
|
||||
}
|
||||
}
|
||||
// SimpleSource 回退(pluginrenderer.cpp:1534-1543:
|
||||
// kOfxImageEffectSimpleSourceClipName 取 k_texture_input,再回退
|
||||
// job.src)。
|
||||
if clip_name == "Source" && !job.src.is_null() {
|
||||
return job.src;
|
||||
}
|
||||
TextureHandle::null()
|
||||
}
|
||||
|
||||
/// isIdentity 透传:把所引输入 clip 在 `t` 的帧拷入输出(CPU 拷贝;
|
||||
/// 目标帧 F32 校验)。
|
||||
fn passthrough(
|
||||
inst: &Instance,
|
||||
clip_name: &str,
|
||||
t: f64,
|
||||
dst: TextureHandle,
|
||||
) -> crate::error::Result<()> {
|
||||
use crate::error::Error;
|
||||
let clip = inst
|
||||
.clips
|
||||
.iter()
|
||||
.find(|c| c.name == clip_name)
|
||||
.ok_or_else(|| Error::Failed(format!("isIdentity 引用未知 clip {clip_name}")))?;
|
||||
let image = clip.fetch_image(t, RenderScale { x: 1.0, y: 1.0 }, None)?;
|
||||
write_output_frame(dst, &image)
|
||||
}
|
||||
|
||||
/// 参数覆盖(pluginrenderer.cpp:132-290 `apply_param_overrides` 的
|
||||
/// Rust 移植):把每帧的节点值注入实例参数。字符串族(String/
|
||||
/// StrChoice)经专用 C ABI(set_param_string),不在此表的 oaknode
|
||||
/// POD 表达范围内 → 跳过(与 C++ 的 k_file/k_text/k_font/k_str_combo
|
||||
/// 走专用桥一致)。
|
||||
fn apply_param_overrides(inst: &Instance, values: &[(String, crate::ffi::OakNodeValue)]) {
|
||||
for (key, v) in values {
|
||||
let Some(p) = inst.params.find(key) else { continue };
|
||||
let Some(pv) = crate::ffi::node_value_to_param(v, &p.def.ofx_type) else {
|
||||
continue;
|
||||
};
|
||||
p.set_ofx(pv);
|
||||
}
|
||||
}
|
||||
|
||||
/// 把 CPU 图像写入目标纹理的帧(行优先、行跨度感知;F32 校验)。
|
||||
/// Phase 2 输出装配的公共落点(CPU render 路径与 isIdentity 透传
|
||||
/// 共用)。
|
||||
pub(crate) fn write_output_frame(dst: TextureHandle, image: &Image) -> crate::error::Result<()> {
|
||||
use crate::error::Error;
|
||||
let mut frame = FrameHandle::null();
|
||||
if unsafe { render::texture_get_frame(dst, &mut frame) } != 0 || frame.is_null() {
|
||||
return Err(Error::Failed("输出纹理无 CPU 帧".into()));
|
||||
}
|
||||
let mut params = render::VideoParams::default();
|
||||
if unsafe { render::frame_get_params(frame, &mut params) } != 0 {
|
||||
unsafe { render::frame_free(&mut frame) };
|
||||
return Err(Error::Failed("输出帧无参数".into()));
|
||||
}
|
||||
if params.format != render::PIXEL_FORMAT_F32 {
|
||||
unsafe { render::frame_free(&mut frame) };
|
||||
return Err(Error::Failed("输出帧格式非 F32(Phase 2 约束)".into()));
|
||||
}
|
||||
let (w, h) = (params.width as usize, params.height as usize);
|
||||
let tight = w * image.components().channel_count() * 4;
|
||||
if tight != image.row_bytes() || tight * h != image.pixels().len() {
|
||||
unsafe { render::frame_free(&mut frame) };
|
||||
return Err(Error::Failed("图像尺寸与输出帧不一致".into()));
|
||||
}
|
||||
let dst_ptr = unsafe { render::frame_data(frame) };
|
||||
if dst_ptr.is_null() {
|
||||
unsafe { render::frame_free(&mut frame) };
|
||||
return Err(Error::Failed("输出帧无数据".into()));
|
||||
}
|
||||
let row = unsafe { render::frame_linesize_bytes(frame) } as usize;
|
||||
let row = if row > 0 { row } else { tight };
|
||||
let dst_bytes =
|
||||
unsafe { std::slice::from_raw_parts_mut(dst_ptr as *mut u8, row * h) };
|
||||
let pixels = image.pixels();
|
||||
for y in 0..h {
|
||||
let d = y * row;
|
||||
let s = y * tight;
|
||||
dst_bytes[d..d + tight].copy_from_slice(&pixels[s..s + tight]);
|
||||
}
|
||||
unsafe { render::frame_free(&mut frame) };
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! OfxImageEffectOpenGLRenderSuiteV1(M11 第 2 期):clipLoadTexture /
|
||||
//! clipFreeTexture / flushResources。
|
||||
//!
|
||||
//! 语义对照 ofxGPURender.h(vendored,OpenGL Render Suite 一节)与
|
||||
//! HostSupport 插件侧实现(HS: ofxhImageEffect.cpp:2296-2367):
|
||||
//!
|
||||
//! - `clipLoadTexture(clip, time, format, region, *texture)`:把 clip
|
||||
//! 在 `time` 的图像加载为 GL 纹理。纹理句柄是**属性集**(标签 0),
|
||||
//! 含 ofxGPURender.h 规定的 12 个属性(OpenGLTextureIndex/
|
||||
//! OpenGLTextureTarget/PixelDepth/Components/PreMultiplication/
|
||||
//! RenderScale/PixelAspectRatio/Bounds/RegionOfDefinition/RowBytes/
|
||||
//! Field/UniqueIdentifier);宿主侧存强引用([`LIVE_TEXTURES`]),
|
||||
//! clipFreeTexture 摘除即释放(对应 HS 的 get/release 配对)。
|
||||
//! - Output clip:`format` 忽略,宿主返回已附着的输出纹理句柄——
|
||||
//! 绑定渲染目标的动作(ofxGPURender.h "the host must bind the
|
||||
//! resulting texture as the current color buffer")由调用方约定:
|
||||
//! GL render 驱动的 C ABI 契约要求 oakrender 在进入 render_job 前
|
||||
//! 已把输出纹理附着为渲染器输出目标(等价 C++ 的
|
||||
//! `PluginRenderer::attach_output_texture`)。clipFreeTexture 对
|
||||
//! Output 只释放句柄、不删纹理(宿主还要读它)。
|
||||
//! - 输入纹理经 [`crate::bridge::render`] 在渲染器上创建(CPU 帧 →
|
||||
//! GL 上传)。纹理格式:全链路 F32 约束下,像素深度按 clip 协商
|
||||
//! 结果(恒 F32);`format` 参数(kOfxImageEffectGLFormat*)若
|
||||
//! 请求的分量与协商分量不符,Phase 2 不做转换 → Failed(规范要求
|
||||
//! "host ensures it gives the requested format"——宁可显式失败也
|
||||
//! 不静默给错格式)。ofxGPURender.h 注明"宿主无需按 Clip
|
||||
//! Preferences 把图像重映射到插件请求的位深",插件以纹理句柄的
|
||||
//! PixelDepth/Components 为准。
|
||||
//! - `flushResources`:宿主在 render 之间不缓存 GPU 资源(纹理随
|
||||
//! clipFreeTexture 立即释放)→ 无可释放 → kOfxStatReplyDefault
|
||||
//! (规范:"nothing the host could do")。
|
||||
//! - GL 上下文规则(ofxGPURender.h "OpenGL Current Context"):宿主
|
||||
//! 只在 Render/BeginSequenceRender/EndSequenceRender/Attach/Detach
|
||||
//! 期间要求上下文 current;本实现的约定是调用方(oakrender
|
||||
//! PluginJob 路径)在调用前置好上下文,本 suite 经
|
||||
//! [`crate::suites::gl_ctx`] TLS 取渲染器句柄。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, c_double, c_int, c_void, CStr, CString};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::clip::ClipInstance;
|
||||
use crate::image::Image;
|
||||
use crate::property::{PropertySet, Value};
|
||||
use crate::suites::{status, tag};
|
||||
|
||||
// ---- GL 常量(ofxGPURender.h;GLFormat 字符串为 ofxOpenGLRender.h
|
||||
// OpenFX 1.4 的规范名,vendored 头文件是 stub 未收录,按规范定义)----
|
||||
|
||||
/// kOfxImageEffectPropOpenGLTextureIndex(ofxGPURender.h:135)。
|
||||
pub(crate) const GL_TEXTURE_INDEX: &str = "OfxImageEffectPropOpenGLTextureIndex";
|
||||
/// kOfxImageEffectPropOpenGLTextureTarget(ofxGPURender.h:152)。
|
||||
pub(crate) const GL_TEXTURE_TARGET: &str = "OfxImageEffectPropOpenGLTextureTarget";
|
||||
|
||||
/// kOfxImageEffectGLFormatRGBA(ofxOpenGLRender.h 1.4)。
|
||||
pub(crate) const GL_FORMAT_RGBA: &str = "OfxImageEffectGLFormatRGBA";
|
||||
/// kOfxImageEffectGLFormatRGB。
|
||||
pub(crate) const GL_FORMAT_RGB: &str = "OfxImageEffectGLFormatRGB";
|
||||
/// kOfxImageEffectGLFormatAlpha。
|
||||
pub(crate) const GL_FORMAT_ALPHA: &str = "OfxImageEffectGLFormatAlpha";
|
||||
/// kOfxImageEffectGLFormatLuminance。
|
||||
pub(crate) const GL_FORMAT_LUMINANCE: &str = "OfxImageEffectGLFormatLuminance";
|
||||
/// kOfxImageEffectGLFormatLuminanceAlpha。
|
||||
pub(crate) const GL_FORMAT_LUMINANCE_ALPHA: &str = "OfxImageEffectGLFormatLuminanceAlpha";
|
||||
|
||||
/// GL_TEXTURE_2D 的 GLenum 值(0x0DE1;纹理句柄的 OpenGLTextureTarget
|
||||
/// 属性。GL 规范值,非 OFX 宏)。
|
||||
pub(crate) const GL_TEXTURE_2D: i32 = 0x0DE1;
|
||||
|
||||
/// kOfxImageEffectPropPreMultiplication 的非预乘默认值。
|
||||
pub(crate) const PREMULT_NONE: &str = "OfxImagePreMultipliedNone";
|
||||
|
||||
/// GL 像素深度协商(ofxGPURender.h:65-89 kOfxOpenGLPropPixelDepth):
|
||||
/// 插件描述符声明的 GL 渲染支持位深列表(可选)。
|
||||
///
|
||||
/// 返回 Some(实际位深) 表示 GL 模式可行,None 表示管线无法满足
|
||||
/// 插件声明 → 宿主应回退 CPU 渲染(ofxGPURender.h "the host will
|
||||
/// try to provide buffers/textures in one of the supported formats";
|
||||
/// Phase 2 全链路 F32,无法提供其他位深):
|
||||
/// - 列表缺失/为空 → Some(Float)(宿主自选,规范默认);
|
||||
/// - 列表含 Float → Some(Float);
|
||||
/// - 列表存在且不含 Float → None(管线约束;GL 模式不可行)。
|
||||
pub(crate) fn pick_gl_pixel_depth(plugin_props: &crate::property::PropertySet) -> Option<&'static str> {
|
||||
use crate::property::Value;
|
||||
let dim = plugin_props.dimension(crate::host::PROP_GL_PIXEL_DEPTH);
|
||||
if dim == 0 {
|
||||
return Some("OfxBitDepthFloat");
|
||||
}
|
||||
for i in 0..dim {
|
||||
if let Some(Value::String(s)) = plugin_props.get(crate::host::PROP_GL_PIXEL_DEPTH, i) {
|
||||
if s.to_string_lossy() == "OfxBitDepthFloat" {
|
||||
return Some("OfxBitDepthFloat");
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::property::{PropertySet, Value};
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
/// GL 像素深度协商矩阵(ofxGPURender.h kOfxOpenGLPropPixelDepth):
|
||||
/// 未声明/含 Float → 管线 F32 可行;声明且不含 Float → None
|
||||
/// (GL 模式不可行,回退 CPU)。
|
||||
#[test]
|
||||
fn pick_gl_pixel_depth_matrix() {
|
||||
let props = PropertySet::new();
|
||||
assert_eq!(pick_gl_pixel_depth(&props), Some("OfxBitDepthFloat"));
|
||||
|
||||
let props2 = PropertySet::new();
|
||||
props2.define(
|
||||
crate::host::PROP_GL_PIXEL_DEPTH,
|
||||
vec![
|
||||
Value::String(cs("OfxBitDepthHalf")),
|
||||
Value::String(cs("OfxBitDepthByte")),
|
||||
],
|
||||
);
|
||||
assert_eq!(pick_gl_pixel_depth(&props2), None);
|
||||
|
||||
let props3 = PropertySet::new();
|
||||
props3.define(
|
||||
crate::host::PROP_GL_PIXEL_DEPTH,
|
||||
vec![
|
||||
Value::String(cs("OfxBitDepthByte")),
|
||||
Value::String(cs("OfxBitDepthFloat")),
|
||||
],
|
||||
);
|
||||
assert_eq!(pick_gl_pixel_depth(&props3), Some("OfxBitDepthFloat"));
|
||||
|
||||
let props4 = PropertySet::new();
|
||||
props4.define(
|
||||
crate::host::PROP_GL_PIXEL_DEPTH,
|
||||
vec![Value::String(cs("OfxBitDepthFloat"))],
|
||||
);
|
||||
assert_eq!(pick_gl_pixel_depth(&props4), Some("OfxBitDepthFloat"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 存活纹理表 -----------------------------------------------------------
|
||||
|
||||
/// 存活 GL 纹理表:clipLoadTexture 产出(props 地址 → 属性集 +
|
||||
/// 纹理强引用 + 是否输出 clip);clipFreeTexture 摘除即释放——对应
|
||||
/// HS 的 get/release 配对(HS: ofxhImageEffect.cpp:2336-2351)。
|
||||
///
|
||||
/// 属性集必须**装箱**(Box 稳定堆地址):纹理句柄指向它,函数返回后
|
||||
/// 必须仍存活;栈上临时变量会悬垂(phase-2 实现初版的 bug)。
|
||||
static LIVE_TEXTURES: std::sync::LazyLock<
|
||||
Mutex<HashMap<usize, (Box<PropertySet>, crate::bridge::render::TextureHandle, bool)>>,
|
||||
> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// 登记纹理(clipLoadTexture 内部;`props` 装箱后取地址为句柄基址)。
|
||||
fn register(props: Box<PropertySet>, texture: crate::bridge::render::TextureHandle, is_output: bool) -> usize {
|
||||
let addr = &*props as *const PropertySet as usize;
|
||||
LIVE_TEXTURES
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(addr, (props, texture, is_output));
|
||||
addr
|
||||
}
|
||||
|
||||
/// 释放全部残留输入纹理(GL render action 返回后的安全网:规范要求
|
||||
/// 插件在 action 返回前 clipFreeTexture 全部句柄;遗漏的输入纹理在
|
||||
/// 此释放,输出纹理保留——宿主还要读它)。
|
||||
pub(crate) fn purge_leftovers() {
|
||||
let leftovers: Vec<(crate::bridge::render::TextureHandle, bool)> = {
|
||||
let mut live = LIVE_TEXTURES.lock().unwrap_or_else(|e| e.into_inner());
|
||||
live.drain().map(|(_k, (_p, t, o))| (t, o)).collect()
|
||||
};
|
||||
for (texture, is_output) in leftovers {
|
||||
if !is_output && !texture.is_null() {
|
||||
let mut t = texture;
|
||||
unsafe { crate::bridge::render::texture_free(&mut t) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
|
||||
catch_unwind(AssertUnwindSafe(f))
|
||||
.map_or_else(|_| status::FAILED, |r| r.map_or_else(|c| c, |()| status::OK))
|
||||
}
|
||||
|
||||
/// clip 句柄解析(实例期)。
|
||||
fn resolve_clip(handle: *mut c_void) -> Result<&'static ClipInstance, c_int> {
|
||||
if handle.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
unsafe {
|
||||
match tag::kind(handle) {
|
||||
tag::CLIP => Ok(&*(tag::strip(handle) as *const ClipInstance)),
|
||||
_ => Err(status::ERR_BAD_HANDLE),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
/// 把矩形写成 Int×4 属性(Bounds/ROD 用;OFX 图像属性是像素坐标)。
|
||||
fn rect_props(props: &PropertySet, name: &str, rect: crate::instance::OfxRectD) {
|
||||
let b = |v: f64| Value::Int(v.round() as i32);
|
||||
props.define(
|
||||
name,
|
||||
vec![b(rect.x1), b(rect.y1), b(rect.x2), b(rect.y2)],
|
||||
);
|
||||
}
|
||||
|
||||
/// 从 clip 属性读字符串(缺失返回默认)。
|
||||
fn clip_string(clip: &ClipInstance, name: &str, default: &str) -> String {
|
||||
clip.props
|
||||
.get(name, 0)
|
||||
.map(|v| match v {
|
||||
Value::String(s) => s.to_string_lossy().into_owned(),
|
||||
_ => default.to_string(),
|
||||
})
|
||||
.unwrap_or_else(|| default.to_string())
|
||||
}
|
||||
|
||||
/// 构造纹理属性集(ofxGPURender.h 规定的属性;输入与输出共用,
|
||||
/// 只是数据来源不同)。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn make_texture_props(
|
||||
texture: crate::bridge::render::TextureHandle,
|
||||
width: f64,
|
||||
height: f64,
|
||||
components: crate::image::Components,
|
||||
depth: &str,
|
||||
premult: &str,
|
||||
par: f64,
|
||||
scale: crate::instance::RenderScale,
|
||||
row_bytes: i32,
|
||||
) -> PropertySet {
|
||||
let props = PropertySet::new();
|
||||
props.set_one(
|
||||
GL_TEXTURE_INDEX,
|
||||
Value::Int(unsafe { crate::bridge::render::texture_id(texture) }),
|
||||
);
|
||||
props.set_one(GL_TEXTURE_TARGET, Value::Int(GL_TEXTURE_2D));
|
||||
props.set_one(
|
||||
crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
|
||||
Value::String(cs(depth)),
|
||||
);
|
||||
props.set_one(
|
||||
crate::image::K_IMAGE_EFFECT_PROP_COMPONENTS,
|
||||
Value::String(cs(components.to_ofx())),
|
||||
);
|
||||
props.set_one("OfxImageEffectPropPreMultiplication", Value::String(cs(premult)));
|
||||
props.define(
|
||||
crate::host::PROP_RENDER_SCALE,
|
||||
vec![Value::Double(scale.x), Value::Double(scale.y)],
|
||||
);
|
||||
props.set_one("OfxImagePropPixelAspectRatio", Value::Double(par));
|
||||
let bounds = crate::instance::OfxRectD { x1: 0.0, y1: 0.0, x2: width, y2: height };
|
||||
rect_props(&props, crate::image::K_IMAGE_PROP_BOUNDS, bounds);
|
||||
rect_props(&props, crate::image::K_IMAGE_PROP_ROD, bounds);
|
||||
props.set_one(crate::image::K_IMAGE_PROP_ROW_BYTES, Value::Int(row_bytes));
|
||||
props.set_one("OfxImagePropField", Value::String(cs("OfxImageFieldNone")));
|
||||
props.set_one(
|
||||
crate::image::K_IMAGE_PROP_UNIQUE_ID,
|
||||
Value::String(crate::image::unique_identifier()),
|
||||
);
|
||||
props
|
||||
}
|
||||
|
||||
/// 纹理请求格式 → 期望分量(NULL/未知 → None = 宿主自选)。
|
||||
fn format_components(format: Option<&str>) -> Option<crate::image::Components> {
|
||||
match format {
|
||||
Some(GL_FORMAT_RGBA) | None => Some(crate::image::Components::Rgba),
|
||||
Some(GL_FORMAT_RGB) => Some(crate::image::Components::Rgb),
|
||||
Some(GL_FORMAT_ALPHA) => Some(crate::image::Components::Alpha),
|
||||
// Luminance 族:Phase 2 不建模(无对应 Components),宿主
|
||||
// 按 RGBA 上传并如实上报——格式核对走 None 分支。
|
||||
Some(GL_FORMAT_LUMINANCE) | Some(GL_FORMAT_LUMINANCE_ALPHA) => None,
|
||||
Some(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// clipLoadTexture:把 clip 在 `time` 的图像加载为 GL 纹理。
|
||||
///
|
||||
/// `format` 为请求的纹理格式(kOfxImageEffectGLFormat*;NULL = 宿主
|
||||
/// 按插件的 kOfxOpenGLPropPixelDepth 决定——Phase 2 全链路 F32)。
|
||||
/// `region`(规范坐标,可空)会裁剪到 clip 的 RoD——Phase 2 仅支持
|
||||
/// 整帧(NULL);子区域请求返回 Failed(插件按规范继续,视作黑底)。
|
||||
unsafe extern "C" fn clip_load_texture(
|
||||
clip: *mut c_void,
|
||||
time: c_double,
|
||||
format: *const c_char,
|
||||
region: *const c_void,
|
||||
out: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
unsafe { *out = std::ptr::null_mut() };
|
||||
let c = resolve_clip(clip)?;
|
||||
// GL 上下文(TLS):仅 GL 渲染期存在(ofxGPURender.h 的
|
||||
// "OpenGL Current Context" 规则)。
|
||||
let gl = crate::suites::gl_ctx().ok_or(status::ERR_MISSING_HOST_FEATURE)?;
|
||||
|
||||
let scale = crate::suites::render_ctx()
|
||||
.map(|ctx| ctx.scale)
|
||||
.unwrap_or(crate::instance::RenderScale { x: 1.0, y: 1.0 });
|
||||
|
||||
if c.name == "Output" {
|
||||
// Output:返回已附着的输出纹理(format 忽略;渲染目标
|
||||
// 绑定由调用方契约保证——等价 C++ attach_output_texture)。
|
||||
let tex = gl.output_texture;
|
||||
if tex.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
let (w, h) = texture_size(&tex);
|
||||
if w <= 0.0 || h <= 0.0 {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
let props = make_texture_props(
|
||||
tex,
|
||||
w,
|
||||
h,
|
||||
crate::image::Components::Rgba,
|
||||
gl.gl_pixel_depth,
|
||||
&clip_string(c, "OfxImageEffectPropPreMultiplication", PREMULT_NONE),
|
||||
clip_par(c),
|
||||
scale,
|
||||
(w as i32) * 4 * 4,
|
||||
);
|
||||
let addr = register(Box::new(props), tex, true);
|
||||
unsafe { *out = tag::make(addr as *const PropertySet, tag::PROPERTY_SET) };
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 输入 clip:Phase 2 仅支持整帧(fetch_image 的 phase-1 约束)。
|
||||
if !region.is_null() {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
let format = if format.is_null() {
|
||||
None
|
||||
} else {
|
||||
unsafe { CStr::from_ptr(format) }.to_str().ok()
|
||||
};
|
||||
|
||||
let image = c
|
||||
.fetch_image(time, scale, None)
|
||||
.map_err(|_| status::FAILED)?;
|
||||
let components = image.components();
|
||||
// 明确请求了不同分量 → Phase 2 不转换 → Failed(规范要求
|
||||
// 满足请求格式;静默给错格式比显式失败更糟)。
|
||||
if let Some(expected) = format_components(format) {
|
||||
if expected != components {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
}
|
||||
let premult = clip_string(c, "OfxImageEffectPropPreMultiplication", PREMULT_NONE);
|
||||
|
||||
let (w, h) = (image_width(&image), image_height(&image));
|
||||
if w <= 0.0 || h <= 0.0 {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
let params = crate::bridge::render::VideoParams {
|
||||
width: w as i32,
|
||||
height: h as i32,
|
||||
format: crate::bridge::render::PIXEL_FORMAT_F32,
|
||||
..Default::default()
|
||||
};
|
||||
let tex = unsafe {
|
||||
crate::bridge::render::texture_create(
|
||||
gl.renderer,
|
||||
¶ms,
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! OfxImageEffectSuite v1:clip/image 操作。
|
||||
//!
|
||||
//! 语义对照 HS: ofxhImageEffect.cpp:
|
||||
//! - clipGetImage 返回图像 **属性集** handle(HS:2038 `getPropHandle`),
|
||||
//! 图像存活由宿主表托管(get → 插入强引用,clipReleaseImage → 摘除
|
||||
//! 释放;对应 HS 的引用计数配对,HS:2044);
|
||||
//! - clipGetRegionOfDefinition:读 clip 实例属性里的
|
||||
//! kOfxImageEffectPropRegionOfDefinition(协商时宿主写入);非法
|
||||
//! RoD → kOfxStatFailed(HS:2143-2147);
|
||||
//! - abort:实例期 → 当前渲染的进度取消状态(HS:2154-2170;
|
||||
//! HS 默认返回 0,本实现按规范返回 REPLY_YES/REPLY_NO);
|
||||
//! - imageMemory*:账本同 memory suite(HS 的 lock 是"锁住防重分配"
|
||||
//! 语义,第 1 期账本不需要 → OK no-op,见 memory.rs 文档)。
|
||||
//!
|
||||
//! `// TODO(clip)`:clipGetImage 依赖 [`crate::clip::ClipInstance::fetch_image`]
|
||||
//! (bridge::render 帧访问 C ABI 未冻结),代码已齐、运行时待 clip.rs。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, c_double, c_int, c_void, CStr};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::clip::ClipInstance;
|
||||
use crate::descriptor::{ClipDescriptor, EffectDescriptor};
|
||||
use crate::image::Image;
|
||||
use crate::instance::{Instance, OfxRectD, RenderScale};
|
||||
use crate::property::PropertySet;
|
||||
use crate::suites::{status, tag};
|
||||
|
||||
/// 函数表布局(与 SDK `OfxImageEffectSuiteV1` 逐字段一致;常用子集
|
||||
/// 注释,完整字段以 SDK 为准)。
|
||||
#[repr(C)]
|
||||
pub struct ImageEffectSuiteV1 {
|
||||
/// getPropertySet:取 effect/clip/image 的属性集。
|
||||
pub get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
|
||||
/// getParamSet:取实例参数集。
|
||||
pub get_param_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
|
||||
/// clipDefine(describe 期间):定义 clip。
|
||||
pub clip_define: unsafe extern "C" fn(*mut c_void, *const c_char, *mut *mut c_void) -> c_int,
|
||||
/// clipGetHandle:按名取 clip。
|
||||
pub clip_get_handle: unsafe extern "C" fn(*mut c_void, *const c_char, *mut *mut c_void, *mut *mut c_void) -> c_int,
|
||||
/// clipGetPropertySet
|
||||
pub clip_get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
|
||||
/// clipGetImage:取图像(host 侧增加引用,必须与
|
||||
/// clipReleaseImage 配对)。
|
||||
pub clip_get_image: unsafe extern "C" fn(*mut c_void, c_double, *const c_void, *mut *mut c_void) -> c_int,
|
||||
/// clipReleaseImage
|
||||
pub clip_release_image: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
/// clipGetRegionOfDefinition
|
||||
pub clip_get_region_of_definition: unsafe extern "C" fn(*mut c_void, c_double, *mut c_void) -> c_int,
|
||||
/// abort:查询是否应中止(进度取消透传)。
|
||||
pub abort: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
/// imageMemoryAlloc / imageMemoryFree / imageMemoryLock /
|
||||
/// imageMemoryUnlock:图像内存管理(账本同 memory suite)。
|
||||
pub image_memory_alloc: unsafe extern "C" fn(*mut c_void, c_int, *mut *mut c_void) -> c_int,
|
||||
/// imageMemoryFree
|
||||
pub image_memory_free: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
/// imageMemoryLock
|
||||
pub image_memory_lock: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
|
||||
/// imageMemoryUnlock
|
||||
pub image_memory_unlock: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
}
|
||||
|
||||
/// 存活图像表:clipGetImage 产出(props 地址 → 强引用;唯一持有者,
|
||||
/// clipReleaseImage 摘除即释放——对应 HS 的 get/release 配对)。
|
||||
static LIVE_IMAGES: std::sync::LazyLock<Mutex<HashMap<usize, std::sync::Arc<Image>>>> =
|
||||
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
|
||||
catch_unwind(AssertUnwindSafe(f))
|
||||
.map_or_else(|_| status::FAILED, |r| r.map_or_else(|c| c, |()| status::OK))
|
||||
}
|
||||
|
||||
/// 属性名(空指针/非 UTF-8 → ErrValue)。
|
||||
unsafe fn c_name<'a>(name: *const c_char) -> Result<&'a str, c_int> {
|
||||
if name.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
unsafe { CStr::from_ptr(name) }
|
||||
.to_str()
|
||||
.map_err(|_| status::ERR_VALUE)
|
||||
}
|
||||
|
||||
/// effect 句柄解析(describe 期 → 描述符;实例期 → 实例)。
|
||||
enum EffectRef<'a> {
|
||||
Descriptor(&'a mut EffectDescriptor),
|
||||
Instance(&'a Instance),
|
||||
}
|
||||
|
||||
fn resolve_effect(handle: *mut c_void) -> Result<EffectRef<'static>, c_int> {
|
||||
if handle.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
// 两者 props 均在偏移 0(句柄约定)。
|
||||
unsafe {
|
||||
match tag::kind(handle) {
|
||||
tag::DESCRIPTOR => Ok(EffectRef::Descriptor(&mut *(tag::strip(handle) as *mut EffectDescriptor))),
|
||||
tag::INSTANCE => Ok(EffectRef::Instance(&*(tag::strip(handle) as *const Instance))),
|
||||
_ => Err(status::ERR_BAD_HANDLE),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// clip 句柄解析(实例期;describe 期 ClipDescriptor 只在属性
|
||||
/// suite 里用,不走这里)。
|
||||
fn resolve_clip(handle: *mut c_void) -> Result<&'static ClipInstance, c_int> {
|
||||
if handle.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
unsafe {
|
||||
match tag::kind(handle) {
|
||||
tag::CLIP => Ok(&*(tag::strip(handle) as *const ClipInstance)),
|
||||
_ => Err(status::ERR_BAD_HANDLE),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// getPropertySet:effect 属性集 = handle 本体(props 在偏移 0;
|
||||
/// HS:1886-1898 返回 `getProps().getHandle()`,同址)。
|
||||
unsafe extern "C" fn get_property_set(effect: *mut c_void, out: *mut *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
let _ = resolve_effect(effect)?;
|
||||
unsafe { *out = effect };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// getParamSet:param-set 即 effect 本体(HS:1901-1931 描述符/实例
|
||||
/// 各自返回其 param set;本设计两者合一)。
|
||||
unsafe extern "C" fn get_param_set(effect: *mut c_void, out: *mut *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
let _ = resolve_effect(effect)?;
|
||||
unsafe { *out = effect };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// clipDefine(describe 期):定义 clip 并返回其属性集 handle。
|
||||
/// 重复名 → 整体替换(HS: `defineClip` 的 map 覆盖语义,
|
||||
/// ofxhImageEffect.cpp:265-271;旧句柄随之失效,与 HS 一致)。
|
||||
unsafe extern "C" fn clip_define(effect: *mut c_void, name: *const c_char, out: *mut *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
unsafe { *out = std::ptr::null_mut() };
|
||||
let n = unsafe { c_name(name)? };
|
||||
let desc = match resolve_effect(effect)? {
|
||||
EffectRef::Descriptor(d) => d,
|
||||
EffectRef::Instance(_) => return Err(status::ERR_BAD_HANDLE),
|
||||
};
|
||||
let clip = ClipDescriptor::new(n);
|
||||
// 重复定义:替换原 Box(HS map 覆盖语义)。
|
||||
if let Some(existing) = desc.clips.iter_mut().find(|c| c.name == n) {
|
||||
*existing = Box::new(clip);
|
||||
} else {
|
||||
desc.clips.push(Box::new(clip));
|
||||
}
|
||||
// 地址取自已入盒的对象(栈上临时变量在移动后失效)。
|
||||
let clip = desc.clips.iter().find(|c| c.name == n).expect("just stored");
|
||||
let addr = &clip.props as *const _ as usize;
|
||||
unsafe { *out = tag::make(addr as *const PropertySet, tag::CLIP) };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// clipGetHandle(实例期):按名取 clip 及其属性集。未找到 →
|
||||
/// BadHandle(HS:2067-2070)。
|
||||
unsafe extern "C" fn clip_get_handle(
|
||||
effect: *mut c_void,
|
||||
name: *const c_char,
|
||||
clip: *mut *mut c_void,
|
||||
property_set: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if clip.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
unsafe { *clip = std::ptr::null_mut() };
|
||||
let n = unsafe { c_name(name)? };
|
||||
let handle = match resolve_effect(effect)? {
|
||||
EffectRef::Instance(i) => {
|
||||
let c = i.clips.iter().find(|c| c.name == n).ok_or(status::ERR_BAD_HANDLE)?;
|
||||
let addr = &c.props as *const _ as usize;
|
||||
tag::make(addr as *const PropertySet, tag::CLIP)
|
||||
}
|
||||
EffectRef::Descriptor(_) => return Err(status::ERR_BAD_HANDLE),
|
||||
};
|
||||
unsafe { *clip = handle };
|
||||
if !property_set.is_null() {
|
||||
// props 在偏移 0:clip handle 与 props handle 同值。
|
||||
unsafe { *property_set = handle };
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// clipGetPropertySet:clip 属性集 = clip handle 本体。
|
||||
unsafe extern "C" fn clip_get_property_set(clip: *mut c_void, out: *mut *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
let _ = resolve_clip(clip)?;
|
||||
unsafe { *out = clip };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// clipGetImage:抓取输入图像并登记到存活表,返回图像属性集 handle
|
||||
/// (HS:2003-2049;`getImage` 失败 → Failed)。
|
||||
///
|
||||
/// `// TODO(clip)`:fetch_image 待 bridge::render 帧访问 C ABI。
|
||||
unsafe extern "C" fn clip_get_image(
|
||||
clip: *mut c_void,
|
||||
time: c_double,
|
||||
region: *const c_void,
|
||||
out: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
unsafe { *out = std::ptr::null_mut() };
|
||||
let c = resolve_clip(clip)?;
|
||||
// Output clip:返回当前渲染的输出图像(render 驱动经 TLS
|
||||
// 设置;对应 HS 渲染期输出图像挂在 Output clip 上)。
|
||||
if c.name == "Output" {
|
||||
if let Some(image) = crate::suites::current_output() {
|
||||
let addr = &image.props as *const _ as usize;
|
||||
LIVE_IMAGES
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(addr, image);
|
||||
unsafe { *out = tag::make(addr as *const PropertySet, tag::IMAGE) };
|
||||
return Ok(());
|
||||
}
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
// 区域(可空):像素坐标的可选 bounds。
|
||||
let region = if region.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { *(region as *const OfxRectD) })
|
||||
};
|
||||
// 渲染比例取自当前渲染上下文(HS 的 clip 在 render 期从
|
||||
// in_args 拿到 scale;本设计经 TLS,见 suites::RenderCtx)。
|
||||
let scale = crate::suites::render_ctx()
|
||||
.map(|ctx| ctx.scale)
|
||||
.unwrap_or(RenderScale { x: 1.0, y: 1.0 });
|
||||
let image = c.fetch_image(time, scale, region).map_err(|_| status::FAILED)?;
|
||||
let image = std::sync::Arc::new(image);
|
||||
let addr = &image.props as *const _ as usize;
|
||||
LIVE_IMAGES
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(addr, image);
|
||||
unsafe { *out = tag::make(addr as *const PropertySet, tag::IMAGE) };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// clipReleaseImage:摘除存活表强引用(图像随之释放;HS:2053-2068
|
||||
/// 的 releaseReference 配对)。
|
||||
unsafe extern "C" fn clip_release_image(image: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
if image.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
if tag::kind(image) != tag::IMAGE {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
let addr = tag::strip(image) as usize;
|
||||
let mut live = LIVE_IMAGES.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match live.remove(&addr) {
|
||||
Some(_) => Ok(()),
|
||||
None => Err(status::ERR_BAD_HANDLE),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// clipGetRegionOfDefinition:读 clip 实例属性的协商 RoD;缺失/非法
|
||||
/// → Failed(HS:2111-2150;非法判断 x2<x1 || y2<y1 → Failed)。
|
||||
unsafe extern "C" fn clip_get_region_of_definition(
|
||||
clip: *mut c_void,
|
||||
_time: c_double,
|
||||
bounds: *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if bounds.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
let c = resolve_clip(clip)?;
|
||||
let rod = read_rod(&c.props).ok_or(status::FAILED)?;
|
||||
if rod.x2 < rod.x1 || rod.y2 < rod.y1 {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
unsafe { *(bounds as *mut OfxRectD) = rod };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// 从 clip 属性读协商 RoD(kOfxImageEffectPropRegionOfDefinition,
|
||||
/// Double×4;协商时宿主写入)。
|
||||
fn read_rod(props: &PropertySet) -> Option<OfxRectD> {
|
||||
let v = props.get("OfxImageEffectPropRegionOfDefinition", 0)?;
|
||||
let Value::Double(x1) = v else { return None };
|
||||
let Value::Double(y1) = props.get("OfxImageEffectPropRegionOfDefinition", 1)? else {
|
||||
return None;
|
||||
};
|
||||
let Value::Double(x2) = props.get("OfxImageEffectPropRegionOfDefinition", 2)? else {
|
||||
return None;
|
||||
};
|
||||
let Value::Double(y2) = props.get("OfxImageEffectPropRegionOfDefinition", 3)? else {
|
||||
return None;
|
||||
};
|
||||
Some(OfxRectD { x1, y1, x2, y2 })
|
||||
}
|
||||
|
||||
/// abort:实例期 → 当前渲染进度是否已取消(HS:2154-2170 的
|
||||
/// `instance->abort()`;HS 默认 0,本实现按规范返回 REPLY_YES/NO)。
|
||||
/// 取消状态是成功应答而非错误码,不能走 caught 的 Err 通道。
|
||||
unsafe extern "C" fn abort(effect: *mut c_void) -> c_int {
|
||||
catch_unwind(AssertUnwindSafe(|| {
|
||||
match resolve_effect(effect)? {
|
||||
EffectRef::Instance(_) => {}
|
||||
EffectRef::Descriptor(_) => return Err(status::ERR_BAD_HANDLE),
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
.map_or_else(
|
||||
|_| status::FAILED,
|
||||
|r| match r {
|
||||
Ok(()) => {
|
||||
if crate::suites::progress::is_cancelled() {
|
||||
status::REPLY_YES
|
||||
} else {
|
||||
status::REPLY_NO
|
||||
}
|
||||
}
|
||||
Err(c) => c,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// imageMemoryAlloc:账本同 memory suite(HS:2174-2212;失败 →
|
||||
/// ErrMemory)。`handle` 忽略。
|
||||
unsafe extern "C" fn image_memory_alloc(
|
||||
_handle: *mut c_void,
|
||||
byte_size: c_int,
|
||||
out: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
match crate::suites::memory::alloc(byte_size as usize) {
|
||||
Some(ptr) => {
|
||||
unsafe { *out = ptr as *mut c_void };
|
||||
Ok(())
|
||||
}
|
||||
None => Err(status::ERR_MEMORY),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// imageMemoryFree:账本销账;未知指针 → BadHandle(SDK 契约)。
|
||||
unsafe extern "C" fn image_memory_free(memory: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
if crate::suites::memory::free(memory as *mut u8) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(status::ERR_BAD_HANDLE)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// imageMemoryLock/Unlock:HS 语义是"锁住防重分配"(ofxhMemory.cpp
|
||||
/// lock/unlock 计数),第 1 期账本不建模 → OK no-op(文档见
|
||||
/// memory.rs)。
|
||||
unsafe extern "C" fn image_memory_lock(memory: *mut c_void, out: *mut *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
// 账本里的地址即数据指针(分配即就绪,无 HS 的延迟分配)。
|
||||
unsafe { *out = memory };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn image_memory_unlock(_memory: *mut c_void) -> c_int {
|
||||
caught(|| Ok(()))
|
||||
}
|
||||
|
||||
use crate::property::Value;
|
||||
|
||||
/// 静态函数表实例。
|
||||
pub fn suite_v1() -> &'static ImageEffectSuiteV1 {
|
||||
static SUITE: std::sync::OnceLock<ImageEffectSuiteV1> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| ImageEffectSuiteV1 {
|
||||
get_property_set: get_property_set,
|
||||
get_param_set: get_param_set,
|
||||
clip_define: clip_define,
|
||||
clip_get_handle: clip_get_handle,
|
||||
clip_get_property_set: clip_get_property_set,
|
||||
clip_get_image: clip_get_image,
|
||||
clip_release_image: clip_release_image,
|
||||
clip_get_region_of_definition: clip_get_region_of_definition,
|
||||
abort: abort,
|
||||
image_memory_alloc: image_memory_alloc,
|
||||
image_memory_free: image_memory_free,
|
||||
image_memory_lock: image_memory_lock,
|
||||
image_memory_unlock: image_memory_unlock,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::ffi::CString;
|
||||
|
||||
use crate::descriptor::EffectDescriptor;
|
||||
use crate::property::PropertySet;
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
fn descriptor_handle(d: &EffectDescriptor) -> *mut c_void {
|
||||
tag::make(&d.props as *const PropertySet, tag::DESCRIPTOR)
|
||||
}
|
||||
|
||||
/// describe 期:clipDefine 建 clip、属性 suite 读写其属性、
|
||||
/// 重复定义整体替换(HS map 覆盖)。
|
||||
#[test]
|
||||
fn describe_clip_define_and_props() {
|
||||
let mut desc = EffectDescriptor::new();
|
||||
let s = suite_v1();
|
||||
let h = descriptor_handle(&desc);
|
||||
|
||||
let mut clip: *mut c_void = std::ptr::null_mut();
|
||||
let name = cs("Source");
|
||||
unsafe {
|
||||
assert_eq!((s.clip_define)(h, name.as_ptr(), &mut clip), 0);
|
||||
}
|
||||
assert_eq!(tag::kind(clip), tag::CLIP);
|
||||
|
||||
// 属性 suite 直接读写 clip handle(props 在偏移 0)。
|
||||
let ps = crate::suites::property::suite_v1();
|
||||
let label_prop = cs(crate::param::PROP_LABEL);
|
||||
let optional_prop = cs(crate::descriptor::CLIP_OPTIONAL);
|
||||
let label = cs("SourceLabel");
|
||||
unsafe {
|
||||
assert_eq!((ps.set_string)(clip, label_prop.as_ptr(), 0, label.as_ptr()), 0);
|
||||
}
|
||||
let mut out: *mut c_char = std::ptr::null_mut();
|
||||
unsafe {
|
||||
assert_eq!((ps.get_string)(clip, label_prop.as_ptr(), 0, &mut out), 0);
|
||||
assert_eq!(CStr::from_ptr(out).to_bytes(), b"SourceLabel");
|
||||
// 可选性标记:插件写 Optional=1。
|
||||
assert_eq!((ps.set_int)(clip, optional_prop.as_ptr(), 0, 1), 0);
|
||||
}
|
||||
|
||||
// getPropertySet / getParamSet:返回 effect 本体 handle。
|
||||
let mut props: *mut c_void = std::ptr::null_mut();
|
||||
unsafe {
|
||||
assert_eq!((s.get_property_set)(h, &mut props), 0);
|
||||
assert_eq!(props, h);
|
||||
assert_eq!((s.get_param_set)(h, &mut props), 0);
|
||||
assert_eq!(props, h);
|
||||
}
|
||||
|
||||
// 重复 clipDefine:替换(旧句柄失效,HS 一致)。
|
||||
let mut clip2: *mut c_void = std::ptr::null_mut();
|
||||
unsafe {
|
||||
assert_eq!((s.clip_define)(h, name.as_ptr(), &mut clip2), 0);
|
||||
}
|
||||
assert_ne!(clip, clip2);
|
||||
assert_eq!(desc.clips.len(), 1);
|
||||
|
||||
// 空 out / 空 handle → BadHandle。
|
||||
unsafe {
|
||||
assert_eq!((s.clip_define)(h, name.as_ptr(), std::ptr::null_mut()), status::ERR_BAD_HANDLE);
|
||||
assert_eq!((s.clip_define)(std::ptr::null_mut(), name.as_ptr(), &mut clip2), status::ERR_BAD_HANDLE);
|
||||
}
|
||||
}
|
||||
|
||||
/// abort:describe 期 → BadHandle;实例期 → REPLY_NO(未取消)。
|
||||
#[test]
|
||||
fn abort_requires_instance() {
|
||||
let desc = EffectDescriptor::new();
|
||||
let s = suite_v1();
|
||||
let dh = descriptor_handle(&desc);
|
||||
unsafe {
|
||||
assert_eq!((s.abort)(dh), status::ERR_BAD_HANDLE);
|
||||
assert_eq!((s.abort)(std::ptr::null_mut()), status::ERR_BAD_HANDLE);
|
||||
}
|
||||
// 实例期:未取消 → REPLY_NO。
|
||||
let inst = std::sync::Arc::new(crate::instance::Instance {
|
||||
props: PropertySet::new(),
|
||||
plugin: std::sync::Arc::new(crate::host::Plugin {
|
||||
identifier: "t".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::new(),
|
||||
contexts: vec![],
|
||||
descriptor: EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: dummy_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
}),
|
||||
context: "OfxImageEffectContextFilter".into(),
|
||||
params: crate::param::ParamSetInstance { params: vec![] },
|
||||
clips: vec![],
|
||||
node_identity: std::sync::atomic::AtomicUsize::new(0),
|
||||
destroyed: std::sync::atomic::AtomicBool::new(false),
|
||||
sequence_range: std::sync::Mutex::new(None),
|
||||
progress_cb: std::sync::Mutex::new(None),
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
});
|
||||
let ih = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
|
||||
unsafe {
|
||||
assert_eq!((s.abort)(ih), status::REPLY_NO);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn dummy_entry(
|
||||
_: *const c_char,
|
||||
_: *const c_void,
|
||||
_: *mut c_void,
|
||||
_: *mut c_void,
|
||||
) -> c_int {
|
||||
status::OK
|
||||
}
|
||||
|
||||
/// imageMemoryAlloc/Free 走 memory 账本。
|
||||
#[test]
|
||||
fn image_memory_ledger() {
|
||||
let s = suite_v1();
|
||||
let mut mem: *mut c_void = std::ptr::null_mut();
|
||||
unsafe {
|
||||
assert_eq!((s.image_memory_alloc)(std::ptr::null_mut(), 1024, &mut mem), 0);
|
||||
assert!(!mem.is_null());
|
||||
// lock 返回数据指针(第 1 期 = 句柄本身)。
|
||||
let mut ptr: *mut c_void = std::ptr::null_mut();
|
||||
assert_eq!((s.image_memory_lock)(mem, &mut ptr), 0);
|
||||
assert_eq!(ptr, mem);
|
||||
assert_eq!((s.image_memory_unlock)(mem), 0);
|
||||
// 未知指针 free → BadHandle。
|
||||
assert_eq!((s.image_memory_free)(0xdeadbeef as *mut c_void), status::ERR_BAD_HANDLE);
|
||||
assert_eq!((s.image_memory_free)(mem), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! OfxMemorySuite v1:宿主代分配内存。
|
||||
//!
|
||||
//! 语义参照 HS: ofxhHost.cpp:26-42(裸 malloc/free 语义:失败 →
|
||||
//! kOfxStatErrMemory,free 恒 OK)——但本 crate 按骨架声明增加
|
||||
//! **宿主级账本**:分配记录进账本,destroyInstance 时兜底回收
|
||||
//! (泄漏防线;插件正常应自行 free)。未知指针 free →
|
||||
//! kOfxStatErrBadHandle(SDK ofxMemory.h:51 的契约)。
|
||||
//! image_effect suite 的 imageMemory* 与此共用账本。
|
||||
|
||||
use std::alloc::Layout;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::suites::status;
|
||||
|
||||
/// 对齐:malloc 保证 max_align_t(本平台 16 字节);插件会把缓冲当
|
||||
/// float*/SIMD 用,16 字节是安全下限。
|
||||
const ALIGN: usize = 16;
|
||||
|
||||
/// 一个已分配块:地址 + 布局(回收时按记录布局 dealloc)。
|
||||
/// 地址以 `usize` 存(不透明令牌,避免裸指针破坏 `static` 的
|
||||
/// Send/Sync 推导;账本只做 alloc/dealloc 配对,从不解引用)。
|
||||
struct Block {
|
||||
ptr: usize,
|
||||
layout: Layout,
|
||||
}
|
||||
|
||||
/// 宿主级账本(进程单例;memory suite 无宿主指针参数,全局即可)。
|
||||
static LEDGER: Mutex<Vec<Block>> = Mutex::new(Vec::new());
|
||||
|
||||
/// 取锁(毒锁接管:一次持锁 panic 不级联)。
|
||||
fn lock() -> std::sync::MutexGuard<'static, Vec<Block>> {
|
||||
LEDGER.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// 分配并记账。返回裸指针;失败(OOM/布局非法)返回 `None`。
|
||||
pub(crate) fn alloc(size: usize) -> Option<*mut u8> {
|
||||
// 零尺寸分配:Layout 要求非零,取 1 字节兜底(free 按同布局回收)。
|
||||
let layout = Layout::from_size_align(size.max(1), ALIGN).ok()?;
|
||||
let ptr = unsafe { std::alloc::alloc(layout) };
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
lock().push(Block {
|
||||
ptr: ptr as usize,
|
||||
layout,
|
||||
});
|
||||
Some(ptr)
|
||||
}
|
||||
|
||||
/// 按指针释放并销账。未知指针返回 `false`(suite 层映射
|
||||
/// kOfxStatErrBadHandle);`NULL` 按 C 语义 no-op 返回 `true`。
|
||||
pub(crate) fn free(ptr: *mut u8) -> bool {
|
||||
if ptr.is_null() {
|
||||
return true;
|
||||
}
|
||||
let mut ledger = lock();
|
||||
let pos = ledger.iter().position(|b| b.ptr == ptr as usize);
|
||||
match pos {
|
||||
Some(i) => {
|
||||
let b = ledger.remove(i);
|
||||
unsafe { std::alloc::dealloc(b.ptr as *mut u8, b.layout) };
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 兜底回收全部在账块(destroyInstance 的泄漏防线)。返回回收块数
|
||||
/// (泄漏断言用)。
|
||||
pub(crate) fn sweep_leaked() -> usize {
|
||||
let mut ledger = lock();
|
||||
let n = ledger.len();
|
||||
for b in ledger.drain(..) {
|
||||
unsafe { std::alloc::dealloc(b.ptr as *mut u8, b.layout) };
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// 函数表布局(与 SDK `OfxMemorySuiteV1` 一致;`size_t` 在本平台
|
||||
/// 与 `usize` 同宽,stable Rust 用 `usize` 表达——骨架的 `c_size_t`
|
||||
/// 是不稳定特性,弃用)。
|
||||
#[repr(C)]
|
||||
pub struct MemorySuiteV1 {
|
||||
/// memoryAlloc:分配 `size` 字节,写 `*out`。
|
||||
pub alloc: unsafe extern "C" fn(*mut c_void, usize, *mut *mut c_void) -> c_int,
|
||||
/// memoryFree:释放;NULL 是 no-op。
|
||||
pub free: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
}
|
||||
|
||||
use std::ffi::{c_int, c_void};
|
||||
|
||||
/// memoryAlloc 实现:`handle` 忽略(HS 同,ofxhHost.cpp:27)。
|
||||
///
|
||||
/// `# Safety`:`out` 必须是可写指针(插件契约)。
|
||||
unsafe extern "C" fn memory_alloc(
|
||||
_handle: *mut c_void,
|
||||
byte_size: usize,
|
||||
out: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
if out.is_null() {
|
||||
return status::ERR_VALUE;
|
||||
}
|
||||
match alloc(byte_size) {
|
||||
Some(ptr) => {
|
||||
unsafe { *out = ptr as *mut c_void };
|
||||
status::OK
|
||||
}
|
||||
// 与 HS 一致:分配失败 → kOfxStatErrMemory(ofxhHost.cpp:35)。
|
||||
None => status::ERR_MEMORY,
|
||||
}
|
||||
}))
|
||||
.unwrap_or(status::FAILED)
|
||||
}
|
||||
|
||||
/// memoryFree 实现:未知指针 → kOfxStatErrBadHandle(SDK 契约);
|
||||
/// NULL 按 C 语义 no-op → OK(HS 的 free(NULL) 行为,ofxhHost.cpp:40)。
|
||||
unsafe extern "C" fn memory_free(data: *mut c_void) -> c_int {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
if free(data as *mut u8) {
|
||||
status::OK
|
||||
} else {
|
||||
status::ERR_BAD_HANDLE
|
||||
}
|
||||
}))
|
||||
.unwrap_or(status::FAILED)
|
||||
}
|
||||
|
||||
/// 静态函数表实例。
|
||||
pub fn suite_v1() -> &'static MemorySuiteV1 {
|
||||
static SUITE: std::sync::OnceLock<MemorySuiteV1> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| MemorySuiteV1 {
|
||||
alloc: memory_alloc,
|
||||
free: memory_free,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! OfxMessageSuite v1/v2:插件消息 → 宿主日志。
|
||||
//!
|
||||
//! stable Rust 无法定义 C-variadic 函数(c_variadic 仍不稳定),
|
||||
//! v1 的 `...` 与 v2 的 va_list 入口由 C shim 承载
|
||||
//! (cbits/ofx_message_shim.c,build.rs 经 `cc` 编译,符号随
|
||||
//! staticlib 进入 liboakplugin):C 侧 vsnprintf 成定长缓冲后转发
|
||||
//! 到本模块的 `oak_ofx_message_impl`。
|
||||
//!
|
||||
//! 消息出口 = facade 注册的 `oakplugin_message_fn`
|
||||
//! (include/plugin/host.h)。注意:骨架 ffi.rs 声明的 `MessageFn`
|
||||
//! 与头文件不符(头文件是 `(type, message, userdata)`,骨架是
|
||||
//! `(userdata, level, message)`)——做 ffi.rs 时以头文件为准修正。
|
||||
//! 本模块按头文件契约建模。
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void, CStr};
|
||||
|
||||
use crate::suites::status;
|
||||
|
||||
/// facade 消息回调(include/plugin/host.h `oakplugin_message_fn`):
|
||||
/// `(type, message, userdata) -> OAKPLUGIN_MESSAGE_ANSWER_NO/YES`。
|
||||
pub(crate) type MessageHandler =
|
||||
unsafe extern "C" fn(*const c_char, *const c_char, *mut c_void) -> c_int;
|
||||
|
||||
/// 消息出口注册表(ffi 层经 `oakplugin_host_set_message_handler`
|
||||
/// 写入,suite 读;userdata 以 usize 存,避免裸指针破坏 static 的
|
||||
/// Send/Sync 推导)。
|
||||
static HANDLER: std::sync::Mutex<(Option<MessageHandler>, usize)> =
|
||||
std::sync::Mutex::new((None, 0));
|
||||
|
||||
/// 注册/注销消息出口(ffi 层 `oakplugin_host_set_message_handler`
|
||||
/// 调用;公开:测试直接注入捕获器)。
|
||||
pub fn set_handler(f: Option<MessageHandler>, userdata: *mut c_void) {
|
||||
let mut h = HANDLER.lock().unwrap_or_else(|e| e.into_inner());
|
||||
*h = (f, userdata as usize);
|
||||
}
|
||||
|
||||
/// 读取当前出口。
|
||||
fn handler() -> (Option<MessageHandler>, *mut c_void) {
|
||||
let h = HANDLER.lock().unwrap_or_else(|e| e.into_inner());
|
||||
(h.0, h.1 as *mut c_void)
|
||||
}
|
||||
|
||||
/// kOfxMessageQuestion(ofxMessage.h:61):无出口时答"否"。
|
||||
const K_MESSAGE_QUESTION: &[u8] = b"OfxMessageQuestion";
|
||||
|
||||
/// 消息类型是否为 question。
|
||||
fn is_question(type_: *const c_char) -> bool {
|
||||
// 空指针已在调用点拦截;此处仍防御(CStr::from_ptr 的调用方义务)。
|
||||
if type_.is_null() {
|
||||
return false;
|
||||
}
|
||||
unsafe { CStr::from_ptr(type_) }.to_bytes() == K_MESSAGE_QUESTION
|
||||
}
|
||||
|
||||
/// Rust 侧实现(C shim 转发至此;`#[no_mangle]` 保证符号名与 C 侧
|
||||
/// 引用一致)。语义逐条对照 C++:
|
||||
/// - type/message 为空 → kOfxStatFailed(olivehost.cpp:267,vmessage
|
||||
/// 对 !type || !format 的处理;format 为空的 case 由 shim 以
|
||||
/// NULL message 转发);
|
||||
/// - 有出口 → 转发 `(type, message, userdata)`,返回按头文件契约
|
||||
/// 映射为 REPLY_YES/REPLY_NO(olivehost.cpp:277-279 的 handler
|
||||
/// 路径;非 question 消息插件忽略返回值);
|
||||
/// - 无出口 → stderr 日志(镜像 olivehost.cpp:282 的 fprintf 默认),
|
||||
/// question 答"否"(olivehost.cpp:283-284)。
|
||||
///
|
||||
/// `# Safety`:`type`/`message` 必须是有效 C 字符串(插件契约,
|
||||
/// shim 已保证 message 非悬垂——指向其栈缓冲,调用期间有效)。
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oak_ofx_message_impl(
|
||||
_handle: *mut c_void,
|
||||
type_: *const c_char,
|
||||
_id: *const c_char,
|
||||
message: *const c_char,
|
||||
) -> c_int {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
if type_.is_null() || message.is_null() {
|
||||
return status::FAILED;
|
||||
}
|
||||
let (handler, userdata) = handler();
|
||||
if let Some(h) = handler {
|
||||
// 头文件契约:返回值 0/1 即答复 NO/YES。
|
||||
let answer = unsafe { h(type_, message, userdata) };
|
||||
return if answer != 0 {
|
||||
status::REPLY_YES
|
||||
} else {
|
||||
status::REPLY_NO
|
||||
};
|
||||
}
|
||||
// Headless 默认:stderr(C++ fprintf 的 Rust 等价物)。
|
||||
eprintln!("OFX message: {}", unsafe { CStr::from_ptr(message) }.to_string_lossy());
|
||||
if is_question(type_) {
|
||||
status::REPLY_NO
|
||||
} else {
|
||||
status::OK
|
||||
}
|
||||
}))
|
||||
.unwrap_or(status::FAILED)
|
||||
}
|
||||
|
||||
/// 函数表布局(OfxMessageSuiteV1)。
|
||||
#[repr(C)]
|
||||
pub struct MessageSuiteV1 {
|
||||
/// message(变长参数版;入口在 C shim)
|
||||
pub message: unsafe extern "C" fn(
|
||||
*mut c_void,
|
||||
*const c_char,
|
||||
*const c_char,
|
||||
*const c_char,
|
||||
...
|
||||
) -> c_int,
|
||||
}
|
||||
|
||||
/// 函数表布局(OfxMessageSuiteV2:第 5 参为平台相关 `va_list`)。
|
||||
///
|
||||
/// 真实 ABI 是 C 的 va_list(Apple arm64 上是结构体按值传递);
|
||||
/// 声明为不透明指针仅作占位——本表只被插件→C shim 调用,Rust 侧
|
||||
/// 永不直接调用,指针类型不参与任何实际调用。
|
||||
#[repr(C)]
|
||||
pub struct MessageSuiteV2 {
|
||||
/// message(va_list 版;入口在 C shim)
|
||||
pub message: unsafe extern "C" fn(
|
||||
*mut c_void,
|
||||
*const c_char,
|
||||
*const c_char,
|
||||
*const c_char,
|
||||
*mut c_void,
|
||||
) -> c_int,
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
/// C shim 的 v1 入口(cbits/ofx_message_shim.c)。
|
||||
fn ofx_message_shim_v1(
|
||||
handle: *mut c_void,
|
||||
type_: *const c_char,
|
||||
id: *const c_char,
|
||||
format: *const c_char,
|
||||
...
|
||||
) -> c_int;
|
||||
/// C shim 的 v2 入口(va_list 已由 C 侧消费,Rust 只见不透明值)。
|
||||
fn ofx_message_shim_v2(
|
||||
handle: *mut c_void,
|
||||
type_: *const c_char,
|
||||
id: *const c_char,
|
||||
format: *const c_char,
|
||||
args: *mut c_void,
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
/// 静态函数表实例(v1)。
|
||||
pub fn suite_v1() -> &'static MessageSuiteV1 {
|
||||
static SUITE: std::sync::OnceLock<MessageSuiteV1> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| MessageSuiteV1 {
|
||||
message: ofx_message_shim_v1,
|
||||
})
|
||||
}
|
||||
|
||||
/// 静态函数表实例(v2)。
|
||||
pub fn suite_v2() -> &'static MessageSuiteV2 {
|
||||
static SUITE: std::sync::OnceLock<MessageSuiteV2> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| MessageSuiteV2 {
|
||||
message: ofx_message_shim_v2,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::ffi::CString;
|
||||
|
||||
/// 测试共享全局 HANDLER,cargo 默认并行执行——串行化契约用例。
|
||||
static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
/// 捕获型出口:把 (type, message) 记进 userdata 指向的 Vec。
|
||||
unsafe extern "C" fn capture(
|
||||
type_: *const c_char,
|
||||
message: *const c_char,
|
||||
userdata: *mut c_void,
|
||||
) -> c_int {
|
||||
let v = unsafe { &mut *(userdata as *mut Vec<(String, String)>) };
|
||||
v.push((
|
||||
unsafe { CStr::from_ptr(type_) }.to_string_lossy().into_owned(),
|
||||
unsafe { CStr::from_ptr(message) }.to_string_lossy().into_owned(),
|
||||
));
|
||||
1
|
||||
}
|
||||
|
||||
/// v1 入口:`...` 在 C shim 侧 vsnprintf 后转发,格式化正确。
|
||||
#[test]
|
||||
fn shim_v1_formats_and_forwards() {
|
||||
let _g = TEST_LOCK.lock().unwrap();
|
||||
let mut captured: Vec<(String, String)> = Vec::new();
|
||||
set_handler(Some(capture), &mut captured as *mut _ as *mut c_void);
|
||||
|
||||
let s = suite_v1();
|
||||
let type_ = CString::new("OfxMessageError").unwrap();
|
||||
let id = CString::new("test-id").unwrap();
|
||||
let fmt = CString::new("value=%d name=%s").unwrap();
|
||||
let name = CString::new("oak").unwrap();
|
||||
let r = unsafe {
|
||||
(s.message)(
|
||||
std::ptr::null_mut(),
|
||||
type_.as_ptr(),
|
||||
id.as_ptr(),
|
||||
fmt.as_ptr(),
|
||||
42,
|
||||
name.as_ptr(),
|
||||
)
|
||||
};
|
||||
|
||||
// 出口答 YES → REPLY_YES。
|
||||
assert_eq!(r, status::REPLY_YES);
|
||||
assert_eq!(captured.len(), 1);
|
||||
assert_eq!(captured[0].0, "OfxMessageError");
|
||||
assert_eq!(captured[0].1, "value=42 name=oak");
|
||||
|
||||
set_handler(None, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
/// v1 入口:NULL format → kOfxStatFailed(C++ olivehost.cpp:267)。
|
||||
#[test]
|
||||
fn shim_v1_null_format_fails() {
|
||||
let _g = TEST_LOCK.lock().unwrap();
|
||||
let s = suite_v1();
|
||||
let type_ = CString::new("OfxMessageError").unwrap();
|
||||
let id = CString::new("id").unwrap();
|
||||
let r = unsafe {
|
||||
(s.message)(
|
||||
std::ptr::null_mut(),
|
||||
type_.as_ptr(),
|
||||
id.as_ptr(),
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
assert_eq!(r, status::FAILED);
|
||||
}
|
||||
|
||||
/// v2 入口:表字段非空且指向 C shim 符号(va_list 路径无法从
|
||||
/// Rust 构造 va_list 调用,链的其余部分与 v1 共用 forward)。
|
||||
#[test]
|
||||
fn shim_v2_table_entry_resolves() {
|
||||
let _g = TEST_LOCK.lock().unwrap();
|
||||
let s = suite_v2();
|
||||
assert!(!std::ptr::eq(s.message as *const (), ofx_message_shim_v1 as *const ()));
|
||||
}
|
||||
|
||||
/// 无出口(headless 默认):非 question → OK;question → REPLY_NO。
|
||||
#[test]
|
||||
fn no_handler_headless_defaults() {
|
||||
let _g = TEST_LOCK.lock().unwrap();
|
||||
set_handler(None, std::ptr::null_mut());
|
||||
let s = suite_v1();
|
||||
let type_ = CString::new("OfxMessageError").unwrap();
|
||||
let id = CString::new("id").unwrap();
|
||||
let fmt = CString::new("hello %s").unwrap();
|
||||
let arg = CString::new("x").unwrap();
|
||||
let r = unsafe {
|
||||
(s.message)(
|
||||
std::ptr::null_mut(),
|
||||
type_.as_ptr(),
|
||||
id.as_ptr(),
|
||||
fmt.as_ptr(),
|
||||
arg.as_ptr(),
|
||||
)
|
||||
};
|
||||
assert_eq!(r, status::OK);
|
||||
|
||||
let q = CString::new("OfxMessageQuestion").unwrap();
|
||||
let r = unsafe {
|
||||
(s.message)(
|
||||
std::ptr::null_mut(),
|
||||
q.as_ptr(),
|
||||
id.as_ptr(),
|
||||
fmt.as_ptr(),
|
||||
arg.as_ptr(),
|
||||
)
|
||||
};
|
||||
assert_eq!(r, status::REPLY_NO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! suite 层:插件调进来的 C 函数表(unsafe trampoline 集中区)。
|
||||
//!
|
||||
//! 每张 suite 是一个 `#[repr(C)]` 的函数表静态实例;插件经
|
||||
//! [`fetch_suite`] 取得。所有入口的安全模式相同:
|
||||
//!
|
||||
//! 1. 解 handle(`RefBox`/`PropertySet` 指针),空指针 →
|
||||
//! kOfxStatErrBadHandle;
|
||||
//! 2. 调 safe Rust 实现;
|
||||
//! 3. `catch_unwind` 兜底 → kOfxStatFailed。
|
||||
//!
|
||||
//! 参照:HS: ofxhImageEffect.cpp:2776(fetchSuite 分发表与版本协商)。
|
||||
|
||||
pub mod gl_render;
|
||||
pub mod image_effect;
|
||||
pub mod memory;
|
||||
pub mod message;
|
||||
pub mod multithread;
|
||||
pub mod param;
|
||||
pub mod progress;
|
||||
pub mod property;
|
||||
pub mod timeline;
|
||||
|
||||
/// OFX 状态码(全表与 SDK ofxCore.h:895-953 逐字一致;插件对状态码
|
||||
/// 做精确比较,任何偏差都是静默兼容性破坏)。
|
||||
pub mod status {
|
||||
/// kOfxStatOK:成功。
|
||||
pub const OK: i32 = 0;
|
||||
/// kOfxStatFailed:通用失败。
|
||||
pub const FAILED: i32 = 1;
|
||||
/// kOfxStatErrFatal:致命错误。
|
||||
pub const ERR_FATAL: i32 = 2;
|
||||
/// kOfxStatErrUnknown:未知对象/属性。
|
||||
pub const ERR_UNKNOWN: i32 = 3;
|
||||
/// kOfxStatErrMissingHostFeature:宿主缺功能。
|
||||
pub const ERR_MISSING_HOST_FEATURE: i32 = 4;
|
||||
/// kOfxStatErrUnsupported:不支持的操作。
|
||||
pub const ERR_UNSUPPORTED: i32 = 5;
|
||||
/// kOfxStatErrExists:对象已存在。
|
||||
pub const ERR_EXISTS: i32 = 6;
|
||||
/// kOfxStatErrFormat:格式错误。
|
||||
pub const ERR_FORMAT: i32 = 7;
|
||||
/// kOfxStatErrMemory:内存不足。
|
||||
pub const ERR_MEMORY: i32 = 8;
|
||||
/// kOfxStatErrBadHandle:非法 handle(含 NULL)。
|
||||
pub const ERR_BAD_HANDLE: i32 = 9;
|
||||
/// kOfxStatErrBadIndex:索引越界。
|
||||
pub const ERR_BAD_INDEX: i32 = 10;
|
||||
/// kOfxStatErrValue:非法值。
|
||||
pub const ERR_VALUE: i32 = 11;
|
||||
/// kOfxStatReplyYes。
|
||||
pub const REPLY_YES: i32 = 12;
|
||||
/// kOfxStatReplyNo。
|
||||
pub const REPLY_NO: i32 = 13;
|
||||
/// kOfxStatReplyDefault。
|
||||
pub const REPLY_DEFAULT: i32 = 14;
|
||||
}
|
||||
|
||||
/// 句柄约定:发给插件的每个 OFX 句柄 = 对象 `props` 字段地址 | 低
|
||||
/// 3 位标签。
|
||||
///
|
||||
/// 前置条件(本 crate 内部纪律,宿主创建对象时遵守):
|
||||
/// - 对象 `#[repr(C)]` 且 `props: PropertySet` 在**偏移 0**——句柄的
|
||||
/// 地址就是属性集地址,与 C++ "基类在偏移 0"(HS 里
|
||||
/// OfxPropertySetHandle 即 Property::Set*)同构,属性 suite 无需
|
||||
/// 反查即可直接解引用;
|
||||
/// - 对象堆上稳定:`Arc<RefBox<T>>` / 结构体内的 `Box<T>` 元素
|
||||
/// (Vec 重分配只移动 Box 指针,不移动负载)。
|
||||
///
|
||||
/// 标签位编码:低 3 位(含 Mutex/Arc 的对象对齐 ≥8,低 3 位恒 0)。
|
||||
pub mod tag {
|
||||
/// 标签掩码(低 3 位)。
|
||||
pub const MASK: usize = 0b111;
|
||||
/// 裸属性集(宿主内部直用 / 测试直传 `&props`)。
|
||||
pub const PROPERTY_SET: usize = 0;
|
||||
/// [`crate::descriptor::EffectDescriptor`](describe 期 effect/
|
||||
/// param-set handle)。
|
||||
pub const DESCRIPTOR: usize = 1;
|
||||
/// [`crate::instance::Instance`](实例期 effect/param-set handle)。
|
||||
pub const INSTANCE: usize = 2;
|
||||
/// [`crate::param::ParamDef`](describe 期 param handle)。
|
||||
pub const PARAM_DEF: usize = 3;
|
||||
/// [`crate::param::ParamInstance`](实例期 param handle)。
|
||||
pub const PARAM_INSTANCE: usize = 4;
|
||||
/// [`crate::clip::ClipInstance`](实例期 clip handle;describe 期
|
||||
/// [`crate::descriptor::ClipDescriptor`] 共用此标签——两类都只在
|
||||
/// 各自的阶段被使用,属性 suite 不区分)。
|
||||
pub const CLIP: usize = 5;
|
||||
/// [`crate::image::Image`](clipGetImage 的产物)。
|
||||
pub const IMAGE: usize = 6;
|
||||
|
||||
/// 打标签(宿主创建对象句柄用;公开供宿主/测试构造句柄)。
|
||||
pub fn make(
|
||||
props: *const crate::property::PropertySet,
|
||||
t: usize,
|
||||
) -> *mut std::ffi::c_void {
|
||||
(props as usize | t) as *mut std::ffi::c_void
|
||||
}
|
||||
|
||||
/// 剥标签,取 props 指针(标签 0 的裸指针原样返回)。
|
||||
pub fn strip(handle: *mut std::ffi::c_void) -> *const crate::property::PropertySet {
|
||||
((handle as usize) & !MASK) as *const crate::property::PropertySet
|
||||
}
|
||||
|
||||
/// 读标签。
|
||||
pub fn kind(handle: *mut std::ffi::c_void) -> usize {
|
||||
(handle as usize) & MASK
|
||||
}
|
||||
}
|
||||
|
||||
/// 渲染上下文(TLS):render 驱动在调用插件 action 前设置,suite
|
||||
/// 回调(timeline、clipGetImage 的 scale、进度)从中读取;对应 HS
|
||||
/// 中实例在渲染期的状态(ofxhImageEffect.cpp Render in_args 的子集)。
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RenderCtx {
|
||||
/// 当前渲染时间(秒)。
|
||||
pub time: f64,
|
||||
/// 渲染比例。
|
||||
pub scale: crate::instance::RenderScale,
|
||||
/// 项目时间域(timeline getTimeBounds)。
|
||||
pub range: crate::instance::OfxRangeD,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static RENDER_CTX: std::cell::RefCell<Option<RenderCtx>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// 设置/清除渲染上下文(render 驱动调用;action 返回前必须清除;
|
||||
/// 公开:render 驱动在 host 层,测试也直接注入)。
|
||||
pub fn set_render_ctx(ctx: Option<RenderCtx>) {
|
||||
RENDER_CTX.with(|c| *c.borrow_mut() = ctx);
|
||||
}
|
||||
|
||||
/// 读取渲染上下文(无上下文返回 None;无 TLS 时插件时间查询得到
|
||||
/// 0 的 headless 默认)。
|
||||
pub(crate) fn render_ctx() -> Option<RenderCtx> {
|
||||
RENDER_CTX.with(|c| *c.borrow())
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// 当前渲染的输出图像(render 驱动设置;image effect suite 的
|
||||
/// clipGetImage 对 Output clip 返回它)。
|
||||
static CURRENT_OUTPUT: std::cell::RefCell<Option<std::sync::Arc<crate::image::Image>>> =
|
||||
const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// 设置/清除当前输出图像(render 驱动调用;action 返回前必须清除)。
|
||||
pub(crate) fn set_current_output(image: Option<std::sync::Arc<crate::image::Image>>) {
|
||||
CURRENT_OUTPUT.with(|c| *c.borrow_mut() = image);
|
||||
}
|
||||
|
||||
/// 读取当前输出图像。
|
||||
pub(crate) fn current_output() -> Option<std::sync::Arc<crate::image::Image>> {
|
||||
CURRENT_OUTPUT.with(|c| c.borrow().clone())
|
||||
}
|
||||
|
||||
/// GL 渲染上下文(TLS):GL render 驱动在调用插件 action 前设置,
|
||||
/// OpenGLRender suite 的 clipLoadTexture 从中取当前渲染器与输出
|
||||
/// 纹理(对应 C++ 里实例渲染期的 GL 状态;ofxGPURender.h
|
||||
/// "OpenGL Current Context" 一节要求宿主在 Render 期间持有 GL
|
||||
/// 上下文——本设计的约定是调用方(oakrender 的 PluginJob 路径)在
|
||||
/// 进入 render_job 前已把渲染器上下文置为 current,本表只传递句柄)。
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GlCtx {
|
||||
/// 当前渲染器(oakrender 句柄)。
|
||||
pub renderer: crate::bridge::render::RendererHandle,
|
||||
/// 已附着的输出纹理(渲染目标;GL 模式下插件把结果画进它)。
|
||||
pub output_texture: crate::bridge::render::TextureHandle,
|
||||
/// 当前 GL 纹理的实际像素深度(kOfxBitDepth* 静态串;Phase 2
|
||||
/// 全链路 F32,由 render_gl 按插件 kOfxOpenGLPropPixelDepth 协商
|
||||
/// 后填入——纹理句柄的 kOfxImageEffectPropPixelDepth 以它为准)。
|
||||
pub gl_pixel_depth: &'static str,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static GL_CTX: std::cell::RefCell<Option<GlCtx>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// 设置/清除 GL 渲染上下文(GL render 驱动调用;action 返回前必须
|
||||
/// 清除;公开:测试直接注入)。
|
||||
pub fn set_gl_ctx(ctx: Option<GlCtx>) {
|
||||
GL_CTX.with(|c| *c.borrow_mut() = ctx);
|
||||
}
|
||||
|
||||
/// 读取 GL 渲染上下文(无上下文返回 None——clipLoadTexture 在非
|
||||
/// GL 渲染期调用时按规范返回 kOfxStatErrMissingHostFeature)。
|
||||
pub(crate) fn gl_ctx() -> Option<GlCtx> {
|
||||
GL_CTX.with(|c| *c.borrow())
|
||||
}
|
||||
|
||||
/// 宿主进程身份(fetchSuite 的 version 检查用;= OFX API 1.5)。
|
||||
pub(crate) const OFX_API_VERSION: i32 = 105;
|
||||
|
||||
/// fetchSuite 宿主入口:按名字与版本返回函数表指针;不认识或版本
|
||||
/// 不符返回 `None`(FFI 层转 NULL)。
|
||||
///
|
||||
/// 第 1 期注册:OfxPropertySuite v1、OfxMemorySuite v1、
|
||||
/// OfxImageEffectSuite v1、OfxParameterSuite v1、OfxMessageSuite
|
||||
/// v1/v2、OfxProgressSuite v1/v2、OfxTimeLineSuite v1、
|
||||
/// OfxMultiThreadSuite v1。
|
||||
/// 第 2 期追加:OfxImageEffectOpenGLRenderSuite v1(GL 路径);
|
||||
/// ofxColour 无 suite 表(纯属性 + GetOutputColourspace action)。
|
||||
pub fn fetch_suite(name: &str, version: i32) -> Option<*const std::ffi::c_void> {
|
||||
let suite: *const std::ffi::c_void = match (name, version) {
|
||||
("OfxPropertySuite", 1) => ptr(property::suite_v1()),
|
||||
("OfxMemorySuite", 1) => ptr(memory::suite_v1()),
|
||||
("OfxImageEffectSuite", 1) => ptr(image_effect::suite_v1()),
|
||||
("OfxParameterSuite", 1) => ptr(param::suite_v1()),
|
||||
("OfxMessageSuite", 1) => ptr(message::suite_v1()),
|
||||
("OfxMessageSuite", 2) => ptr(message::suite_v2()),
|
||||
("OfxProgressSuite", 1) => ptr(progress::suite_v1()),
|
||||
("OfxProgressSuite", 2) => ptr(progress::suite_v2()),
|
||||
("OfxTimeLineSuite", 1) => ptr(timeline::suite_v1()),
|
||||
("OfxMultiThreadSuite", 1) => ptr(multithread::suite_v1()),
|
||||
("OfxImageEffectOpenGLRenderSuite", 1) => ptr(gl_render::suite_v1()),
|
||||
_ => return None,
|
||||
};
|
||||
Some(suite)
|
||||
}
|
||||
|
||||
/// 表引用 → 不透明指针(fetch_suite 的统一出口)。
|
||||
fn ptr<T>(p: &'static T) -> *const std::ffi::c_void {
|
||||
p as *const T as *const std::ffi::c_void
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 八张 suite 的分发表:版本精确匹配、未知版本/名字 → None。
|
||||
#[test]
|
||||
fn fetch_suite_dispatch() {
|
||||
assert!(fetch_suite("OfxPropertySuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxMemorySuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxImageEffectSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxParameterSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxMessageSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxMessageSuite", 2).is_some());
|
||||
assert!(fetch_suite("OfxProgressSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxProgressSuite", 2).is_some());
|
||||
assert!(fetch_suite("OfxTimeLineSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxMultiThreadSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxImageEffectOpenGLRenderSuite", 1).is_some());
|
||||
|
||||
assert!(fetch_suite("OfxPropertySuite", 2).is_none());
|
||||
assert!(fetch_suite("OfxMessageSuite", 3).is_none());
|
||||
assert!(fetch_suite("OfxImageEffectOpenGLRenderSuite", 2).is_none());
|
||||
assert!(fetch_suite("OfxBogusSuite", 1).is_none());
|
||||
assert!(fetch_suite("", 1).is_none());
|
||||
}
|
||||
|
||||
/// 句柄标签:打标/剥标/读 kind 往返;对齐地址低位为 0。
|
||||
#[test]
|
||||
fn handle_tag_roundtrip() {
|
||||
let set = crate::property::PropertySet::new();
|
||||
let props = &set as *const crate::property::PropertySet;
|
||||
assert_eq!(props as usize & tag::MASK, 0, "PropertySet 必须 ≥8 对齐");
|
||||
for t in [
|
||||
tag::PROPERTY_SET,
|
||||
tag::DESCRIPTOR,
|
||||
tag::INSTANCE,
|
||||
tag::PARAM_DEF,
|
||||
tag::PARAM_INSTANCE,
|
||||
tag::CLIP,
|
||||
tag::IMAGE,
|
||||
] {
|
||||
let h = tag::make(props, t);
|
||||
assert_eq!(tag::kind(h), t);
|
||||
assert_eq!(tag::strip(h) as usize, props as usize);
|
||||
}
|
||||
}
|
||||
|
||||
/// 渲染上下文 TLS:设置/读取/清除。
|
||||
#[test]
|
||||
fn render_ctx_tls() {
|
||||
assert!(render_ctx().is_none());
|
||||
let ctx = RenderCtx {
|
||||
time: 1.5,
|
||||
scale: crate::instance::RenderScale { x: 2.0, y: 2.0 },
|
||||
range: crate::instance::OfxRangeD { min: 0.0, max: 100.0 },
|
||||
};
|
||||
set_render_ctx(Some(ctx));
|
||||
let got = render_ctx().unwrap();
|
||||
assert_eq!(got.time, 1.5);
|
||||
assert_eq!(got.scale.x, 2.0);
|
||||
assert_eq!(got.range.max, 100.0);
|
||||
set_render_ctx(None);
|
||||
assert!(render_ctx().is_none());
|
||||
}
|
||||
|
||||
/// GL 上下文 TLS:设置/读取/清除(clipLoadTexture 的渲染期取值)。
|
||||
#[test]
|
||||
fn gl_ctx_tls() {
|
||||
assert!(gl_ctx().is_none());
|
||||
let renderer = crate::handle::CHandle::null();
|
||||
let tex = crate::handle::CHandle::null();
|
||||
set_gl_ctx(Some(GlCtx {
|
||||
renderer,
|
||||
output_texture: tex,
|
||||
gl_pixel_depth: "OfxBitDepthFloat",
|
||||
}));
|
||||
let got = gl_ctx().unwrap();
|
||||
assert!(got.renderer.is_null());
|
||||
assert!(got.output_texture.is_null());
|
||||
assert_eq!(got.gl_pixel_depth, "OfxBitDepthFloat");
|
||||
set_gl_ctx(None);
|
||||
assert!(gl_ctx().is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! OfxMultiThreadSuite v1:插件自起线程的登记与索引分配。
|
||||
//!
|
||||
//! 纪律:插件线程可在本 suite 存活期回调任意 suite——所有共享状态
|
||||
//! 必须 Mutex(README §3)。线程经 `std::thread::scope` 启动并同步
|
||||
//! join(OFX 语义:multiThread 返回时全部线程已完成;
|
||||
//! HS: ofxhImageEffect.cpp gMultiThreadSuite),故无需常驻线程表。
|
||||
//! 线程索引经 TLS 分配:宿主线程 [`index`] 为 -1、
|
||||
//! [`is_spawned`] 为 0。
|
||||
//!
|
||||
//! 参照:HS: ofxhImageEffect.cpp gMultiThreadSuite。
|
||||
|
||||
use std::ffi::{c_int, c_uint, c_void};
|
||||
|
||||
use crate::suites::status;
|
||||
|
||||
thread_local! {
|
||||
/// 本线程的 OFX 线程索引(宿主线程 = None)。
|
||||
static THREAD_INDEX: std::cell::Cell<Option<c_uint>> = const { std::cell::Cell::new(None) };
|
||||
}
|
||||
|
||||
/// 函数表布局(OfxMultiThreadSuiteV1)。
|
||||
#[repr(C)]
|
||||
pub struct MultiThreadSuiteV1 {
|
||||
/// multiThread:启动 `n_threads` 个线程跑 `func`,线程索引
|
||||
/// 0..n_threads-1 经 `thread_arg` 传入。
|
||||
pub multi_thread: unsafe extern "C" fn(
|
||||
func: unsafe extern "C" fn(c_uint, c_uint, *mut c_void),
|
||||
n_threads: c_uint,
|
||||
thread_arg: *mut c_void,
|
||||
) -> c_int,
|
||||
/// multiThreadNumCPUs
|
||||
pub num_cpus: unsafe extern "C" fn(*mut c_int) -> c_int,
|
||||
/// multiThreadIndex:当前线程的 OFX 线程索引(宿主线程为 -1)。
|
||||
pub index: unsafe extern "C" fn(*mut c_int) -> c_int,
|
||||
/// multiThreadIsSpawnedThread:当前线程是否插件线程。
|
||||
pub is_spawned: unsafe extern "C" fn(*mut c_int) -> c_int,
|
||||
}
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
fn caught(f: impl FnOnce() -> c_int) -> c_int {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or(status::FAILED)
|
||||
}
|
||||
|
||||
/// multiThread 实现:同步起 n 个线程(0..n-1),join 后返回。
|
||||
/// 线程体只做两件事:登记 TLS 索引、调 `func`(C 代码,不 panic)。
|
||||
unsafe extern "C" fn multi_thread(
|
||||
func: unsafe extern "C" fn(c_uint, c_uint, *mut c_void),
|
||||
n_threads: c_uint,
|
||||
thread_arg: *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
// 裸指针不可 Send:以 usize 搬运,线程内还原(C 侧本即
|
||||
// 整数传递语义)。
|
||||
let arg = thread_arg as usize;
|
||||
let _ = std::thread::scope(|scope| {
|
||||
for i in 0..n_threads {
|
||||
scope.spawn(move || {
|
||||
THREAD_INDEX.with(|t| t.set(Some(i)));
|
||||
// func 是插件代码:按 OFX 契约不 panic;Rust 侧
|
||||
// 只做 TLS 登记,无 panic 源。
|
||||
unsafe { (func)(i, n_threads, arg as *mut c_void) };
|
||||
THREAD_INDEX.with(|t| t.set(None));
|
||||
});
|
||||
}
|
||||
});
|
||||
status::OK
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn multi_thread_num_cpus(out: *mut c_int) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return status::ERR_VALUE;
|
||||
}
|
||||
// 物理核数(HS 同:系统 CPU 数)。
|
||||
let n = std::thread::available_parallelism()
|
||||
.map(|n| n.get() as c_int)
|
||||
.unwrap_or(1);
|
||||
unsafe { *out = n };
|
||||
status::OK
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn multi_thread_index(out: *mut c_int) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return status::ERR_VALUE;
|
||||
}
|
||||
// 宿主线程 → -1(HS 约定)。
|
||||
unsafe { *out = THREAD_INDEX.with(|t| t.get()).map_or(-1, |i| i as c_int) };
|
||||
status::OK
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn multi_thread_is_spawned(out: *mut c_int) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return status::ERR_VALUE;
|
||||
}
|
||||
unsafe { *out = THREAD_INDEX.with(|t| t.get().is_some()) as c_int };
|
||||
status::OK
|
||||
})
|
||||
}
|
||||
|
||||
/// 静态函数表实例。
|
||||
pub fn suite_v1() -> &'static MultiThreadSuiteV1 {
|
||||
static SUITE: std::sync::OnceLock<MultiThreadSuiteV1> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| MultiThreadSuiteV1 {
|
||||
multi_thread: multi_thread,
|
||||
num_cpus: multi_thread_num_cpus,
|
||||
index: multi_thread_index,
|
||||
is_spawned: multi_thread_is_spawned,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
/// 每个插件线程把 (index, count) 累计进 userdata。
|
||||
unsafe extern "C" fn worker(index: c_uint, count: c_uint, arg: *mut c_void) {
|
||||
let state = unsafe { &mut *(arg as *mut (AtomicUsize, AtomicUsize)) };
|
||||
state.0.fetch_add(index as usize, Ordering::Relaxed);
|
||||
state.1.fetch_add(count as usize, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_thread_spawns_and_joins() {
|
||||
let s = suite_v1();
|
||||
let state = (AtomicUsize::new(0), AtomicUsize::new(0));
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
(s.multi_thread)(worker, 8, &state as *const _ as *mut c_void),
|
||||
status::OK
|
||||
);
|
||||
}
|
||||
// 索引 0..7 各一次 → 28;count 各 8 → 64。
|
||||
assert_eq!(state.0.load(Ordering::Relaxed), 28);
|
||||
assert_eq!(state.1.load(Ordering::Relaxed), 64);
|
||||
|
||||
// 0 线程:no-op OK。
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
(s.multi_thread)(worker, 0, &state as *const _ as *mut c_void),
|
||||
status::OK
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件线程内查询 index/isSpawned 并把结果写回 arg 指向的槽。
|
||||
/// (闭包捕获 suite 无法转 fn 指针,用具名函数 + 静态槽。)
|
||||
static SPAWNED_RESULT: std::sync::LazyLock<std::sync::Mutex<Option<(c_int, c_int)>>> =
|
||||
std::sync::LazyLock::new(|| std::sync::Mutex::new(None));
|
||||
|
||||
unsafe extern "C" fn query_worker(_index: c_uint, _count: c_uint, _arg: *mut c_void) {
|
||||
let s = suite_v1();
|
||||
let mut i = 0;
|
||||
let mut sp = 1;
|
||||
unsafe {
|
||||
let _ = (s.index)(&mut i);
|
||||
let _ = (s.is_spawned)(&mut sp);
|
||||
}
|
||||
*SPAWNED_RESULT.lock().unwrap_or_else(|e| e.into_inner()) = Some((i, sp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_and_spawned() {
|
||||
let s = suite_v1();
|
||||
let mut i = 0;
|
||||
let mut spawned = 1;
|
||||
unsafe {
|
||||
// 宿主线程:index = -1,isSpawned = 0。
|
||||
assert_eq!((s.index)(&mut i), status::OK);
|
||||
assert_eq!(i, -1);
|
||||
assert_eq!((s.is_spawned)(&mut spawned), status::OK);
|
||||
assert_eq!(spawned, 0);
|
||||
|
||||
// 插件线程内:index 正确、isSpawned = 1。
|
||||
assert_eq!(
|
||||
(s.multi_thread)(query_worker, 1, std::ptr::null_mut()),
|
||||
status::OK
|
||||
);
|
||||
assert_eq!(
|
||||
*SPAWNED_RESULT.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
Some((0, 1))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_cpus_positive() {
|
||||
let s = suite_v1();
|
||||
let mut n = 0;
|
||||
unsafe {
|
||||
assert_eq!((s.num_cpus)(&mut n), status::OK);
|
||||
}
|
||||
assert!(n >= 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,983 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! OfxParameterSuite v1:参数定义与读写。
|
||||
//!
|
||||
//! 语义对照 HS: ofxhParam.cpp:
|
||||
//! - paramDefine:describe 期把参数定义挂到效果描述符(未定义类型 →
|
||||
//! kOfxStatErrUnsupported,HS:1665-1710;parametric 第 1 期不支持);
|
||||
//! - paramGetHandle:按名查(未找到 → kOfxStatErrUnknown,HS:1758);
|
||||
//! - paramGetValue/paramSetValue 等变长入口在 C shim
|
||||
//! (cbits/ofx_param_shim.c):按 [`crate::param::ParamKind`] 解析
|
||||
//! va_args 后转发到本模块的 `ofx_param_*_impl`;
|
||||
//! - paramSetValue 成功后触发 instanceChanged(kOfxChangePluginEdited,
|
||||
//! HS:1982-1994 的 `paramChangedByPlugin`),经
|
||||
//! [`crate::param::notify_instance_changed`] 回写绑定节点;
|
||||
//! - 第 1 期无动画:AtTime == 当前值;keys 恒 0;derivative/integral
|
||||
//! → kOfxStatErrMissingHostFeature;editBegin/End 为编辑事务括号
|
||||
//! (undo 分组:事务内回写并入一条 multi 命令,见
|
||||
//! [`crate::instance::Instance::edit_begin`])。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, c_double, c_int, c_void, CStr, CString};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::descriptor::EffectDescriptor;
|
||||
use crate::instance::Instance;
|
||||
use crate::param::{ChangeReason, ParamDef, ParamInstance, ParamKind, ParamValue};
|
||||
use crate::suites::status;
|
||||
use crate::suites::tag;
|
||||
|
||||
/// 函数表布局(与 SDK `OfxParameterSuiteV1` 一致;字段序以
|
||||
/// SDK 为准)。
|
||||
#[repr(C)]
|
||||
pub struct ParameterSuiteV1 {
|
||||
/// paramDefine(describe 期间):定义参数。
|
||||
pub param_define: unsafe extern "C" fn(*mut c_void, *const c_char, *const c_char, *mut *mut c_void) -> c_int,
|
||||
/// paramGetHandle
|
||||
pub param_get_handle: unsafe extern "C" fn(*mut c_void, *const c_char, *mut *mut c_void, *mut *mut c_void) -> c_int,
|
||||
/// paramSetGetPropertySet
|
||||
pub param_set_get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
|
||||
/// paramGetPropertySet
|
||||
pub param_get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
|
||||
/// paramGetValue(变长出口参数按类型解释;入口在 C shim)
|
||||
pub param_get_value: unsafe extern "C" fn(*mut c_void, ...) -> c_int,
|
||||
/// paramGetValueAtTime
|
||||
pub param_get_value_at_time: unsafe extern "C" fn(*mut c_void, c_double, ...) -> c_int,
|
||||
/// paramGetDerivative
|
||||
pub param_get_derivative: unsafe extern "C" fn(*mut c_void, c_double, ...) -> c_int,
|
||||
/// paramGetIntegral
|
||||
pub param_get_integral: unsafe extern "C" fn(*mut c_void, c_double, c_double, ...) -> c_int,
|
||||
/// paramSetValue(触发 instanceChanged)
|
||||
pub param_set_value: unsafe extern "C" fn(*mut c_void, ...) -> c_int,
|
||||
/// paramSetValueAtTime
|
||||
pub param_set_value_at_time: unsafe extern "C" fn(*mut c_void, c_double, ...) -> c_int,
|
||||
/// paramGetNumKeys
|
||||
pub param_get_num_keys: unsafe extern "C" fn(*mut c_void, *mut c_int) -> c_int,
|
||||
/// paramGetKeyTime
|
||||
pub param_get_key_time: unsafe extern "C" fn(*mut c_void, c_int, *mut c_double) -> c_int,
|
||||
/// paramGetKeyIndex
|
||||
pub param_get_key_index: unsafe extern "C" fn(*mut c_void, c_double, c_int, *mut c_int) -> c_int,
|
||||
/// paramDeleteKey
|
||||
pub param_delete_key: unsafe extern "C" fn(*mut c_void, c_double) -> c_int,
|
||||
/// paramDeleteAllKeys
|
||||
pub param_delete_all_keys: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
/// paramCopy
|
||||
pub param_copy: unsafe extern "C" fn(*mut c_void, *mut c_void, c_double, c_double, *const c_void) -> c_int,
|
||||
/// paramEditBegin / paramEditEnd(编辑事务括号)
|
||||
pub param_edit_begin: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
/// paramEditEnd
|
||||
pub param_edit_end: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
}
|
||||
|
||||
// ---- 句柄解析 -----------------------------------------------------------
|
||||
|
||||
/// param-set handle:describe 期是效果描述符,实例期是实例
|
||||
/// (getParamSet 返回与 effect 相同的 handle,见 image_effect suite)。
|
||||
enum ParamSetRef<'a> {
|
||||
Descriptor(&'a mut EffectDescriptor),
|
||||
Instance(&'a Instance),
|
||||
}
|
||||
|
||||
/// param handle:describe 期是定义(值=默认值),实例期是实例。
|
||||
enum ParamRef<'a> {
|
||||
Def(&'a ParamDef),
|
||||
Instance(&'a ParamInstance),
|
||||
}
|
||||
|
||||
/// 解析 param-set(effect)句柄。空指针/标签不符 → BadHandle。
|
||||
fn resolve_param_set(handle: *mut c_void) -> Result<ParamSetRef<'static>, c_int> {
|
||||
if handle.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
// 两种对象 props 均在偏移 0(句柄约定)。
|
||||
unsafe {
|
||||
match tag::kind(handle) {
|
||||
tag::DESCRIPTOR => {
|
||||
Ok(ParamSetRef::Descriptor(&mut *(tag::strip(handle) as *mut EffectDescriptor)))
|
||||
}
|
||||
tag::INSTANCE => Ok(ParamSetRef::Instance(&*(tag::strip(handle) as *const Instance))),
|
||||
_ => Err(status::ERR_BAD_HANDLE),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析 param 句柄。
|
||||
fn resolve_param(handle: *mut c_void) -> Result<ParamRef<'static>, c_int> {
|
||||
if handle.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
unsafe {
|
||||
match tag::kind(handle) {
|
||||
tag::PARAM_DEF => Ok(ParamRef::Def(&*(tag::strip(handle) as *const ParamDef))),
|
||||
tag::PARAM_INSTANCE => {
|
||||
Ok(ParamRef::Instance(&*(tag::strip(handle) as *const ParamInstance)))
|
||||
}
|
||||
_ => Err(status::ERR_BAD_HANDLE),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 属性名(空指针/非 UTF-8 → ErrValue;防御性)。
|
||||
unsafe fn c_name<'a>(name: *const c_char) -> Result<&'a str, c_int> {
|
||||
if name.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
unsafe { CStr::from_ptr(name) }
|
||||
.to_str()
|
||||
.map_err(|_| status::ERR_VALUE)
|
||||
}
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f))
|
||||
.map_or_else(|_| status::FAILED, |r| r.map_or_else(|c| c, |()| status::OK))
|
||||
}
|
||||
|
||||
// ---- 变长参数实现(C shim 转发)----------------------------------------
|
||||
|
||||
impl ParamKind {
|
||||
fn from_i32(v: c_int) -> Option<ParamKind> {
|
||||
Some(match v {
|
||||
1 => ParamKind::Int,
|
||||
2 => ParamKind::Int2,
|
||||
3 => ParamKind::Int3,
|
||||
4 => ParamKind::Double,
|
||||
5 => ParamKind::Double2,
|
||||
6 => ParamKind::Double3,
|
||||
7 => ParamKind::Bool,
|
||||
8 => ParamKind::Choice,
|
||||
9 => ParamKind::Rgb,
|
||||
10 => ParamKind::Rgba,
|
||||
11 => ParamKind::Str,
|
||||
12 => ParamKind::StrChoice,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 按 kind 把 ParamValue 写进类型化 out(数值类)。
|
||||
fn write_value(v: &ParamValue, kind: ParamKind, out: *mut c_void) -> Result<(), c_int> {
|
||||
let err = || Err(status::FAILED);
|
||||
unsafe {
|
||||
match kind {
|
||||
ParamKind::Int => match v {
|
||||
ParamValue::Int(a, _) => {
|
||||
*(out as *mut c_int) = a[0];
|
||||
Ok(())
|
||||
}
|
||||
_ => err(),
|
||||
},
|
||||
ParamKind::Int2 | ParamKind::Int3 => match v {
|
||||
ParamValue::Int(a, dim) => {
|
||||
let n = if kind == ParamKind::Int2 { 2 } else { 3 };
|
||||
let dst = std::slice::from_raw_parts_mut(out as *mut c_int, n);
|
||||
for (d, s) in dst.iter_mut().zip(a.iter()).take(n) {
|
||||
*d = *s;
|
||||
}
|
||||
let _ = dim;
|
||||
Ok(())
|
||||
}
|
||||
_ => err(),
|
||||
},
|
||||
ParamKind::Double => match v {
|
||||
ParamValue::Double(d, _) => {
|
||||
*(out as *mut f64) = d[0];
|
||||
Ok(())
|
||||
}
|
||||
_ => err(),
|
||||
},
|
||||
ParamKind::Double2 | ParamKind::Double3 => match v {
|
||||
ParamValue::Double(d, _) => {
|
||||
let n = if kind == ParamKind::Double2 { 2 } else { 3 };
|
||||
let dst = std::slice::from_raw_parts_mut(out as *mut f64, n);
|
||||
for (dd, s) in dst.iter_mut().zip(d.iter()).take(n) {
|
||||
*dd = *s;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => err(),
|
||||
},
|
||||
ParamKind::Bool => match v {
|
||||
ParamValue::Bool(b) => {
|
||||
*(out as *mut c_int) = *b as c_int;
|
||||
Ok(())
|
||||
}
|
||||
_ => err(),
|
||||
},
|
||||
ParamKind::Choice => match v {
|
||||
ParamValue::Choice(c) => {
|
||||
*(out as *mut c_int) = *c;
|
||||
Ok(())
|
||||
}
|
||||
_ => err(),
|
||||
},
|
||||
ParamKind::Rgb | ParamKind::Rgba => match v {
|
||||
ParamValue::Color(c, _) => {
|
||||
let n = if kind == ParamKind::Rgb { 3 } else { 4 };
|
||||
let dst = std::slice::from_raw_parts_mut(out as *mut f64, n);
|
||||
for (dd, s) in dst.iter_mut().zip(c.iter()).take(n) {
|
||||
*dd = *s;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => err(),
|
||||
},
|
||||
ParamKind::Str | ParamKind::StrChoice => {
|
||||
// 字符串走 ofx_param_get_string_impl(内驻指针)。
|
||||
err()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 按 kind 从类型化 input 构造 ParamValue(数值类)。
|
||||
fn read_value(kind: ParamKind, input: *const c_void) -> Option<ParamValue> {
|
||||
unsafe {
|
||||
match kind {
|
||||
ParamKind::Int => Some(ParamValue::Int([*(input as *const c_int), 0, 0], 1)),
|
||||
ParamKind::Int2 => {
|
||||
let a = *(input as *const [c_int; 2]);
|
||||
Some(ParamValue::Int([a[0], a[1], 0], 2))
|
||||
}
|
||||
ParamKind::Int3 => {
|
||||
let a = *(input as *const [c_int; 3]);
|
||||
Some(ParamValue::Int([a[0], a[1], a[2]], 3))
|
||||
}
|
||||
ParamKind::Double => Some(ParamValue::Double([*(input as *const f64), 0.0, 0.0], 1)),
|
||||
ParamKind::Double2 => {
|
||||
let a = *(input as *const [f64; 2]);
|
||||
Some(ParamValue::Double([a[0], a[1], 0.0], 2))
|
||||
}
|
||||
ParamKind::Double3 => {
|
||||
let a = *(input as *const [f64; 3]);
|
||||
Some(ParamValue::Double([a[0], a[1], a[2]], 3))
|
||||
}
|
||||
ParamKind::Bool => Some(ParamValue::Bool(*(input as *const c_int) != 0)),
|
||||
ParamKind::Choice => Some(ParamValue::Choice(*(input as *const c_int))),
|
||||
ParamKind::Rgb => {
|
||||
let a = *(input as *const [f64; 3]);
|
||||
Some(ParamValue::Color([a[0], a[1], a[2], 0.0], 3))
|
||||
}
|
||||
ParamKind::Rgba => {
|
||||
let a = *(input as *const [f64; 4]);
|
||||
Some(ParamValue::Color([a[0], a[1], a[2], a[3]], 4))
|
||||
}
|
||||
ParamKind::Str | ParamKind::StrChoice => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 取"当前值"(实例期=当前值,describe 期=默认值)。
|
||||
fn value_of(r: &ParamRef) -> ParamValue {
|
||||
match r {
|
||||
ParamRef::Instance(p) => p.get(),
|
||||
ParamRef::Def(d) => d.default.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// param → 所属 instance 的回写定位表(instanceChanged 用)。
|
||||
/// `// TODO(instance)`:createInstance 后由宿主登记,destroy 时摘除。
|
||||
static PARAM_OWNER: std::sync::LazyLock<Mutex<HashMap<usize, usize>>> =
|
||||
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// 登记参数 → 实例(实例化路径调用)。
|
||||
pub(crate) fn register_param_owner(param_props: usize, instance_props: usize) {
|
||||
PARAM_OWNER
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(param_props, instance_props);
|
||||
}
|
||||
|
||||
/// 摘除某实例的全部参数登记(destroy 路径调用)。
|
||||
pub(crate) fn unregister_params_of(instance_props: usize) {
|
||||
PARAM_OWNER
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.retain(|_, owner| *owner != instance_props);
|
||||
}
|
||||
|
||||
/// paramSetValue 成功后的 instanceChanged 触发(HS:
|
||||
/// ofxhParam.cpp:1991-1994 `paramChangedByPlugin`)。未登记实例时
|
||||
/// no-op(未绑定节点的场景本就 no-op)。
|
||||
fn notify_changed(param_props: usize, name: &str) {
|
||||
let owner = PARAM_OWNER
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.get(¶m_props)
|
||||
.copied();
|
||||
if let Some(inst_addr) = owner {
|
||||
// instance 的 props 在偏移 0(句柄约定)。
|
||||
let inst = unsafe { &*(inst_addr as *const Instance) };
|
||||
crate::param::notify_instance_changed(inst, name, ChangeReason::PluginEdited);
|
||||
}
|
||||
}
|
||||
|
||||
/// 参数类型查询(C shim 调用):未知/非法句柄 → 0(dispatch 失败)。
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn ofx_param_kind_of(param: *mut c_void) -> c_int {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
match resolve_param(param) {
|
||||
Ok(r) => match &r {
|
||||
ParamRef::Instance(p) => p.def.kind().map_or(0, |k| k as c_int),
|
||||
ParamRef::Def(d) => d.kind().map_or(0, |k| k as c_int),
|
||||
},
|
||||
Err(_) => 0,
|
||||
}
|
||||
}))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// 数值类 get(C shim 转发):`out` 为按 kind 的类型化数组。
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn ofx_param_get_impl(
|
||||
param: *mut c_void,
|
||||
kind: c_int,
|
||||
out: *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let k = ParamKind::from_i32(kind).ok_or(status::ERR_UNKNOWN)?;
|
||||
let r = resolve_param(param)?;
|
||||
write_value(&value_of(&r), k, out)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// 字符串 get(内驻指针:指向实例存储 / 定义默认值;下次 set 前
|
||||
/// 有效——与属性 suite 的字符串指针契约一致)。
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn ofx_param_get_string_impl(
|
||||
param: *mut c_void,
|
||||
out: *mut *mut c_char,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let ptr: *const c_char = match resolve_param(param)? {
|
||||
ParamRef::Instance(p) => p.string_ptr().ok_or(status::FAILED)?,
|
||||
ParamRef::Def(d) => match &d.default {
|
||||
ParamValue::String(s) | ParamValue::StrChoice(s) => s.as_ptr(),
|
||||
_ => return Err(status::FAILED),
|
||||
},
|
||||
};
|
||||
unsafe { *out = ptr as *mut c_char };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// 数值类 set(C shim 转发):`input` 为按 kind 的类型化数组。
|
||||
/// describe 期(ParamDef)→ BadHandle(HS: paramSetValue 的
|
||||
/// verifyMagic 对 descriptor 失败)。
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn ofx_param_set_impl(
|
||||
param: *mut c_void,
|
||||
kind: c_int,
|
||||
input: *const c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if input.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let k = ParamKind::from_i32(kind).ok_or(status::ERR_UNKNOWN)?;
|
||||
let value = read_value(k, input).ok_or(status::FAILED)?;
|
||||
let (name, props_addr) = match resolve_param(param)? {
|
||||
ParamRef::Instance(p) => {
|
||||
p.set_ofx(value);
|
||||
(p.def.name.clone(), &p.props as *const _ as usize)
|
||||
}
|
||||
ParamRef::Def(_) => return Err(status::ERR_BAD_HANDLE),
|
||||
};
|
||||
notify_changed(props_addr, &name);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// 字符串 set。
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn ofx_param_set_string_impl(
|
||||
param: *mut c_void,
|
||||
input: *const c_char,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if input.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let s = CString::new(unsafe { CStr::from_ptr(input) }.to_bytes()).unwrap();
|
||||
let (name, props_addr) = match resolve_param(param)? {
|
||||
ParamRef::Instance(p) => {
|
||||
p.set_ofx(ParamValue::String(s));
|
||||
(p.def.name.clone(), &p.props as *const _ as usize)
|
||||
}
|
||||
ParamRef::Def(_) => return Err(status::ERR_BAD_HANDLE),
|
||||
};
|
||||
notify_changed(props_addr, &name);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// derivative/integral:第 1 期无动画 → MissingHostFeature
|
||||
/// (C shim 不消费 va_args 直接转发)。
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn ofx_param_missing_feature_impl(param: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
// handle 合法性仍检查(空/坏句柄 → BadHandle)。
|
||||
let _ = resolve_param(param)?;
|
||||
Err(status::ERR_MISSING_HOST_FEATURE)
|
||||
})
|
||||
}
|
||||
|
||||
// ---- 非变长入口 ----------------------------------------------------------
|
||||
|
||||
/// paramDefine:describe 期定义参数(未定义类型/parametric →
|
||||
/// Unsupported,HS:1704;重复名 → HS 允许,原样再建一条——宿主以
|
||||
/// 首个为准,与 HS 的 map 覆盖语义一致)。
|
||||
unsafe extern "C" fn param_define(
|
||||
param_set: *mut c_void,
|
||||
param_type: *const c_char,
|
||||
name: *const c_char,
|
||||
property_set: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if property_set.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
unsafe { *property_set = std::ptr::null_mut() };
|
||||
let t = unsafe { c_name(param_type)? };
|
||||
let n = unsafe { c_name(name)? };
|
||||
// 仅 describe 期(HS: SetDescriptor 专属,实例期 dynamic_cast
|
||||
// 失败 → BadHandle)。
|
||||
let desc = match resolve_param_set(param_set)? {
|
||||
ParamSetRef::Descriptor(d) => d,
|
||||
ParamSetRef::Instance(_) => return Err(status::ERR_BAD_HANDLE),
|
||||
};
|
||||
// 类型合法性(kind_of_type 之外还有无值类与 parametric)。
|
||||
let valid = crate::param::kind_of_type(t).is_some()
|
||||
|| matches!(
|
||||
t,
|
||||
crate::param::TYPE_BYTES
|
||||
| crate::param::TYPE_CUSTOM
|
||||
| crate::param::TYPE_PUSHBUTTON
|
||||
| crate::param::TYPE_GROUP
|
||||
| crate::param::TYPE_PAGE
|
||||
);
|
||||
if !valid || t == crate::param::TYPE_PARAMETRIC {
|
||||
return Err(status::ERR_UNSUPPORTED);
|
||||
}
|
||||
let def = ParamDef::new(n, t);
|
||||
desc.params.push(Box::new(def));
|
||||
// 地址必须取自已入盒的对象(栈上临时变量在 push 后失效,
|
||||
// 句柄会指向死内存——Box 负载地址在 Vec 重分配时稳定)。
|
||||
let def = desc.params.last().expect("just pushed");
|
||||
let addr = &def.props as *const _ as usize;
|
||||
unsafe { *property_set = tag::make(addr as *const crate::property::PropertySet, tag::PARAM_DEF) };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// paramGetHandle:按名查(未找到 → ErrUnknown,HS:1758)。
|
||||
unsafe extern "C" fn param_get_handle(
|
||||
param_set: *mut c_void,
|
||||
name: *const c_char,
|
||||
param: *mut *mut c_void,
|
||||
property_set: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if param.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
unsafe { *param = std::ptr::null_mut() };
|
||||
let n = unsafe { c_name(name)? };
|
||||
let handle = match resolve_param_set(param_set)? {
|
||||
ParamSetRef::Descriptor(d) => {
|
||||
let def = d.param(n).ok_or(status::ERR_UNKNOWN)?;
|
||||
let addr = &def.props as *const _ as usize;
|
||||
tag::make(addr as *const crate::property::PropertySet, tag::PARAM_DEF)
|
||||
}
|
||||
ParamSetRef::Instance(i) => {
|
||||
let p = i.params.find(n).ok_or(status::ERR_UNKNOWN)?;
|
||||
let addr = &p.props as *const _ as usize;
|
||||
tag::make(addr as *const crate::property::PropertySet, tag::PARAM_INSTANCE)
|
||||
}
|
||||
};
|
||||
unsafe { *param = handle };
|
||||
if !property_set.is_null() {
|
||||
// props 在偏移 0:param handle 与 props handle 同值。
|
||||
unsafe { *property_set = handle };
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// paramSetGetPropertySet:param-set 的属性集(= effect handle 本体)。
|
||||
unsafe extern "C" fn param_set_get_property_set(
|
||||
param_set: *mut c_void,
|
||||
property_set: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if property_set.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
let _ = resolve_param_set(param_set)?;
|
||||
unsafe { *property_set = param_set };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// paramGetPropertySet:param 的属性集(= param handle 本体)。
|
||||
unsafe extern "C" fn param_get_property_set(
|
||||
param: *mut c_void,
|
||||
property_set: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if property_set.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
let _ = resolve_param(param)?;
|
||||
unsafe { *property_set = param };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// paramGetNumKeys:第 1 期无动画 → 恒 0。
|
||||
unsafe extern "C" fn param_get_num_keys(param: *mut c_void, count: *mut c_int) -> c_int {
|
||||
caught(|| {
|
||||
let _ = resolve_param(param)?;
|
||||
if count.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
unsafe { *count = 0 };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// paramGetKeyTime:无关键帧 → BadIndex。
|
||||
unsafe extern "C" fn param_get_key_time(param: *mut c_void, _index: c_int, _time: *mut c_double) -> c_int {
|
||||
caught(|| {
|
||||
let _ = resolve_param(param)?;
|
||||
Err(status::ERR_BAD_INDEX)
|
||||
})
|
||||
}
|
||||
|
||||
/// paramGetKeyIndex:无关键帧 → BadIndex。
|
||||
unsafe extern "C" fn param_get_key_index(
|
||||
param: *mut c_void,
|
||||
_time: c_double,
|
||||
_direction: c_int,
|
||||
_index: *mut c_int,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
let _ = resolve_param(param)?;
|
||||
Err(status::ERR_BAD_INDEX)
|
||||
})
|
||||
}
|
||||
|
||||
/// paramDeleteKey / paramDeleteAllKeys:无关键帧 → OK no-op。
|
||||
unsafe extern "C" fn param_delete_key(param: *mut c_void, _time: c_double) -> c_int {
|
||||
caught(|| {
|
||||
let _ = resolve_param(param)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn param_delete_all_keys(param: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
let _ = resolve_param(param)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// paramCopy:第 1 期无关键帧可拷 → OK no-op。
|
||||
unsafe extern "C" fn param_copy(
|
||||
dst: *mut c_void,
|
||||
src: *mut c_void,
|
||||
_dst_time: c_double,
|
||||
_src_time: c_double,
|
||||
_key_range: *const c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
let _ = resolve_param(dst)?;
|
||||
let _ = resolve_param(src)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// paramEditBegin:进入编辑事务(undo 分组)。解析 param 句柄后经
|
||||
/// PARAM_OWNER 定位所属实例并递增其事务深度;未登记实例时 no-op
|
||||
/// (手工构造的实例没有宿主注册,edit 括号退化为无分组)。
|
||||
unsafe extern "C" fn param_edit_begin(param: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
let addr = match resolve_param(param)? {
|
||||
ParamRef::Instance(p) => &p.props as *const _ as usize,
|
||||
ParamRef::Def(_) => return Err(status::ERR_BAD_HANDLE),
|
||||
};
|
||||
if let Some(owner) = PARAM_OWNER
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.get(&addr)
|
||||
.copied()
|
||||
{
|
||||
let inst = unsafe { &*(owner as *const Instance) };
|
||||
inst.edit_begin();
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// paramEditEnd:退出编辑事务;最外层结束时把累积的 multi 命令
|
||||
/// redo 生效并释放(见 [`crate::instance::Instance::edit_end`])。
|
||||
unsafe extern "C" fn param_edit_end(param: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
let addr = match resolve_param(param)? {
|
||||
ParamRef::Instance(p) => &p.props as *const _ as usize,
|
||||
ParamRef::Def(_) => return Err(status::ERR_BAD_HANDLE),
|
||||
};
|
||||
if let Some(owner) = PARAM_OWNER
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.get(&addr)
|
||||
.copied()
|
||||
{
|
||||
let inst = unsafe { &*(owner as *const Instance) };
|
||||
inst.edit_end();
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn ofx_param_get_value_shim(param: *mut c_void, ...) -> c_int;
|
||||
fn ofx_param_get_value_at_time_shim(param: *mut c_void, time: c_double, ...) -> c_int;
|
||||
fn ofx_param_set_value_shim(param: *mut c_void, ...) -> c_int;
|
||||
fn ofx_param_set_value_at_time_shim(param: *mut c_void, time: c_double, ...) -> c_int;
|
||||
fn ofx_param_get_derivative_shim(param: *mut c_void, time: c_double, ...) -> c_int;
|
||||
fn ofx_param_get_integral_shim(
|
||||
param: *mut c_void,
|
||||
time1: c_double,
|
||||
time2: c_double,
|
||||
...
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
/// 静态函数表实例。
|
||||
pub fn suite_v1() -> &'static ParameterSuiteV1 {
|
||||
static SUITE: std::sync::OnceLock<ParameterSuiteV1> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| ParameterSuiteV1 {
|
||||
param_define: param_define,
|
||||
param_get_handle: param_get_handle,
|
||||
param_set_get_property_set: param_set_get_property_set,
|
||||
param_get_property_set: param_get_property_set,
|
||||
param_get_value: ofx_param_get_value_shim,
|
||||
param_get_value_at_time: ofx_param_get_value_at_time_shim,
|
||||
param_get_derivative: ofx_param_get_derivative_shim,
|
||||
param_get_integral: ofx_param_get_integral_shim,
|
||||
param_set_value: ofx_param_set_value_shim,
|
||||
param_set_value_at_time: ofx_param_set_value_at_time_shim,
|
||||
param_get_num_keys: param_get_num_keys,
|
||||
param_get_key_time: param_get_key_time,
|
||||
param_get_key_index: param_get_key_index,
|
||||
param_delete_key: param_delete_key,
|
||||
param_delete_all_keys: param_delete_all_keys,
|
||||
param_copy: param_copy,
|
||||
param_edit_begin: param_edit_begin,
|
||||
param_edit_end: param_edit_end,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::ffi::CString;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::host::Plugin;
|
||||
use crate::param::{ParamInstance, ParamSetInstance};
|
||||
use crate::property::PropertySet;
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
fn descriptor_handle(d: &EffectDescriptor) -> *mut c_void {
|
||||
tag::make(&d.props as *const crate::property::PropertySet, tag::DESCRIPTOR)
|
||||
}
|
||||
|
||||
/// 假插件(host::Plugin 的构造只为拿 Arc 喂给 Instance;describe
|
||||
/// 之外的字段不被本测试触碰)。
|
||||
fn dummy_plugin(descriptor: EffectDescriptor) -> Arc<Plugin> {
|
||||
unsafe extern "C" fn dummy_entry(
|
||||
_: *const c_char,
|
||||
_: *const c_void,
|
||||
_: *mut c_void,
|
||||
_: *mut c_void,
|
||||
) -> c_int {
|
||||
status::OK
|
||||
}
|
||||
Arc::new(Plugin {
|
||||
identifier: "test.plugin".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::new(),
|
||||
contexts: vec![],
|
||||
descriptor,
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: dummy_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
})
|
||||
}
|
||||
|
||||
fn instance_handle(i: &Instance) -> *mut c_void {
|
||||
tag::make(&i.props as *const crate::property::PropertySet, tag::INSTANCE)
|
||||
}
|
||||
|
||||
/// 用 paramDefine 建一个实例(describe 产物 → createInstance)。
|
||||
fn make_instance() -> (Arc<Instance>, *mut c_void) {
|
||||
let mut desc = EffectDescriptor::new();
|
||||
let s = suite_v1();
|
||||
let dhandle = descriptor_handle(&desc);
|
||||
unsafe {
|
||||
for (t, n) in [
|
||||
("OfxParamTypeInteger", "gain"),
|
||||
("OfxParamTypeDouble", "opacity"),
|
||||
("OfxParamTypeDouble2D", "pos"),
|
||||
("OfxParamTypeRGB", "color"),
|
||||
("OfxParamTypeBoolean", "enabled"),
|
||||
("OfxParamTypeString", "label"),
|
||||
] {
|
||||
let t = cs(t);
|
||||
let n = cs(n);
|
||||
let mut ph: *mut c_void = std::ptr::null_mut();
|
||||
assert_eq!((s.param_define)(dhandle, t.as_ptr(), n.as_ptr(), &mut ph), 0);
|
||||
}
|
||||
}
|
||||
let params = ParamSetInstance {
|
||||
params: desc
|
||||
.params
|
||||
.iter()
|
||||
.map(|d| Box::new(ParamInstance::from_def((**d).clone())))
|
||||
.collect(),
|
||||
};
|
||||
let plugin = dummy_plugin(desc);
|
||||
let inst = Arc::new(Instance {
|
||||
props: PropertySet::new(),
|
||||
plugin,
|
||||
context: "OfxImageEffectContextFilter".into(),
|
||||
params,
|
||||
clips: vec![],
|
||||
node_identity: std::sync::atomic::AtomicUsize::new(0),
|
||||
destroyed: std::sync::atomic::AtomicBool::new(false),
|
||||
sequence_range: std::sync::Mutex::new(None),
|
||||
progress_cb: std::sync::Mutex::new(None),
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
});
|
||||
(inst.clone(), instance_handle(&inst))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn describe_define_and_get() {
|
||||
let mut desc = EffectDescriptor::new();
|
||||
let s = suite_v1();
|
||||
let handle = descriptor_handle(&desc);
|
||||
let t = cs("OfxParamTypeDouble");
|
||||
let n = cs("opacity");
|
||||
|
||||
let mut ph: *mut c_void = std::ptr::null_mut();
|
||||
unsafe {
|
||||
assert_eq!((s.param_define)(handle, t.as_ptr(), n.as_ptr(), &mut ph), 0);
|
||||
}
|
||||
assert!(!ph.is_null());
|
||||
assert_eq!(tag::kind(ph), tag::PARAM_DEF);
|
||||
|
||||
// describe 期 paramGetValue:默认值 0.0。
|
||||
let mut v = 99.0;
|
||||
unsafe {
|
||||
assert_eq!((s.param_get_value)(ph, &mut v), 0);
|
||||
}
|
||||
assert_eq!(v, 0.0);
|
||||
|
||||
// describe 期 set → BadHandle(HS: verifyMagic 对 descriptor 失败)。
|
||||
unsafe {
|
||||
assert_eq!((s.param_set_value)(ph, 42.0), status::ERR_BAD_HANDLE);
|
||||
}
|
||||
|
||||
// paramGetHandle 按名取(props handle 同值)。
|
||||
let mut ph2: *mut c_void = std::ptr::null_mut();
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
(s.param_get_handle)(handle, n.as_ptr(), &mut ph2, std::ptr::null_mut()),
|
||||
0
|
||||
);
|
||||
assert_eq!(ph, ph2);
|
||||
}
|
||||
|
||||
// 未找到 → ErrUnknown(HS:1758)。
|
||||
let nope = cs("nope");
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
(s.param_get_handle)(handle, nope.as_ptr(), &mut ph2, std::ptr::null_mut()),
|
||||
status::ERR_UNKNOWN
|
||||
);
|
||||
}
|
||||
|
||||
// 未定义类型 / parametric → ErrUnsupported(HS:1704)。
|
||||
let bad = cs("OfxParamTypeBogus");
|
||||
let par = cs("OfxParamTypeParametric");
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
(s.param_define)(handle, bad.as_ptr(), n.as_ptr(), &mut ph2),
|
||||
status::ERR_UNSUPPORTED
|
||||
);
|
||||
assert_eq!(
|
||||
(s.param_define)(handle, par.as_ptr(), n.as_ptr(), &mut ph2),
|
||||
status::ERR_UNSUPPORTED
|
||||
);
|
||||
}
|
||||
|
||||
// 实例期 paramDefine → BadHandle。
|
||||
let (_inst, ih) = make_instance();
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
(s.param_define)(ih, t.as_ptr(), n.as_ptr(), &mut ph2),
|
||||
status::ERR_BAD_HANDLE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instance_get_set_variadic_roundtrip() {
|
||||
let (_inst, ih) = make_instance();
|
||||
let s = suite_v1();
|
||||
|
||||
// 取各参数 handle。
|
||||
let mut gain: *mut c_void = std::ptr::null_mut();
|
||||
let mut opacity: *mut c_void = std::ptr::null_mut();
|
||||
let mut pos: *mut c_void = std::ptr::null_mut();
|
||||
let mut color: *mut c_void = std::ptr::null_mut();
|
||||
let mut enabled: *mut c_void = std::ptr::null_mut();
|
||||
let mut label: *mut c_void = std::ptr::null_mut();
|
||||
unsafe {
|
||||
for (n, out) in [
|
||||
("gain", &mut gain),
|
||||
("opacity", &mut opacity),
|
||||
("pos", &mut pos),
|
||||
("color", &mut color),
|
||||
("enabled", &mut enabled),
|
||||
("label", &mut label),
|
||||
] {
|
||||
let n = cs(n);
|
||||
let r = (s.param_get_handle)(ih, n.as_ptr(), out, std::ptr::null_mut());
|
||||
assert_eq!(r, 0, "gethandle {} failed: {}", n.to_str().unwrap(), r);
|
||||
assert_eq!(tag::kind(*out), tag::PARAM_INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
// Integer set/get。
|
||||
unsafe {
|
||||
assert_eq!((s.param_set_value)(gain, 7), 0);
|
||||
}
|
||||
let mut v = 0;
|
||||
unsafe {
|
||||
assert_eq!((s.param_get_value)(gain, &mut v), 0);
|
||||
}
|
||||
assert_eq!(v, 7);
|
||||
|
||||
// Double2D。
|
||||
unsafe {
|
||||
assert_eq!((s.param_set_value)(pos, 1.5, -2.5), 0);
|
||||
}
|
||||
let (mut x, mut y) = (0.0, 0.0);
|
||||
unsafe {
|
||||
assert_eq!((s.param_get_value)(pos, &mut x, &mut y), 0);
|
||||
}
|
||||
assert_eq!((x, y), (1.5, -2.5));
|
||||
|
||||
// RGB。
|
||||
unsafe {
|
||||
assert_eq!((s.param_set_value)(color, 0.1, 0.2, 0.3), 0);
|
||||
}
|
||||
let (mut r, mut g, mut b) = (0.0, 0.0, 0.0);
|
||||
unsafe {
|
||||
assert_eq!((s.param_get_value)(color, &mut r, &mut g, &mut b), 0);
|
||||
}
|
||||
assert_eq!((r, g, b), (0.1, 0.2, 0.3));
|
||||
|
||||
// Boolean(int 0/1)。
|
||||
unsafe {
|
||||
assert_eq!((s.param_set_value)(enabled, 1), 0);
|
||||
}
|
||||
let mut e = 0;
|
||||
unsafe {
|
||||
assert_eq!((s.param_get_value)(enabled, &mut e), 0);
|
||||
}
|
||||
assert_eq!(e, 1);
|
||||
|
||||
// String:set 后 get 返回内驻指针。
|
||||
let hello = cs("hello");
|
||||
unsafe {
|
||||
assert_eq!((s.param_set_value)(label, hello.as_ptr()), 0);
|
||||
}
|
||||
let mut p: *mut c_char = std::ptr::null_mut();
|
||||
unsafe {
|
||||
assert_eq!((s.param_get_value)(label, &mut p), 0);
|
||||
assert_eq!(CStr::from_ptr(p).to_bytes(), b"hello");
|
||||
}
|
||||
|
||||
// AtTime == 当前值(无动画)。
|
||||
let mut v2 = 0.0;
|
||||
unsafe {
|
||||
assert_eq!((s.param_get_value_at_time)(opacity, 12.0, &mut v2), 0);
|
||||
assert_eq!(v2, 0.0);
|
||||
assert_eq!((s.param_set_value_at_time)(opacity, 12.0, 0.75), 0);
|
||||
}
|
||||
let mut v3 = 0.0;
|
||||
unsafe {
|
||||
assert_eq!((s.param_get_value)(opacity, &mut v3), 0);
|
||||
}
|
||||
assert_eq!(v3, 0.75);
|
||||
|
||||
// keys:恒 0 / BadIndex。
|
||||
let mut nkeys = -1;
|
||||
unsafe {
|
||||
assert_eq!((s.param_get_num_keys)(opacity, &mut nkeys), 0);
|
||||
assert_eq!(nkeys, 0);
|
||||
let mut kt = 0.0;
|
||||
assert_eq!((s.param_get_key_time)(opacity, 0, &mut kt), status::ERR_BAD_INDEX);
|
||||
}
|
||||
|
||||
// derivative/integral → MissingHostFeature。
|
||||
let mut dv = 0.0;
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
(s.param_get_derivative)(opacity, 1.0, &mut dv),
|
||||
status::ERR_MISSING_HOST_FEATURE
|
||||
);
|
||||
}
|
||||
|
||||
// editBegin/End、deleteKey(s)、copy:no-op OK。
|
||||
unsafe {
|
||||
assert_eq!((s.param_edit_begin)(opacity), 0);
|
||||
assert_eq!((s.param_edit_end)(opacity), 0);
|
||||
assert_eq!((s.param_delete_key)(opacity, 1.0), 0);
|
||||
assert_eq!((s.param_delete_all_keys)(opacity), 0);
|
||||
assert_eq!((s.param_copy)(opacity, opacity, 1.0, 1.0, std::ptr::null()), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! OfxProgressSuite v1/v2:进度 → 当前渲染的
|
||||
//! [`crate::progress::ProgressReporter`]。
|
||||
//!
|
||||
//! 定位方式:TLS(render 驱动在调用插件 action 前
|
||||
//! [`set_current`],action 返回后清除)——进度只在渲染期内有意义,
|
||||
//! 不设全局状态(progress.rs 文档)。
|
||||
//! 语义:update 返回 false(取消)→ kOfxStatReplyNo
|
||||
//! (HS: ofxhImageEffect.cpp gProgressSuite);无报告器(渲染外
|
||||
//! 调用)→ 静默 OK。start/end 为括号,OK。
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
|
||||
use crate::progress::ProgressReporter;
|
||||
use crate::suites::status;
|
||||
|
||||
thread_local! {
|
||||
static CURRENT: std::cell::RefCell<Option<ProgressReporter>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// 设置/清除当前报告器(render 驱动调用;公开:render 驱动在
|
||||
/// host 层,测试也直接注入)。
|
||||
pub fn set_current(reporter: Option<ProgressReporter>) {
|
||||
CURRENT.with(|c| *c.borrow_mut() = reporter);
|
||||
}
|
||||
|
||||
/// 读取当前报告器的取消状态(image effect suite 的 abort 用)。
|
||||
pub(crate) fn is_cancelled() -> bool {
|
||||
CURRENT.with(|c| c.borrow().as_ref().is_some_and(|r| r.is_cancelled()))
|
||||
}
|
||||
|
||||
/// 进度更新:取消 → kOfxStatReplyNo;无报告器/未取消 → OK。
|
||||
fn progress_update(progress: f64) -> c_int {
|
||||
let cancelled = CURRENT.with(|c| match c.borrow().as_ref() {
|
||||
Some(r) => !r.update(progress),
|
||||
None => false,
|
||||
});
|
||||
if cancelled {
|
||||
status::REPLY_NO
|
||||
} else {
|
||||
status::OK
|
||||
}
|
||||
}
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
fn caught(f: impl FnOnce() -> c_int) -> c_int {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or(status::FAILED)
|
||||
}
|
||||
|
||||
/// 函数表布局(OfxProgressSuiteV1)。
|
||||
#[repr(C)]
|
||||
pub struct ProgressSuiteV1 {
|
||||
/// progressStart
|
||||
pub start: unsafe extern "C" fn(*mut c_void, *const c_char) -> c_int,
|
||||
/// progressUpdate(返回非 kOfxStatOK 即取消)
|
||||
pub update: unsafe extern "C" fn(*mut c_void, c_double) -> c_int,
|
||||
/// progressEnd
|
||||
pub end: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
}
|
||||
|
||||
/// 函数表布局(OfxProgressSuiteV2:start 带 label + message)。
|
||||
#[repr(C)]
|
||||
pub struct ProgressSuiteV2 {
|
||||
/// progressStart
|
||||
pub start: unsafe extern "C" fn(*mut c_void, *const c_char, *const c_char) -> c_int,
|
||||
/// progressUpdate
|
||||
pub update: unsafe extern "C" fn(*mut c_void, c_double) -> c_int,
|
||||
/// progressEnd
|
||||
pub end: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
}
|
||||
|
||||
/// progressStart:括号起点,OK(label/message 留作未来 UI 展示,
|
||||
/// 第 1 期不建模)。
|
||||
unsafe extern "C" fn progress_start_v1(_handle: *mut c_void, _label: *const c_char) -> c_int {
|
||||
caught(|| status::OK)
|
||||
}
|
||||
|
||||
unsafe extern "C" fn progress_update_v1(_handle: *mut c_void, progress: c_double) -> c_int {
|
||||
caught(|| progress_update(progress))
|
||||
}
|
||||
|
||||
unsafe extern "C" fn progress_end_v1(_handle: *mut c_void) -> c_int {
|
||||
caught(|| status::OK)
|
||||
}
|
||||
|
||||
unsafe extern "C" fn progress_start_v2(
|
||||
_handle: *mut c_void,
|
||||
_label: *const c_char,
|
||||
_message: *const c_char,
|
||||
) -> c_int {
|
||||
caught(|| status::OK)
|
||||
}
|
||||
|
||||
unsafe extern "C" fn progress_update_v2(_handle: *mut c_void, progress: c_double) -> c_int {
|
||||
caught(|| progress_update(progress))
|
||||
}
|
||||
|
||||
unsafe extern "C" fn progress_end_v2(_handle: *mut c_void) -> c_int {
|
||||
caught(|| status::OK)
|
||||
}
|
||||
|
||||
/// 静态函数表实例(v1)。
|
||||
pub fn suite_v1() -> &'static ProgressSuiteV1 {
|
||||
static SUITE: std::sync::OnceLock<ProgressSuiteV1> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| ProgressSuiteV1 {
|
||||
start: progress_start_v1,
|
||||
update: progress_update_v1,
|
||||
end: progress_end_v1,
|
||||
})
|
||||
}
|
||||
|
||||
/// 静态函数表实例(v2)。
|
||||
pub fn suite_v2() -> &'static ProgressSuiteV2 {
|
||||
static SUITE: std::sync::OnceLock<ProgressSuiteV2> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| ProgressSuiteV2 {
|
||||
start: progress_start_v2,
|
||||
update: progress_update_v2,
|
||||
end: progress_end_v2,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 取消型出口(头文件契约:非 0 = 中止)。
|
||||
unsafe extern "C" fn cancel_cb(_p: f64, _userdata: *mut c_void) -> c_int {
|
||||
1
|
||||
}
|
||||
|
||||
/// 静默:update 恒 OK;带取消回调:update → REPLY_NO 且 abort
|
||||
/// 查询为真(粘滞)。
|
||||
#[test]
|
||||
fn update_cancel_and_abort() {
|
||||
// 无报告器(渲染外):静默 OK。
|
||||
set_current(None);
|
||||
let s = suite_v1();
|
||||
unsafe {
|
||||
assert_eq!((s.update)(std::ptr::null_mut(), 0.5), status::OK);
|
||||
assert_eq!((s.start)(std::ptr::null_mut(), std::ptr::null()), status::OK);
|
||||
assert_eq!((s.end)(std::ptr::null_mut()), status::OK);
|
||||
}
|
||||
|
||||
// 带取消回调:update → REPLY_NO;is_cancelled 为真。
|
||||
set_current(Some(unsafe { ProgressReporter::new(cancel_cb, std::ptr::null_mut()) }));
|
||||
unsafe {
|
||||
assert_eq!((s.update)(std::ptr::null_mut(), 0.5), status::REPLY_NO);
|
||||
}
|
||||
assert!(is_cancelled());
|
||||
set_current(None);
|
||||
assert!(!is_cancelled());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! OfxPropertySuite v1:属性读写。
|
||||
//!
|
||||
//! 语义逐条对照 HS: ofxhPropertySuite.cpp:
|
||||
//! - 读未定义属性 / 类型不符 → kOfxStatErrUnknown(HS: propGet 的
|
||||
//! `fetchTypedProperty` 失败路径,ofxhPropertySuite.cpp:787-794);
|
||||
//! - 越界读写 → kOfxStatErrBadIndex(HS: `getValueRaw`/
|
||||
//! `setValue`,ofxhPropertySuite.cpp:257/284);
|
||||
//! - propGetN 拷贝 min(count, dimension) 个元素,不报错
|
||||
//! (HS: `getValueNRaw`,ofxhPropertySuite.cpp:271-280);
|
||||
//! - propSetN 维度变化时整体替换:本 crate 的属性维度始终跟随数组
|
||||
//! 长度(HS 对固定维度属性 count > dimension 会返回
|
||||
//! kOfxStatErrBadIndex,ofxhPropertySuite.cpp:299——本 crate 不区分
|
||||
//! 固定/可变维度,如实更宽容;三个系统 bundle 均以正确 count 调用,
|
||||
//! 无行为分歧);
|
||||
//! - 只读属性:SDK 无 kOfxStatErrReadOnly,HostSupport 也不在 suite
|
||||
//! 层拦截写(`_pluginReadOnly` 仅供宿主内部逻辑查询)——本实现
|
||||
//! 如实不拦截;若日后需要,在 PropertySet 增加只读标记再评。
|
||||
//! - propReset:HostSupport 恢复到 define 时默认值
|
||||
//! (ofxhPropertySuite.cpp:330);本 crate 不保存默认值快照,
|
||||
//! 第 1 期明确返回 kOfxStatErrUnsupported(真话比静默 no-op 安全)。
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_void, CStr, CString};
|
||||
|
||||
use crate::property::{Property, PropertySet, Value};
|
||||
use crate::suites::status;
|
||||
|
||||
/// 函数表布局(与 SDK `OfxPropertySuiteV1` 逐字段一致)。
|
||||
#[repr(C)]
|
||||
pub struct PropertySuiteV1 {
|
||||
/// propSetPointer
|
||||
pub set_pointer: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_void) -> c_int,
|
||||
/// propSetString
|
||||
pub set_string: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const c_char) -> c_int,
|
||||
/// propSetDouble
|
||||
pub set_double: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, c_double) -> c_int,
|
||||
/// propSetInt
|
||||
pub set_int: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, c_int) -> c_int,
|
||||
/// propSetPointerN
|
||||
pub set_pointer_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const *mut c_void) -> c_int,
|
||||
/// propSetStringN
|
||||
pub set_string_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const *const c_char) -> c_int,
|
||||
/// propSetDoubleN
|
||||
pub set_double_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const c_double) -> c_int,
|
||||
/// propSetIntN
|
||||
pub set_int_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *const c_int) -> c_int,
|
||||
/// propGetPointer
|
||||
pub get_pointer: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut *mut c_void) -> c_int,
|
||||
/// propGetString
|
||||
pub get_string: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut *mut c_char) -> c_int,
|
||||
/// propGetDouble
|
||||
pub get_double: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_double) -> c_int,
|
||||
/// propGetInt
|
||||
pub get_int: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_int) -> c_int,
|
||||
/// propGetPointerN
|
||||
pub get_pointer_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut *mut c_void) -> c_int,
|
||||
/// propGetStringN
|
||||
pub get_string_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut *mut c_char) -> c_int,
|
||||
/// propGetDoubleN
|
||||
pub get_double_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_double) -> c_int,
|
||||
/// propGetIntN
|
||||
pub get_int_n: unsafe extern "C" fn(*mut c_void, *const c_char, c_int, *mut c_int) -> c_int,
|
||||
/// propReset
|
||||
pub reset: unsafe extern "C" fn(*mut c_void, *const c_char) -> c_int,
|
||||
/// propGetDimension
|
||||
pub get_dimension: unsafe extern "C" fn(*mut c_void, *const c_char, *mut c_int) -> c_int,
|
||||
}
|
||||
|
||||
/// 属性值类别(suite 层的类型检查;对应 HS 的 `Property::TypeEnum`)。
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Kind {
|
||||
Int,
|
||||
Double,
|
||||
Str,
|
||||
Pointer,
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
fn of(v: &Value) -> Kind {
|
||||
match v {
|
||||
Value::Int(_) => Kind::Int,
|
||||
Value::Double(_) => Kind::Double,
|
||||
Value::String(_) => Kind::Str,
|
||||
Value::Pointer(_) => Kind::Pointer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 属性 suite 公共入口模板:解句柄 + panic 兜底。
|
||||
///
|
||||
/// 句柄按 [`crate::suites::tag`] 约定:低 3 位是对象种类标签,地址
|
||||
/// 即对象 props 字段(偏移 0);剥标签后可直接当 PropertySet 用。
|
||||
/// 裸 PropertySet 指针(标签 0,宿主内部/测试直传)原样通过。
|
||||
///
|
||||
/// `# Safety`:`handle` 必须指向活的 `PropertySet` 或已注册对象
|
||||
/// (suite 生命周期契约:宿主对象先于 suite 调用创建,后于全部调用
|
||||
/// 销毁)。
|
||||
unsafe fn caught(handle: *mut c_void, f: impl FnOnce(&PropertySet) -> Result<(), c_int>) -> c_int {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
if handle.is_null() {
|
||||
return status::ERR_BAD_HANDLE;
|
||||
}
|
||||
let set = unsafe { &*crate::suites::tag::strip(handle) };
|
||||
f(set).map_or_else(|code| code, |()| status::OK)
|
||||
}))
|
||||
.unwrap_or(status::FAILED)
|
||||
}
|
||||
|
||||
/// 属性名:空指针 / 非 UTF-8 → kOfxStatErrValue(防御性;HostSupport
|
||||
/// 直接解引用会崩)。
|
||||
///
|
||||
/// `# Safety`:`name` 必须是有效的 NUL 结尾 C 字符串(插件契约)。
|
||||
unsafe fn c_name<'a>(name: *const c_char) -> Result<&'a str, c_int> {
|
||||
if name.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
unsafe { CStr::from_ptr(name) }
|
||||
.to_str()
|
||||
.map_err(|_| status::ERR_VALUE)
|
||||
}
|
||||
|
||||
/// 越界防护:`index` 转 usize(负数 → 越界错误)。
|
||||
fn idx(index: c_int) -> Result<usize, c_int> {
|
||||
usize::try_from(index).map_err(|_| status::ERR_BAD_INDEX)
|
||||
}
|
||||
|
||||
/// 先类型后索引的取值(HS 顺序:`fetchTypedProperty` 在
|
||||
/// `getValueRaw` 之前,ofxhPropertySuite.cpp:787/794/257)。
|
||||
fn get_value<'a>(props: &'a [Property], name: &str, index: c_int, kind: Kind) -> Result<&'a Value, c_int> {
|
||||
let p = props.iter().find(|p| p.name == name).ok_or(status::ERR_UNKNOWN)?;
|
||||
// 首元素代理整条属性的类型(宿主只定义同构数组;HS 是定义期
|
||||
// 固定类型,等价)。
|
||||
let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?;
|
||||
if Kind::of(probe) != kind {
|
||||
return Err(status::ERR_UNKNOWN);
|
||||
}
|
||||
p.values.get(idx(index)?).ok_or(status::ERR_BAD_INDEX)
|
||||
}
|
||||
|
||||
// ---- propSet(单元素)-----------------------------------------------------
|
||||
|
||||
/// propSet 通用实现:类型不符/未定义 → Unknown;越界 → BadIndex。
|
||||
fn set_value(set: &PropertySet, name: &str, index: c_int, value: Value) -> Result<(), c_int> {
|
||||
set.with_locked(|props| {
|
||||
let p = props.iter_mut().find(|p| p.name == name).ok_or(status::ERR_UNKNOWN)?;
|
||||
let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?;
|
||||
if Kind::of(probe) != Kind::of(&value) {
|
||||
return Err(status::ERR_UNKNOWN);
|
||||
}
|
||||
// HS `setValue` 允许 index == size 时追加(ofxhPropertySuite.cpp:284);
|
||||
// 本 crate 维度固定语义(扩容只能经 define)——越界一律 BadIndex。
|
||||
let slot = p.values.get_mut(idx(index)?).ok_or(status::ERR_BAD_INDEX)?;
|
||||
*slot = value;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_set_pointer(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
index: c_int,
|
||||
value: *mut c_void,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
set_value(set, name, index, Value::Pointer(value))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_set_string(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
index: c_int,
|
||||
value: *const c_char,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if value.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
// C 语义:截断到首个 NUL;CString::new 不会失败
|
||||
//(from_ptr 已保证 NUL 结尾)。
|
||||
let s = CString::new(CStr::from_ptr(value).to_bytes()).unwrap();
|
||||
set_value(set, name, index, Value::String(s))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_set_double(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
index: c_int,
|
||||
value: c_double,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
set_value(set, name, index, Value::Double(value))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_set_int(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
index: c_int,
|
||||
value: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
set_value(set, name, index, Value::Int(value))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- propSetN(批量)------------------------------------------------------
|
||||
|
||||
/// 从 C 数组读出 `count` 个元素并整体写入;count != 现有维度时
|
||||
/// 整体替换(HS `setValueN` 的 resize 语义,ofxhPropertySuite.cpp:299-311)。
|
||||
fn set_values(set: &PropertySet, name: &str, count: c_int, values: Vec<Value>) -> Result<(), c_int> {
|
||||
set.with_locked(|props| {
|
||||
let p = props.iter_mut().find(|p| p.name == name).ok_or(status::ERR_UNKNOWN)?;
|
||||
// count == 0 时无类型可探(HS 仍会做 fetchTypedProperty)。
|
||||
if let Some(first) = p.values.first() {
|
||||
if let Some(v) = values.first() {
|
||||
if Kind::of(first) != Kind::of(v) {
|
||||
return Err(status::ERR_UNKNOWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
let count = idx(count)?;
|
||||
if count != p.values.len() {
|
||||
// 维度变化:整体替换(HS 是 resize + 逐位写,等价)。
|
||||
p.values = values;
|
||||
} else {
|
||||
for (i, v) in values.into_iter().enumerate() {
|
||||
p.values[i] = v;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_set_pointer_n(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
count: c_int,
|
||||
values: *const *mut c_void,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if count > 0 && values.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let vals = (0..idx(count)?)
|
||||
.map(|i| Value::Pointer(*values.add(i)))
|
||||
.collect();
|
||||
set_values(set, name, count, vals)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_set_string_n(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
count: c_int,
|
||||
values: *const *const c_char,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if count > 0 && values.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let mut vals = Vec::with_capacity(count.max(0) as usize);
|
||||
for i in 0..idx(count)? {
|
||||
let v = *values.add(i);
|
||||
if v.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
vals.push(Value::String(
|
||||
CString::new(CStr::from_ptr(v).to_bytes()).unwrap(),
|
||||
));
|
||||
}
|
||||
set_values(set, name, count, vals)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_set_double_n(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
count: c_int,
|
||||
values: *const c_double,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if count > 0 && values.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let vals = (0..idx(count)?)
|
||||
.map(|i| Value::Double(*values.add(i)))
|
||||
.collect();
|
||||
set_values(set, name, count, vals)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_set_int_n(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
count: c_int,
|
||||
values: *const c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if count > 0 && values.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let vals = (0..idx(count)?)
|
||||
.map(|i| Value::Int(*values.add(i)))
|
||||
.collect();
|
||||
set_values(set, name, count, vals)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- propGet(单元素)-----------------------------------------------------
|
||||
|
||||
/// 非字符串标量读取(克隆值写出;字符串走内驻指针路径)。
|
||||
fn get_scalar(set: &PropertySet, name: &str, index: c_int, kind: Kind) -> Result<Value, c_int> {
|
||||
set.with_locked(|props| get_value(props, name, index, kind).cloned())
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_get_pointer(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
index: c_int,
|
||||
out: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let v = get_scalar(set, name, index, Kind::Pointer)?;
|
||||
match v {
|
||||
Value::Pointer(p) => {
|
||||
*out = p;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(status::FAILED),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// propGetString:写**内驻** C 串指针(OFX 契约:指向宿主内部存储,
|
||||
/// 属性被下次修改前有效;克隆的 CString 指针会悬垂,绝不能给插件)。
|
||||
fn get_string(set: &PropertySet, name: &str, index: c_int) -> Result<*mut c_char, c_int> {
|
||||
set.with_locked(|props| {
|
||||
match get_value(props, name, index, Kind::Str)? {
|
||||
Value::String(s) => Ok(s.as_ptr() as *mut c_char),
|
||||
_ => Err(status::FAILED),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_get_string(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
index: c_int,
|
||||
out: *mut *mut c_char,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
*out = get_string(set, name, index)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_get_double(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
index: c_int,
|
||||
out: *mut c_double,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let v = get_scalar(set, name, index, Kind::Double)?;
|
||||
match v {
|
||||
Value::Double(d) => {
|
||||
*out = d;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(status::FAILED),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_get_int(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
index: c_int,
|
||||
out: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let v = get_scalar(set, name, index, Kind::Int)?;
|
||||
match v {
|
||||
Value::Int(i) => {
|
||||
*out = i;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(status::FAILED),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- propGetN(批量)------------------------------------------------------
|
||||
|
||||
/// 批量读取:拷贝 min(count, dimension) 个(HS `getValueNRaw`,
|
||||
/// ofxhPropertySuite.cpp:271-280,不报错)。
|
||||
fn get_n(set: &PropertySet, name: &str, count: c_int, kind: Kind, write: impl Fn(&Value, usize)) -> Result<(), c_int> {
|
||||
set.with_locked(|props| {
|
||||
let p = props.iter().find(|p| p.name == name).ok_or(status::ERR_UNKNOWN)?;
|
||||
let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?;
|
||||
if Kind::of(probe) != kind {
|
||||
return Err(status::ERR_UNKNOWN);
|
||||
}
|
||||
let n = idx(count)?.min(p.values.len());
|
||||
for (i, v) in p.values.iter().take(n).enumerate() {
|
||||
write(v, i);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_get_pointer_n(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
count: c_int,
|
||||
out: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if count > 0 && out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
get_n(set, name, count, Kind::Pointer, |v, i| match v {
|
||||
Value::Pointer(p) => *out.add(i) = *p,
|
||||
_ => {}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_get_string_n(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
count: c_int,
|
||||
out: *mut *mut c_char,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if count > 0 && out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
get_n(set, name, count, Kind::Str, |v, i| match v {
|
||||
Value::String(s) => *out.add(i) = s.as_ptr() as *mut c_char,
|
||||
_ => {}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_get_double_n(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
count: c_int,
|
||||
out: *mut c_double,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if count > 0 && out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
get_n(set, name, count, Kind::Double, |v, i| match v {
|
||||
Value::Double(d) => *out.add(i) = *d,
|
||||
_ => {}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_get_int_n(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
count: c_int,
|
||||
out: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if count > 0 && out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
get_n(set, name, count, Kind::Int, |v, i| match v {
|
||||
Value::Int(d) => *out.add(i) = *d,
|
||||
_ => {}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- propReset / propGetDimension -----------------------------------------
|
||||
|
||||
unsafe extern "C" fn prop_reset(handle: *mut c_void, name: *const c_char) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
// HS `reset()` 恢复到 define 时的默认值
|
||||
// (ofxhPropertySuite.cpp:330);本 crate 不保存默认值快照,
|
||||
// 第 1 期明确不支持(见模块文档)。
|
||||
let _ = (set, name);
|
||||
Err(status::ERR_UNSUPPORTED)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn prop_get_dimension(
|
||||
handle: *mut c_void,
|
||||
name: *const c_char,
|
||||
out: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
caught(handle, |set| {
|
||||
let name = c_name(name)?;
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
// HS: fetchProperty 失败(未定义)→ Unknown(ofxhPropertySuite.cpp:992)。
|
||||
let dim = set.dimension(name);
|
||||
if dim == 0 {
|
||||
return Err(status::ERR_UNKNOWN);
|
||||
}
|
||||
*out = dim as c_int;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 静态函数表实例(fetch_suite 返回其地址)。
|
||||
pub fn suite_v1() -> &'static PropertySuiteV1 {
|
||||
static SUITE: std::sync::OnceLock<PropertySuiteV1> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| PropertySuiteV1 {
|
||||
set_pointer: prop_set_pointer,
|
||||
set_string: prop_set_string,
|
||||
set_double: prop_set_double,
|
||||
set_int: prop_set_int,
|
||||
set_pointer_n: prop_set_pointer_n,
|
||||
set_string_n: prop_set_string_n,
|
||||
set_double_n: prop_set_double_n,
|
||||
set_int_n: prop_set_int_n,
|
||||
get_pointer: prop_get_pointer,
|
||||
get_string: prop_get_string,
|
||||
get_double: prop_get_double,
|
||||
get_int: prop_get_int,
|
||||
get_pointer_n: prop_get_pointer_n,
|
||||
get_string_n: prop_get_string_n,
|
||||
get_double_n: prop_get_double_n,
|
||||
get_int_n: prop_get_int_n,
|
||||
reset: prop_reset,
|
||||
get_dimension: prop_get_dimension,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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_instance(absent 时 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::stub:src=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 ← INT(num 载荷)。
|
||||
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 ← FLOAT(f[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 ← BOOL(num 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 ← COLOR(r,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 ← COLOR(alpha 忽略,恒 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 ← VEC2;Double3D ← 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-op;paramSetValue 仍成功且值本地生效。
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 未绑定节点的实例收到 instanceChanged:no-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");
|
||||
}
|
||||
// 值即时生效(事务内子命令 redo;editEnd 的 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:?}");
|
||||
});
|
||||
}
|
||||
@@ -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/>.
|
||||
|
||||
//! ofxColour(M11 §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) };
|
||||
});
|
||||
}
|
||||
@@ -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.c,build.rs 编译为共享库,
|
||||
//! 运行时装配成 oak-test-plugin.ofx.bundle):filter 上下文、
|
||||
//! Double 参数 gain、双 clip(Source/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 会 canonicalize;bundle 在临时目录下,直接给父目录。
|
||||
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")
|
||||
}
|
||||
|
||||
/// 当前平台是否有 GPU(GL 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();
|
||||
}
|
||||
@@ -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() };
|
||||
});
|
||||
}
|
||||
@@ -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_scan(ffi 模块内路径统一)。
|
||||
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_param(double):round-trip 一致;未知参数名
|
||||
/// E_NOT_FOUND;空句柄 E_INVALID;out 为 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_string:round-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 F32,alpha=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.5,alpha 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 > 0;name/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/label,count={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 range(gain:-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));
|
||||
|
||||
// secret(debug = 1;gain = 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);
|
||||
|
||||
// choice(mode:2 个选项;选项内容见 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 clip;optional 标记)。
|
||||
#[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_count:create 增、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);
|
||||
});
|
||||
}
|
||||
@@ -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 golden(EXR 容差比对)为 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(), ®ion 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()) };
|
||||
});
|
||||
}
|
||||
@@ -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 期渲染 golden(tests/ofx/frames/)+ 真 liboakrender 缺失;落地后实现并摘除"]
|
||||
fn render_golden_cpu_bitexact() {
|
||||
todo!("0 期渲染 golden 落地后实现")
|
||||
}
|
||||
|
||||
/// GL 渲染 golden:1e-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 链路落地后实现")
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// 拥有型句柄:创建计数为 1;addref 后 release 一次对象仍活;
|
||||
/// 再 release 对象销毁(用析构标志位断言)。
|
||||
#[test]
|
||||
fn owned_handle_refcount_lifecycle() {
|
||||
let drops = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// 按值传句柄的位级复制(C 侧 `OakPluginInstance` 结构体拷贝),
|
||||
// 复制方必须先 addref:refs 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 出 FFI;Err 映射为对应负码;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_handle:panic/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());
|
||||
}
|
||||
|
||||
/// Registry:register 返回唯一身份;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);
|
||||
}
|
||||
@@ -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() };
|
||||
});
|
||||
}
|
||||
@@ -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、实现 RoD(1920×1080)与 RoI
|
||||
//! (回写 region)、isIdentity 恒非透传、无 field——需要插件变体
|
||||
//! 的矩阵案(回退链、身份透传、field)标记 `// TODO(plugin)`,
|
||||
//! 随 M11 §2.4 测试插件的变体落地。
|
||||
|
||||
mod common;
|
||||
|
||||
use std::ffi::{c_char, c_void, CString};
|
||||
|
||||
use oakplugin::ffi::{
|
||||
oakplugin_host_init, oakplugin_host_scan, oakplugin_host_shutdown, oakplugin_instance_create,
|
||||
oakplugin_instance_free,
|
||||
};
|
||||
use oakplugin::handle::{get, CHandle};
|
||||
|
||||
const OK: i32 = 0;
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
|
||||
|
||||
/// 扫描并创建实例;不可用返回空句柄。
|
||||
fn create_instance() -> CHandle {
|
||||
if common::test_plugin_scan_dir().is_none() {
|
||||
common::skip("最小测试插件未构建");
|
||||
return CHandle::null();
|
||||
}
|
||||
let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap());
|
||||
let dirs = [dir.as_ptr()];
|
||||
unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) };
|
||||
let id = cs(TEST_PLUGIN_ID);
|
||||
unsafe { oakplugin_instance_create(id.as_ptr()) }
|
||||
}
|
||||
|
||||
/// 句柄 → Instance 引用。
|
||||
fn instance_of(h: &CHandle) -> Option<std::sync::Arc<oakplugin::handle::RefBox<oakplugin::instance::Instance>>> {
|
||||
unsafe { get::<std::sync::Arc<oakplugin::handle::RefBox<oakplugin::instance::Instance>>>(h) }
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// 协商顺序:测试插件声明 RGBA/F32、帧率 24——协商结果必须
|
||||
/// 原样采纳(HS: ofxhImageEffect.cpp:1686-1740 的回灌路径)。
|
||||
/// 全组合矩阵(RGBA/RGB/Alpha × Float/Half/Byte)需要插件变体:
|
||||
/// `// TODO(plugin)`。
|
||||
#[test]
|
||||
fn clip_preferences_component_depth_matrix() {
|
||||
common::with_host(|| {
|
||||
unsafe { oakplugin_host_init() };
|
||||
let mut h = create_instance();
|
||||
if h.is_null() {
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
return;
|
||||
}
|
||||
let inst = instance_of(&h).expect("句柄应可解析");
|
||||
let prefs = inst.value.get_clip_preferences().expect("协商应成功");
|
||||
assert_eq!(prefs.output_components, "OfxImageComponentRGBA");
|
||||
assert_eq!(prefs.output_bit_depth, "OfxBitDepthFloat");
|
||||
assert_eq!(prefs.frame_rate, 24.0);
|
||||
// 回灌:clip 实例属性带上了协商结果(clipGetPropertySet 读)。
|
||||
let output = inst.value.clips.iter().find(|c| c.name == "Output").unwrap();
|
||||
assert_eq!(
|
||||
output
|
||||
.props
|
||||
.get("OfxImageEffectPropComponents", 0)
|
||||
.map(|v| format!("{v:?}")),
|
||||
Some("String(\"OfxImageComponentRGBA\")".into())
|
||||
);
|
||||
unsafe { oakplugin_instance_free(&mut h) };
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
});
|
||||
}
|
||||
|
||||
/// 位深回退链(Half→Byte):宿主当前只宣告 F32 支持、插件声明
|
||||
/// F32——直通。回退链的插件侧变体 `// TODO(plugin)`。
|
||||
#[test]
|
||||
fn bit_depth_fallback_chain() {
|
||||
common::with_host(|| {
|
||||
unsafe { oakplugin_host_init() };
|
||||
let mut h = create_instance();
|
||||
if h.is_null() {
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
return;
|
||||
}
|
||||
let inst = instance_of(&h).expect("句柄应可解析");
|
||||
let prefs = inst.value.get_clip_preferences().unwrap();
|
||||
assert_eq!(prefs.output_bit_depth, "OfxBitDepthFloat");
|
||||
unsafe { oakplugin_instance_free(&mut h) };
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
});
|
||||
}
|
||||
|
||||
/// RoD:插件实现 getRegionOfDefinition → 用插件值(1920×1080,
|
||||
/// HS: ofxhImageEffect.cpp:1087-1130 的 out args 读取)。
|
||||
/// "未实现时用输入 RoD 并集"的默认语义需要插件变体:`// TODO(plugin)`。
|
||||
#[test]
|
||||
fn region_of_definition_default_and_override() {
|
||||
common::with_host(|| {
|
||||
unsafe { oakplugin_host_init() };
|
||||
let mut h = create_instance();
|
||||
if h.is_null() {
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
return;
|
||||
}
|
||||
let inst = instance_of(&h).expect("句柄应可解析");
|
||||
let rod = inst
|
||||
.value
|
||||
.get_region_of_definition(0.0, oakplugin::instance::RenderScale { x: 1.0, y: 1.0 })
|
||||
.expect("getRoD 应成功");
|
||||
assert_eq!((rod.x1, rod.y1, rod.x2, rod.y2), (0.0, 0.0, 1920.0, 1080.0));
|
||||
unsafe { oakplugin_instance_free(&mut h) };
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
});
|
||||
}
|
||||
|
||||
/// RoI:插件把 region 原样写回 Source 的 per-clip 属性
|
||||
/// ("OfxImageEffectPropRegionOfInterest_Source");返回值与
|
||||
/// 请求 region 一致(HS: getRegionsOfInterest 的 per-clip 前缀)。
|
||||
#[test]
|
||||
fn regions_of_interest_writeback() {
|
||||
common::with_host(|| {
|
||||
unsafe { oakplugin_host_init() };
|
||||
let mut h = create_instance();
|
||||
if h.is_null() {
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
return;
|
||||
}
|
||||
let inst = instance_of(&h).expect("句柄应可解析");
|
||||
let region = oakplugin::instance::OfxRectD { x1: 10.0, y1: 20.0, x2: 100.0, y2: 200.0 };
|
||||
let rois = inst
|
||||
.value
|
||||
.get_regions_of_interest(
|
||||
0.0,
|
||||
oakplugin::instance::RenderScale { x: 1.0, y: 1.0 },
|
||||
region,
|
||||
)
|
||||
.expect("getRoI 应成功");
|
||||
assert_eq!(rois.len(), 2, "clips 数与 RoI 数一致");
|
||||
// RoI 只对输入 clip 有意义:Source 回写 region;Output 是
|
||||
// 宿主预定义的默认(零),渲染驱动只用输入条目。
|
||||
let source = rois
|
||||
.get(inst.value.clips.iter().position(|c| c.name == "Source").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!((source.x1, source.y1, source.x2, source.y2), (10.0, 20.0, 100.0, 200.0));
|
||||
unsafe { oakplugin_instance_free(&mut h) };
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
});
|
||||
}
|
||||
|
||||
/// isIdentity:测试插件恒非透传 → None(render 必须真正执行)。
|
||||
/// 透传短路(Some)需要插件变体:`// TODO(plugin)`。
|
||||
#[test]
|
||||
fn is_identity_shortcircuit() {
|
||||
common::with_host(|| {
|
||||
unsafe { oakplugin_host_init() };
|
||||
let mut h = create_instance();
|
||||
if h.is_null() {
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
return;
|
||||
}
|
||||
let inst = instance_of(&h).expect("句柄应可解析");
|
||||
let identity = inst.value.is_identity(1.5).expect("isIdentity 应成功");
|
||||
assert!(identity.is_none(), "测试插件恒非透传:{identity:?}");
|
||||
unsafe { oakplugin_instance_free(&mut h) };
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
});
|
||||
}
|
||||
|
||||
/// field 透传:插件声明的 field order(OfxFieldNone)原样透传到
|
||||
/// 协商结果(宿主只透传不处理)。多值矩阵 `// TODO(plugin)`。
|
||||
#[test]
|
||||
fn field_passthrough() {
|
||||
common::with_host(|| {
|
||||
unsafe { oakplugin_host_init() };
|
||||
let mut h = create_instance();
|
||||
if h.is_null() {
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
return;
|
||||
}
|
||||
let inst = instance_of(&h).expect("句柄应可解析");
|
||||
let prefs = inst.value.get_clip_preferences().unwrap();
|
||||
assert_eq!(prefs.field, "OfxFieldNone", "插件声明的 field 应透传");
|
||||
unsafe { oakplugin_instance_free(&mut h) };
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
});
|
||||
}
|
||||
|
||||
/// begin/endSequenceRender:配对调用成功;begin 后 timeline
|
||||
/// getTimeBounds 返回该序列范围(sequence_range 接线)。
|
||||
#[test]
|
||||
fn sequence_render_brackets() {
|
||||
common::with_host(|| {
|
||||
use oakplugin::instance::OfxRangeD;
|
||||
unsafe { oakplugin_host_init() };
|
||||
let mut h = create_instance();
|
||||
if h.is_null() {
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
return;
|
||||
}
|
||||
let inst = instance_of(&h).expect("句柄应可解析");
|
||||
let range = OfxRangeD { min: 10.0, max: 200.0 };
|
||||
assert!(inst.value.begin_sequence_render(range).is_ok());
|
||||
assert!(inst.value.end_sequence_render(range).is_ok());
|
||||
|
||||
// begin → timeline 上下文带范围(经 render 设置;单测直达
|
||||
// RenderCtx 的接线见 suites::timeline 测试)。
|
||||
unsafe { oakplugin_instance_free(&mut h) };
|
||||
unsafe { oakplugin_host_shutdown() };
|
||||
});
|
||||
}
|
||||
@@ -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` 未实现 PartialEq(Pointer 无法比较),测试用 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 返回 None;dimension 为 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);
|
||||
|
||||
// 缺失属性 remove:no-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())
|
||||
}
|
||||
@@ -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_job(CPU):begin → job → end;输出像素为测试
|
||||
/// 插件常量填充(0.5 RGBA F32,alpha=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.5(FLOAT)。
|
||||
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) };
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// 假插件(仅喂给 Instance;describe 之外的字段不被触碰)。
|
||||
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 suite:define→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 suite:alloc/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 suite:describe 期 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);
|
||||
// 未找到 → BadHandle(HS: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 suite:describe 期 define→getHandle→getValue(默认值);
|
||||
/// 实例期 int/double/bool/choice/string/RGBA/2D/3D 的
|
||||
/// setValue/getValue round-trip;AtTime == 当前值。
|
||||
///
|
||||
/// 声明原含"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 → BadHandle(HS 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);
|
||||
|
||||
// String(get 返回内驻指针)。
|
||||
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_changed(bridge 期)落地。
|
||||
}
|
||||
|
||||
/// 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 suite:v1 变长参数经 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 → kOfxStatReplyYes(12)。
|
||||
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 → Failed(C++ 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_NO(13)。
|
||||
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 suite:start/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 suite:getTime/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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user