refactor: workspace layout — crates/, app at root, legacy C++ removed
Single mechanical restructure commit: - root Cargo.toml = oakapp bin + workspace; one cargo build produces oakapp, oak-cli, oak-worker, liboakengine.dylib - app/rust/src -> src/ (app at repo root, no rust/ nesting) - src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore; src/bindings/oakotio -> crates/oakotio; src/engine/rust -> crates/oakengine (keeps cdylib+staticlib+rlib) - public C headers include/<mod>/ -> crates/oakengine/include/<mod>/ - OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone) - legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/, app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets submodule, otio-install, all build-* output (~40GB) - oakstorage kept but excluded from the workspace (skeleton w/ todos); gpui excluded (own workspace) - verified: cargo build green, cargo test --workspace 1845/0 (with the documented OCIO_RS_* env override for the homebrew OCIO)
This commit is contained in:
Generated
+1657
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
[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"]
|
||||
|
||||
[dependencies]
|
||||
# 共享 ABI 类型(CHandle 等,单库化统一,见 docs/zh/plans/riir/single-lib.md)。
|
||||
# 模块桥改直接 Rust 调用(单库化,见 docs/zh/plans/riir/single-lib.md);
|
||||
# 只允许零依赖起步。确需引入的 crate 必须在 README 登记理由。
|
||||
oakcore-rs = { path = "../oakcore" }
|
||||
oakundo = { path = "../oakundo" }
|
||||
oaknode = { path = "../oaknode" }
|
||||
oakrender = { path = "../oakrender" }
|
||||
|
||||
[features]
|
||||
# 测试桩(库内状态访问器):cargo test --features test-stubs 时桥直连
|
||||
# 桩,像素路径可在无真实 GL/GPU 的环境下跑通。桩的 C ABI 符号不再
|
||||
# no_mangle(真实 oaknode/oakrender/oakundo 已作为 path 依赖链接,
|
||||
# 避免重复符号);真实链路走直接 Rust 调用。
|
||||
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",
|
||||
"-Iofx",
|
||||
"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,839 @@
|
||||
|
||||
|
||||
#ifndef _ofx_native_v1_5_aces_v1_3_ocio_v2_3_h_
|
||||
#define _ofx_native_v1_5_aces_v1_3_ocio_v2_3_h_
|
||||
|
||||
// Copyright OpenFX and contributors to the OpenFX project.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** @file ofx-native-v1.5_aces-v1.3_ocio-v2.3.h
|
||||
Contains the list of supported colourspaces.
|
||||
This file was auto-generated by scripts/genColour from ofx-native-v1.5_aces-v1.3_ocio-v2.3.
|
||||
*/
|
||||
|
||||
// For use with kOfxImageEffectPropColourManagementAvailableConfigs
|
||||
#define kOfxConfigIdentifier "ofx-native-v1.5_aces-v1.3_ocio-v2.3"
|
||||
|
||||
// Basic Colourspaces
|
||||
// These colourspaces are generic names for any colourspace with the correct attributes.
|
||||
|
||||
/** @brief ofx_display_hdr
|
||||
Any display-referred HDR video such as Rec. 2100 HLG or PQ.
|
||||
*/
|
||||
#define kOfxColourspaceOfxDisplayHdr "ofx_display_hdr"
|
||||
#define kOfxColourspaceOfxDisplayHdrLabel "OFX generic display HDR"
|
||||
#define kOfxColourspaceOfxDisplayHdrEncoding "hdr-video"
|
||||
#define kOfxColourspaceOfxDisplayHdrIsData false
|
||||
#define kOfxColourspaceOfxDisplayHdrIsBasic true
|
||||
#define kOfxColourspaceOfxDisplayHdrIsCore true
|
||||
#define kOfxColourspaceOfxDisplayHdrIsDisplay true
|
||||
|
||||
/** @brief ofx_display_sdr
|
||||
Any display-referred SDR video such as Rec. 709.
|
||||
*/
|
||||
#define kOfxColourspaceOfxDisplaySdr "ofx_display_sdr"
|
||||
#define kOfxColourspaceOfxDisplaySdrLabel "OFX generic display SDR"
|
||||
#define kOfxColourspaceOfxDisplaySdrEncoding "sdr-video"
|
||||
#define kOfxColourspaceOfxDisplaySdrIsData false
|
||||
#define kOfxColourspaceOfxDisplaySdrIsBasic true
|
||||
#define kOfxColourspaceOfxDisplaySdrIsCore true
|
||||
#define kOfxColourspaceOfxDisplaySdrIsDisplay true
|
||||
|
||||
/** @brief ofx_raw
|
||||
Image values should not be treated as colour, e.g. motion vectors or masks.
|
||||
*/
|
||||
#define kOfxColourspaceOfxRaw "ofx_raw"
|
||||
#define kOfxColourspaceOfxRawLabel "OFX generic raw"
|
||||
#define kOfxColourspaceOfxRawEncoding ""
|
||||
#define kOfxColourspaceOfxRawIsData true
|
||||
#define kOfxColourspaceOfxRawIsBasic true
|
||||
#define kOfxColourspaceOfxRawIsCore true
|
||||
#define kOfxColourspaceOfxRawIsDisplay false
|
||||
|
||||
/** @brief ofx_scene_linear
|
||||
Any scene-referred linear colourspace.
|
||||
*/
|
||||
#define kOfxColourspaceOfxSceneLinear "ofx_scene_linear"
|
||||
#define kOfxColourspaceOfxSceneLinearLabel "OFX generic scene linear"
|
||||
#define kOfxColourspaceOfxSceneLinearEncoding "scene-linear"
|
||||
#define kOfxColourspaceOfxSceneLinearIsData false
|
||||
#define kOfxColourspaceOfxSceneLinearIsBasic true
|
||||
#define kOfxColourspaceOfxSceneLinearIsCore true
|
||||
#define kOfxColourspaceOfxSceneLinearIsDisplay false
|
||||
|
||||
/** @brief ofx_scene_log
|
||||
Any scene-referred colourspace with a log transfer function.
|
||||
*/
|
||||
#define kOfxColourspaceOfxSceneLog "ofx_scene_log"
|
||||
#define kOfxColourspaceOfxSceneLogLabel "OFX generic scene log"
|
||||
#define kOfxColourspaceOfxSceneLogEncoding "log"
|
||||
#define kOfxColourspaceOfxSceneLogIsData false
|
||||
#define kOfxColourspaceOfxSceneLogIsBasic true
|
||||
#define kOfxColourspaceOfxSceneLogIsCore true
|
||||
#define kOfxColourspaceOfxSceneLogIsDisplay false
|
||||
|
||||
// Core Colourspaces
|
||||
|
||||
// srgb_display
|
||||
// Convert CIE XYZ (D65 white) to sRGB (piecewise EOTF)
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.RGBmonitor_100nits_dim.a1.0.3
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvODT.Academy.RGBmonitor_100nits_dim.a1.0.3
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.RGBmonitor_D60sim_100nits_dim.a1.0.3
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvODT.Academy.RGBmonitor_D60sim_100nits_dim.a1.0.3
|
||||
#define kOfxColourspaceSrgbDisplay "srgb_display"
|
||||
#define kOfxColourspaceSrgbDisplayLabel "sRGB - Display"
|
||||
#define kOfxColourspaceSrgbDisplayEncoding "sdr-video"
|
||||
#define kOfxColourspaceSrgbDisplayIsData false
|
||||
#define kOfxColourspaceSrgbDisplayIsBasic false
|
||||
#define kOfxColourspaceSrgbDisplayIsCore true
|
||||
#define kOfxColourspaceSrgbDisplayIsDisplay true
|
||||
|
||||
// displayp3_display
|
||||
// Convert CIE XYZ (D65 white) to Apple Display P3
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.DisplayP3_dim.a1.0.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvODT.Academy.DisplayP3_dim.a1.0.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.DisplayP3_D60sim_dim.a1.0.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvODT.Academy.DisplayP3_D60sim_dim.a1.0.0
|
||||
#define kOfxColourspaceDisplayp3Display "displayp3_display"
|
||||
#define kOfxColourspaceDisplayp3DisplayLabel "Display P3 - Display"
|
||||
#define kOfxColourspaceDisplayp3DisplayEncoding "sdr-video"
|
||||
#define kOfxColourspaceDisplayp3DisplayIsData false
|
||||
#define kOfxColourspaceDisplayp3DisplayIsBasic false
|
||||
#define kOfxColourspaceDisplayp3DisplayIsCore true
|
||||
#define kOfxColourspaceDisplayp3DisplayIsDisplay true
|
||||
|
||||
// rec1886_rec709_display
|
||||
// Convert CIE XYZ (D65 white) to Rec.1886/Rec.709 (HD video)
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.Rec709_100nits_dim.a1.0.3
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvODT.Academy.Rec709_100nits_dim.a1.0.3
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.Rec709_D60sim_100nits_dim.a1.0.3
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvODT.Academy.Rec709_D60sim_100nits_dim.a1.0.3
|
||||
#define kOfxColourspaceRec1886Rec709Display "rec1886_rec709_display"
|
||||
#define kOfxColourspaceRec1886Rec709DisplayLabel "Rec.1886 Rec.709 - Display"
|
||||
#define kOfxColourspaceRec1886Rec709DisplayEncoding "sdr-video"
|
||||
#define kOfxColourspaceRec1886Rec709DisplayIsData false
|
||||
#define kOfxColourspaceRec1886Rec709DisplayIsBasic false
|
||||
#define kOfxColourspaceRec1886Rec709DisplayIsCore true
|
||||
#define kOfxColourspaceRec1886Rec709DisplayIsDisplay true
|
||||
|
||||
// rec1886_rec2020_display
|
||||
// Convert CIE XYZ (D65 white) to Rec.1886/Rec.2020 (UHD video)
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.Rec2020_100nits_dim.a1.0.3
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvODT.Academy.Rec2020_100nits_dim.a1.0.3
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.Rec2020_P3D65limited_100nits_dim.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.Rec2020_Rec709limited_100nits_dim.a1.1.0
|
||||
#define kOfxColourspaceRec1886Rec2020Display "rec1886_rec2020_display"
|
||||
#define kOfxColourspaceRec1886Rec2020DisplayLabel "Rec.1886 Rec.2020 - Display"
|
||||
#define kOfxColourspaceRec1886Rec2020DisplayEncoding "sdr-video"
|
||||
#define kOfxColourspaceRec1886Rec2020DisplayIsData false
|
||||
#define kOfxColourspaceRec1886Rec2020DisplayIsBasic false
|
||||
#define kOfxColourspaceRec1886Rec2020DisplayIsCore true
|
||||
#define kOfxColourspaceRec1886Rec2020DisplayIsDisplay true
|
||||
|
||||
// rec2100_hlg_display
|
||||
// Convert CIE XYZ (D65 white) to Rec.2100-HLG, 1000 nit
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:RRTODT.Academy.Rec2020_1000nits_15nits_HLG.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvRRTODT.Academy.Rec2020_1000nits_15nits_HLG.a1.1.0
|
||||
#define kOfxColourspaceRec2100HlgDisplay "rec2100_hlg_display"
|
||||
#define kOfxColourspaceRec2100HlgDisplayLabel "Rec.2100-HLG - Display"
|
||||
#define kOfxColourspaceRec2100HlgDisplayEncoding "hdr-video"
|
||||
#define kOfxColourspaceRec2100HlgDisplayIsData false
|
||||
#define kOfxColourspaceRec2100HlgDisplayIsBasic false
|
||||
#define kOfxColourspaceRec2100HlgDisplayIsCore true
|
||||
#define kOfxColourspaceRec2100HlgDisplayIsDisplay true
|
||||
|
||||
// rec2100_pq_display
|
||||
// Convert CIE XYZ (D65 white) to Rec.2100-PQ
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:RRTODT.Academy.Rec2020_1000nits_15nits_ST2084.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvRRTODT.Academy.Rec2020_1000nits_15nits_ST2084.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:RRTODT.Academy.Rec2020_2000nits_15nits_ST2084.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvRRTODT.Academy.Rec2020_2000nits_15nits_ST2084.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:RRTODT.Academy.Rec2020_4000nits_15nits_ST2084.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvRRTODT.Academy.Rec2020_4000nits_15nits_ST2084.a1.1.0
|
||||
#define kOfxColourspaceRec2100PqDisplay "rec2100_pq_display"
|
||||
#define kOfxColourspaceRec2100PqDisplayLabel "Rec.2100-PQ - Display"
|
||||
#define kOfxColourspaceRec2100PqDisplayEncoding "hdr-video"
|
||||
#define kOfxColourspaceRec2100PqDisplayIsData false
|
||||
#define kOfxColourspaceRec2100PqDisplayIsBasic false
|
||||
#define kOfxColourspaceRec2100PqDisplayIsCore true
|
||||
#define kOfxColourspaceRec2100PqDisplayIsDisplay true
|
||||
|
||||
// st2084_p3d65_display
|
||||
// Convert CIE XYZ (D65 white) to ST-2084 (PQ), P3-D65 primaries
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:RRTODT.Academy.P3D65_1000nits_15nits_ST2084.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvRRTODT.Academy.P3D65_1000nits_15nits_ST2084.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:RRTODT.Academy.P3D65_2000nits_15nits_ST2084.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvRRTODT.Academy.P3D65_2000nits_15nits_ST2084.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:RRTODT.Academy.P3D65_4000nits_15nits_ST2084.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvRRTODT.Academy.P3D65_4000nits_15nits_ST2084.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:RRTODT.Academy.P3D65_108nits_7point2nits_ST2084.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvRRTODT.Academy.P3D65_108nits_7point2nits_ST2084.a1.1.0
|
||||
#define kOfxColourspaceSt2084P3d65Display "st2084_p3d65_display"
|
||||
#define kOfxColourspaceSt2084P3d65DisplayLabel "ST2084-P3-D65 - Display"
|
||||
#define kOfxColourspaceSt2084P3d65DisplayEncoding "hdr-video"
|
||||
#define kOfxColourspaceSt2084P3d65DisplayIsData false
|
||||
#define kOfxColourspaceSt2084P3d65DisplayIsBasic false
|
||||
#define kOfxColourspaceSt2084P3d65DisplayIsCore true
|
||||
#define kOfxColourspaceSt2084P3d65DisplayIsDisplay true
|
||||
|
||||
// p3d65_display
|
||||
// Convert CIE XYZ (D65 white) to Gamma 2.6, P3-D65
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.P3D65_48nits.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvODT.Academy.P3D65_48nits.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.P3D65_Rec709limited_48nits.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.P3D65_D60sim_48nits.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvODT.Academy.P3D65_D60sim_48nits.a1.1.0
|
||||
#define kOfxColourspaceP3d65Display "p3d65_display"
|
||||
#define kOfxColourspaceP3d65DisplayLabel "P3-D65 - Display"
|
||||
#define kOfxColourspaceP3d65DisplayEncoding "sdr-video"
|
||||
#define kOfxColourspaceP3d65DisplayIsData false
|
||||
#define kOfxColourspaceP3d65DisplayIsBasic false
|
||||
#define kOfxColourspaceP3d65DisplayIsCore true
|
||||
#define kOfxColourspaceP3d65DisplayIsDisplay true
|
||||
|
||||
// ACES2065-1
|
||||
// The "Academy Color Encoding System" reference colorspace.
|
||||
#define kOfxColourspaceACES20651 "ACES2065-1"
|
||||
#define kOfxColourspaceACES20651Label "ACES2065-1"
|
||||
#define kOfxColourspaceACES20651Encoding "scene-linear"
|
||||
#define kOfxColourspaceACES20651IsData false
|
||||
#define kOfxColourspaceACES20651IsBasic false
|
||||
#define kOfxColourspaceACES20651IsCore true
|
||||
#define kOfxColourspaceACES20651IsDisplay false
|
||||
|
||||
// ACEScc
|
||||
// Convert ACEScc to ACES2065-1
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACEScc_to_ACES.a1.0.3
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_ACEScc.a1.0.3
|
||||
#define kOfxColourspaceACEScc "ACEScc"
|
||||
#define kOfxColourspaceACESccLabel "ACEScc"
|
||||
#define kOfxColourspaceACESccEncoding "log"
|
||||
#define kOfxColourspaceACESccIsData false
|
||||
#define kOfxColourspaceACESccIsBasic false
|
||||
#define kOfxColourspaceACESccIsCore true
|
||||
#define kOfxColourspaceACESccIsDisplay false
|
||||
|
||||
// ACEScct
|
||||
// Convert ACEScct to ACES2065-1
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACEScct_to_ACES.a1.0.3
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_ACEScct.a1.0.3
|
||||
#define kOfxColourspaceACEScct "ACEScct"
|
||||
#define kOfxColourspaceACEScctLabel "ACEScct"
|
||||
#define kOfxColourspaceACEScctEncoding "log"
|
||||
#define kOfxColourspaceACEScctIsData false
|
||||
#define kOfxColourspaceACEScctIsBasic false
|
||||
#define kOfxColourspaceACEScctIsCore true
|
||||
#define kOfxColourspaceACEScctIsDisplay false
|
||||
|
||||
// ACEScg
|
||||
// Convert ACEScg to ACES2065-1
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACEScg_to_ACES.a1.0.3
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_ACEScg.a1.0.3
|
||||
#define kOfxColourspaceACEScg "ACEScg"
|
||||
#define kOfxColourspaceACEScgLabel "ACEScg"
|
||||
#define kOfxColourspaceACEScgEncoding "scene-linear"
|
||||
#define kOfxColourspaceACEScgIsData false
|
||||
#define kOfxColourspaceACEScgIsBasic false
|
||||
#define kOfxColourspaceACEScgIsCore true
|
||||
#define kOfxColourspaceACEScgIsDisplay false
|
||||
|
||||
// lin_p3d65
|
||||
// Convert ACES2065-1 to linear P3 primaries, D65 white point
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:OCIO:Utility:AP0_to_Linear_P3-D65:1.0
|
||||
#define kOfxColourspaceLinP3d65 "lin_p3d65"
|
||||
#define kOfxColourspaceLinP3d65Label "Linear P3-D65"
|
||||
#define kOfxColourspaceLinP3d65Encoding "scene-linear"
|
||||
#define kOfxColourspaceLinP3d65IsData false
|
||||
#define kOfxColourspaceLinP3d65IsBasic false
|
||||
#define kOfxColourspaceLinP3d65IsCore true
|
||||
#define kOfxColourspaceLinP3d65IsDisplay false
|
||||
|
||||
// lin_rec2020
|
||||
// Convert ACES2065-1 to linear Rec.2020 primaries, D65 white point
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:OCIO:Utility:AP0_to_Linear_Rec2020:1.0
|
||||
#define kOfxColourspaceLinRec2020 "lin_rec2020"
|
||||
#define kOfxColourspaceLinRec2020Label "Linear Rec.2020"
|
||||
#define kOfxColourspaceLinRec2020Encoding "scene-linear"
|
||||
#define kOfxColourspaceLinRec2020IsData false
|
||||
#define kOfxColourspaceLinRec2020IsBasic false
|
||||
#define kOfxColourspaceLinRec2020IsCore true
|
||||
#define kOfxColourspaceLinRec2020IsDisplay false
|
||||
|
||||
// lin_rec709_srgb
|
||||
// Convert ACES2065-1 to linear Rec.709 primaries, D65 white point
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:OCIO:Utility:AP0_to_Linear_Rec709:1.0
|
||||
#define kOfxColourspaceLinRec709Srgb "lin_rec709_srgb"
|
||||
#define kOfxColourspaceLinRec709SrgbLabel "Linear Rec.709 (sRGB)"
|
||||
#define kOfxColourspaceLinRec709SrgbEncoding "scene-linear"
|
||||
#define kOfxColourspaceLinRec709SrgbIsData false
|
||||
#define kOfxColourspaceLinRec709SrgbIsBasic false
|
||||
#define kOfxColourspaceLinRec709SrgbIsCore true
|
||||
#define kOfxColourspaceLinRec709SrgbIsDisplay false
|
||||
|
||||
// g18_rec709_tx
|
||||
// Convert ACES2065-1 to 1.8 gamma-corrected Rec.709 primaries, D65 white point
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:OCIO:Utility:AP0_to_Gamma1.8_Rec709-Texture:1.0
|
||||
#define kOfxColourspaceG18Rec709Tx "g18_rec709_tx"
|
||||
#define kOfxColourspaceG18Rec709TxLabel "Gamma 1.8 Rec.709 - Texture"
|
||||
#define kOfxColourspaceG18Rec709TxEncoding "sdr-video"
|
||||
#define kOfxColourspaceG18Rec709TxIsData false
|
||||
#define kOfxColourspaceG18Rec709TxIsBasic false
|
||||
#define kOfxColourspaceG18Rec709TxIsCore true
|
||||
#define kOfxColourspaceG18Rec709TxIsDisplay false
|
||||
|
||||
// g22_ap1_tx
|
||||
// Convert ACES2065-1 to 2.2 gamma-corrected AP1 primaries, ACES ~=D60 white point
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:OCIO:Utility:AP0_to_Gamma2.2_AP1-Texture:1.0
|
||||
#define kOfxColourspaceG22Ap1Tx "g22_ap1_tx"
|
||||
#define kOfxColourspaceG22Ap1TxLabel "Gamma 2.2 AP1 - Texture"
|
||||
#define kOfxColourspaceG22Ap1TxEncoding "sdr-video"
|
||||
#define kOfxColourspaceG22Ap1TxIsData false
|
||||
#define kOfxColourspaceG22Ap1TxIsBasic false
|
||||
#define kOfxColourspaceG22Ap1TxIsCore true
|
||||
#define kOfxColourspaceG22Ap1TxIsDisplay false
|
||||
|
||||
// g22_rec709_tx
|
||||
// Convert ACES2065-1 to 2.2 gamma-corrected Rec.709 primaries, D65 white point
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:OCIO:Utility:AP0_to_Gamma2.2_Rec709-Texture:1.0
|
||||
#define kOfxColourspaceG22Rec709Tx "g22_rec709_tx"
|
||||
#define kOfxColourspaceG22Rec709TxLabel "Gamma 2.2 Rec.709 - Texture"
|
||||
#define kOfxColourspaceG22Rec709TxEncoding "sdr-video"
|
||||
#define kOfxColourspaceG22Rec709TxIsData false
|
||||
#define kOfxColourspaceG22Rec709TxIsBasic false
|
||||
#define kOfxColourspaceG22Rec709TxIsCore true
|
||||
#define kOfxColourspaceG22Rec709TxIsDisplay false
|
||||
|
||||
// g24_rec709_tx
|
||||
// Convert ACES2065-1 to 2.4 gamma-corrected Rec.709 primaries, D65 white point
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:OCIO:Utility:AP0_to_Gamma2.4_Rec709-Texture:1.0
|
||||
#define kOfxColourspaceG24Rec709Tx "g24_rec709_tx"
|
||||
#define kOfxColourspaceG24Rec709TxLabel "Gamma 2.4 Rec.709 - Texture"
|
||||
#define kOfxColourspaceG24Rec709TxEncoding "sdr-video"
|
||||
#define kOfxColourspaceG24Rec709TxIsData false
|
||||
#define kOfxColourspaceG24Rec709TxIsBasic false
|
||||
#define kOfxColourspaceG24Rec709TxIsCore true
|
||||
#define kOfxColourspaceG24Rec709TxIsDisplay false
|
||||
|
||||
// srgb_encoded_ap1_tx
|
||||
// Convert ACES2065-1 to sRGB Encoded AP1 primaries, ACES ~=D60 white point
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:OCIO:Utility:AP0_to_sRGB_Encoded_AP1-Texture:1.0
|
||||
#define kOfxColourspaceSrgbEncodedAp1Tx "srgb_encoded_ap1_tx"
|
||||
#define kOfxColourspaceSrgbEncodedAp1TxLabel "sRGB Encoded AP1 - Texture"
|
||||
#define kOfxColourspaceSrgbEncodedAp1TxEncoding "sdr-video"
|
||||
#define kOfxColourspaceSrgbEncodedAp1TxIsData false
|
||||
#define kOfxColourspaceSrgbEncodedAp1TxIsBasic false
|
||||
#define kOfxColourspaceSrgbEncodedAp1TxIsCore true
|
||||
#define kOfxColourspaceSrgbEncodedAp1TxIsDisplay false
|
||||
|
||||
// srgb_encoded_p3d65_tx
|
||||
// Convert ACES2065-1 to sRGB Encoded P3-D65 primaries, D65 white point
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:OCIO:Utility:AP0_to_sRGB_Encoded_P3-D65-Texture:1.0
|
||||
#define kOfxColourspaceSrgbEncodedP3d65Tx "srgb_encoded_p3d65_tx"
|
||||
#define kOfxColourspaceSrgbEncodedP3d65TxLabel "sRGB Encoded P3-D65 - Texture"
|
||||
#define kOfxColourspaceSrgbEncodedP3d65TxEncoding "sdr-video"
|
||||
#define kOfxColourspaceSrgbEncodedP3d65TxIsData false
|
||||
#define kOfxColourspaceSrgbEncodedP3d65TxIsBasic false
|
||||
#define kOfxColourspaceSrgbEncodedP3d65TxIsCore true
|
||||
#define kOfxColourspaceSrgbEncodedP3d65TxIsDisplay false
|
||||
|
||||
// srgb_tx
|
||||
// Convert ACES2065-1 to sRGB
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:OCIO:Utility:AP0_to_sRGB-Texture:1.0
|
||||
#define kOfxColourspaceSrgbTx "srgb_tx"
|
||||
#define kOfxColourspaceSrgbTxLabel "sRGB - Texture"
|
||||
#define kOfxColourspaceSrgbTxEncoding "sdr-video"
|
||||
#define kOfxColourspaceSrgbTxIsData false
|
||||
#define kOfxColourspaceSrgbTxIsBasic false
|
||||
#define kOfxColourspaceSrgbTxIsCore true
|
||||
#define kOfxColourspaceSrgbTxIsDisplay false
|
||||
|
||||
// Raw
|
||||
// The utility "Raw" colorspace.
|
||||
#define kOfxColourspaceRaw "Raw"
|
||||
#define kOfxColourspaceRawLabel "Raw"
|
||||
#define kOfxColourspaceRawEncoding ""
|
||||
#define kOfxColourspaceRawIsData true
|
||||
#define kOfxColourspaceRawIsBasic false
|
||||
#define kOfxColourspaceRawIsCore true
|
||||
#define kOfxColourspaceRawIsDisplay false
|
||||
|
||||
// Non-core Colourspaces
|
||||
|
||||
// CIE-XYZ-D65
|
||||
// The "CIE XYZ (D65)" display connection colorspace.
|
||||
#define kOfxColourspaceCIEXYZD65 "CIE-XYZ-D65"
|
||||
#define kOfxColourspaceCIEXYZD65Label "CIE-XYZ-D65"
|
||||
#define kOfxColourspaceCIEXYZD65Encoding ""
|
||||
#define kOfxColourspaceCIEXYZD65IsData false
|
||||
#define kOfxColourspaceCIEXYZD65IsBasic false
|
||||
#define kOfxColourspaceCIEXYZD65IsCore false
|
||||
#define kOfxColourspaceCIEXYZD65IsDisplay true
|
||||
|
||||
// p3d60_display
|
||||
// Convert CIE XYZ (D65 white) to Gamma 2.6, P3-D60 (Bradford adaptation)
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.P3D60_48nits.a1.0.3
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvODT.Academy.P3D60_48nits.a1.0.3
|
||||
#define kOfxColourspaceP3d60Display "p3d60_display"
|
||||
#define kOfxColourspaceP3d60DisplayLabel "P3-D60 - Display"
|
||||
#define kOfxColourspaceP3d60DisplayEncoding "sdr-video"
|
||||
#define kOfxColourspaceP3d60DisplayIsData false
|
||||
#define kOfxColourspaceP3d60DisplayIsBasic false
|
||||
#define kOfxColourspaceP3d60DisplayIsCore false
|
||||
#define kOfxColourspaceP3d60DisplayIsDisplay true
|
||||
|
||||
// p3_dci_display
|
||||
// Convert CIE XYZ (D65 white) to Gamma 2.6, P3-DCI (DCI white with Bradford adaptation)
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.P3DCI_48nits.a1.0.3
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvODT.Academy.P3DCI_48nits.a1.0.3
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ODT.Academy.P3DCI_D65sim_48nits.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:InvODT.Academy.P3DCI_D65sim_48nits.a1.1.0
|
||||
#define kOfxColourspaceP3DciDisplay "p3_dci_display"
|
||||
#define kOfxColourspaceP3DciDisplayLabel "P3-DCI - Display"
|
||||
#define kOfxColourspaceP3DciDisplayEncoding "sdr-video"
|
||||
#define kOfxColourspaceP3DciDisplayIsData false
|
||||
#define kOfxColourspaceP3DciDisplayIsBasic false
|
||||
#define kOfxColourspaceP3DciDisplayIsCore false
|
||||
#define kOfxColourspaceP3DciDisplayIsDisplay true
|
||||
|
||||
// ADX10
|
||||
// Convert ADX10 to ACES2065-1
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ADX10_to_ACES.a1.0.3
|
||||
#define kOfxColourspaceADX10 "ADX10"
|
||||
#define kOfxColourspaceADX10Label "ADX10"
|
||||
#define kOfxColourspaceADX10Encoding "log"
|
||||
#define kOfxColourspaceADX10IsData false
|
||||
#define kOfxColourspaceADX10IsBasic false
|
||||
#define kOfxColourspaceADX10IsCore false
|
||||
#define kOfxColourspaceADX10IsDisplay false
|
||||
|
||||
// ADX16
|
||||
// Convert ADX16 to ACES2065-1
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ADX16_to_ACES.a1.0.3
|
||||
#define kOfxColourspaceADX16 "ADX16"
|
||||
#define kOfxColourspaceADX16Label "ADX16"
|
||||
#define kOfxColourspaceADX16Encoding "log"
|
||||
#define kOfxColourspaceADX16IsData false
|
||||
#define kOfxColourspaceADX16IsBasic false
|
||||
#define kOfxColourspaceADX16IsCore false
|
||||
#define kOfxColourspaceADX16IsDisplay false
|
||||
|
||||
// lin_arri_wide_gamut_3
|
||||
// Convert Linear ARRI Wide Gamut 3 to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:ARRI:Input:Linear_ARRI_Wide_Gamut_3_to_ACES2065-1:1.0
|
||||
#define kOfxColourspaceLinArriWideGamut3 "lin_arri_wide_gamut_3"
|
||||
#define kOfxColourspaceLinArriWideGamut3Label "Linear ARRI Wide Gamut 3"
|
||||
#define kOfxColourspaceLinArriWideGamut3Encoding "scene-linear"
|
||||
#define kOfxColourspaceLinArriWideGamut3IsData false
|
||||
#define kOfxColourspaceLinArriWideGamut3IsBasic false
|
||||
#define kOfxColourspaceLinArriWideGamut3IsCore false
|
||||
#define kOfxColourspaceLinArriWideGamut3IsDisplay false
|
||||
|
||||
// arri_logc3_ei800
|
||||
// Convert ARRI LogC3 (EI800) to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:ARRI:Input:ARRI_LogC3_EI800_to_ACES2065-1:1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:IDT.ARRI.Alexa-v3-logC-EI800.a1.v2
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_LogC_EI800_AWG.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.LogC_EI800_AWG_to_ACES.a1.1.0
|
||||
#define kOfxColourspaceArriLogc3Ei800 "arri_logc3_ei800"
|
||||
#define kOfxColourspaceArriLogc3Ei800Label "ARRI LogC3 (EI800)"
|
||||
#define kOfxColourspaceArriLogc3Ei800Encoding "log"
|
||||
#define kOfxColourspaceArriLogc3Ei800IsData false
|
||||
#define kOfxColourspaceArriLogc3Ei800IsBasic false
|
||||
#define kOfxColourspaceArriLogc3Ei800IsCore false
|
||||
#define kOfxColourspaceArriLogc3Ei800IsDisplay false
|
||||
|
||||
// lin_arri_wide_gamut_4
|
||||
// Convert Linear ARRI Wide Gamut 4 to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:ARRI:Input:Linear_ARRI_Wide_Gamut_4_to_ACES2065-1:1.0
|
||||
#define kOfxColourspaceLinArriWideGamut4 "lin_arri_wide_gamut_4"
|
||||
#define kOfxColourspaceLinArriWideGamut4Label "Linear ARRI Wide Gamut 4"
|
||||
#define kOfxColourspaceLinArriWideGamut4Encoding "scene-linear"
|
||||
#define kOfxColourspaceLinArriWideGamut4IsData false
|
||||
#define kOfxColourspaceLinArriWideGamut4IsBasic false
|
||||
#define kOfxColourspaceLinArriWideGamut4IsCore false
|
||||
#define kOfxColourspaceLinArriWideGamut4IsDisplay false
|
||||
|
||||
// arri_logc4
|
||||
// Convert ARRI LogC4 to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:ARRI:Input:ARRI_LogC4_to_ACES2065-1:1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:IDT.ARRI.ARRI-LogC4.a1.v1
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.LogC4_to_ACES.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_LogC4.a1.1.0
|
||||
#define kOfxColourspaceArriLogc4 "arri_logc4"
|
||||
#define kOfxColourspaceArriLogc4Label "ARRI LogC4"
|
||||
#define kOfxColourspaceArriLogc4Encoding "log"
|
||||
#define kOfxColourspaceArriLogc4IsData false
|
||||
#define kOfxColourspaceArriLogc4IsBasic false
|
||||
#define kOfxColourspaceArriLogc4IsCore false
|
||||
#define kOfxColourspaceArriLogc4IsDisplay false
|
||||
|
||||
// bmdfilm_widegamut_gen5
|
||||
// Convert Blackmagic Film Wide Gamut (Gen 5) to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:BlackmagicDesign:Input:BMDFilm_WideGamut_Gen5_to_ACES2065-1:1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:IDT.BlackmagicDesign.BMDFilm_WideGamut_Gen5.a1.v1
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_BMDFilm_WideGamut_Gen5.a1.v1
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.BMDFilm_WideGamut_Gen5_to_ACES.a1.v1
|
||||
#define kOfxColourspaceBmdfilmWidegamutGen5 "bmdfilm_widegamut_gen5"
|
||||
#define kOfxColourspaceBmdfilmWidegamutGen5Label "BMDFilm WideGamut Gen5"
|
||||
#define kOfxColourspaceBmdfilmWidegamutGen5Encoding "log"
|
||||
#define kOfxColourspaceBmdfilmWidegamutGen5IsData false
|
||||
#define kOfxColourspaceBmdfilmWidegamutGen5IsBasic false
|
||||
#define kOfxColourspaceBmdfilmWidegamutGen5IsCore false
|
||||
#define kOfxColourspaceBmdfilmWidegamutGen5IsDisplay false
|
||||
|
||||
// davinci_intermediate_widegamut
|
||||
// Convert DaVinci Intermediate Wide Gamut to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:BlackmagicDesign:Input:DaVinci_Intermediate_WideGamut_to_ACES2065-1:1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.DaVinci_Intermediate_WideGamut_to_ACES.a1.v1
|
||||
#define kOfxColourspaceDavinciIntermediateWidegamut "davinci_intermediate_widegamut"
|
||||
#define kOfxColourspaceDavinciIntermediateWidegamutLabel "DaVinci Intermediate WideGamut"
|
||||
#define kOfxColourspaceDavinciIntermediateWidegamutEncoding "log"
|
||||
#define kOfxColourspaceDavinciIntermediateWidegamutIsData false
|
||||
#define kOfxColourspaceDavinciIntermediateWidegamutIsBasic false
|
||||
#define kOfxColourspaceDavinciIntermediateWidegamutIsCore false
|
||||
#define kOfxColourspaceDavinciIntermediateWidegamutIsDisplay false
|
||||
|
||||
// lin_bmd_widegamut_gen5
|
||||
// Convert Linear Blackmagic Wide Gamut (Gen 5) to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:BlackmagicDesign:Input:Linear_BMD_WideGamut_Gen5_to_ACES2065-1:1.0
|
||||
#define kOfxColourspaceLinBmdWidegamutGen5 "lin_bmd_widegamut_gen5"
|
||||
#define kOfxColourspaceLinBmdWidegamutGen5Label "Linear BMD WideGamut Gen5"
|
||||
#define kOfxColourspaceLinBmdWidegamutGen5Encoding "scene-linear"
|
||||
#define kOfxColourspaceLinBmdWidegamutGen5IsData false
|
||||
#define kOfxColourspaceLinBmdWidegamutGen5IsBasic false
|
||||
#define kOfxColourspaceLinBmdWidegamutGen5IsCore false
|
||||
#define kOfxColourspaceLinBmdWidegamutGen5IsDisplay false
|
||||
|
||||
// lin_davinci_widegamut
|
||||
// Convert Linear DaVinci Wide Gamut to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:BlackmagicDesign:Input:Linear_DaVinci_WideGamut_to_ACES2065-1:1.0
|
||||
#define kOfxColourspaceLinDavinciWidegamut "lin_davinci_widegamut"
|
||||
#define kOfxColourspaceLinDavinciWidegamutLabel "Linear DaVinci WideGamut"
|
||||
#define kOfxColourspaceLinDavinciWidegamutEncoding "scene-linear"
|
||||
#define kOfxColourspaceLinDavinciWidegamutIsData false
|
||||
#define kOfxColourspaceLinDavinciWidegamutIsBasic false
|
||||
#define kOfxColourspaceLinDavinciWidegamutIsCore false
|
||||
#define kOfxColourspaceLinDavinciWidegamutIsDisplay false
|
||||
|
||||
// canonlog2_cinemagamut_d55
|
||||
// Convert Canon Log 2 Cinema Gamut (Daylight) to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Canon:Input:CanonLog2_CinemaGamut-D55_to_ACES2065-1:1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.CLog2_CGamut_to_ACES.a1.1.0
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_CLog2_CGamut.a1.1.0
|
||||
#define kOfxColourspaceCanonlog2CinemagamutD55 "canonlog2_cinemagamut_d55"
|
||||
#define kOfxColourspaceCanonlog2CinemagamutD55Label "CanonLog2 CinemaGamut D55"
|
||||
#define kOfxColourspaceCanonlog2CinemagamutD55Encoding "log"
|
||||
#define kOfxColourspaceCanonlog2CinemagamutD55IsData false
|
||||
#define kOfxColourspaceCanonlog2CinemagamutD55IsBasic false
|
||||
#define kOfxColourspaceCanonlog2CinemagamutD55IsCore false
|
||||
#define kOfxColourspaceCanonlog2CinemagamutD55IsDisplay false
|
||||
|
||||
// canonlog3_cinemagamut_d55
|
||||
// Convert Canon Log 3 Cinema Gamut (Daylight) to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Canon:Input:CanonLog3_CinemaGamut-D55_to_ACES2065-1:1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.CLog3_CGamut_to_ACES.a1.1.0
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_CLog3_CGamut.a1.1.0
|
||||
#define kOfxColourspaceCanonlog3CinemagamutD55 "canonlog3_cinemagamut_d55"
|
||||
#define kOfxColourspaceCanonlog3CinemagamutD55Label "CanonLog3 CinemaGamut D55"
|
||||
#define kOfxColourspaceCanonlog3CinemagamutD55Encoding "log"
|
||||
#define kOfxColourspaceCanonlog3CinemagamutD55IsData false
|
||||
#define kOfxColourspaceCanonlog3CinemagamutD55IsBasic false
|
||||
#define kOfxColourspaceCanonlog3CinemagamutD55IsCore false
|
||||
#define kOfxColourspaceCanonlog3CinemagamutD55IsDisplay false
|
||||
|
||||
// lin_cinemagamut_d55
|
||||
// Convert Linear Canon Cinema Gamut (Daylight) to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Canon:Input:Linear-CinemaGamut-D55_to_ACES2065-1:1.0
|
||||
#define kOfxColourspaceLinCinemagamutD55 "lin_cinemagamut_d55"
|
||||
#define kOfxColourspaceLinCinemagamutD55Label "Linear CinemaGamut D55"
|
||||
#define kOfxColourspaceLinCinemagamutD55Encoding "scene-linear"
|
||||
#define kOfxColourspaceLinCinemagamutD55IsData false
|
||||
#define kOfxColourspaceLinCinemagamutD55IsBasic false
|
||||
#define kOfxColourspaceLinCinemagamutD55IsCore false
|
||||
#define kOfxColourspaceLinCinemagamutD55IsDisplay false
|
||||
|
||||
// lin_vgamut
|
||||
// Convert Linear Panasonic V-Gamut to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Panasonic:Input:Linear_VGamut_to_ACES2065-1:1.0
|
||||
#define kOfxColourspaceLinVgamut "lin_vgamut"
|
||||
#define kOfxColourspaceLinVgamutLabel "Linear V-Gamut"
|
||||
#define kOfxColourspaceLinVgamutEncoding "scene-linear"
|
||||
#define kOfxColourspaceLinVgamutIsData false
|
||||
#define kOfxColourspaceLinVgamutIsBasic false
|
||||
#define kOfxColourspaceLinVgamutIsCore false
|
||||
#define kOfxColourspaceLinVgamutIsDisplay false
|
||||
|
||||
// vlog_vgamut
|
||||
// Convert Panasonic V-Log - V-Gamut to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Panasonic:Input:VLog_VGamut_to_ACES2065-1:1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.VLog_VGamut_to_ACES.a1.1.0
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_VLog_VGamut.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:IDT.Panasonic.VLog_VGamut.a1.v1
|
||||
#define kOfxColourspaceVlogVgamut "vlog_vgamut"
|
||||
#define kOfxColourspaceVlogVgamutLabel "V-Log V-Gamut"
|
||||
#define kOfxColourspaceVlogVgamutEncoding "log"
|
||||
#define kOfxColourspaceVlogVgamutIsData false
|
||||
#define kOfxColourspaceVlogVgamutIsBasic false
|
||||
#define kOfxColourspaceVlogVgamutIsCore false
|
||||
#define kOfxColourspaceVlogVgamutIsDisplay false
|
||||
|
||||
// lin_redwidegamutrgb
|
||||
// Convert Linear REDWideGamutRGB to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:RED:Input:Linear_REDWideGamutRGB_to_ACES2065-1:1.0
|
||||
#define kOfxColourspaceLinRedwidegamutrgb "lin_redwidegamutrgb"
|
||||
#define kOfxColourspaceLinRedwidegamutrgbLabel "Linear REDWideGamutRGB"
|
||||
#define kOfxColourspaceLinRedwidegamutrgbEncoding "scene-linear"
|
||||
#define kOfxColourspaceLinRedwidegamutrgbIsData false
|
||||
#define kOfxColourspaceLinRedwidegamutrgbIsBasic false
|
||||
#define kOfxColourspaceLinRedwidegamutrgbIsCore false
|
||||
#define kOfxColourspaceLinRedwidegamutrgbIsDisplay false
|
||||
|
||||
// log3g10_redwidegamutrgb
|
||||
// Convert RED Log3G10 REDWideGamutRGB to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:RED:Input:Log3G10_REDWideGamutRGB_to_ACES2065-1:1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:IDT.RED.Log3G10_REDWideGamutRGB.a1.v1
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_Log3G10_RWG.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.Log3G10_RWG_to_ACES.a1.1.0
|
||||
#define kOfxColourspaceLog3g10Redwidegamutrgb "log3g10_redwidegamutrgb"
|
||||
#define kOfxColourspaceLog3g10RedwidegamutrgbLabel "Log3G10 REDWideGamutRGB"
|
||||
#define kOfxColourspaceLog3g10RedwidegamutrgbEncoding "log"
|
||||
#define kOfxColourspaceLog3g10RedwidegamutrgbIsData false
|
||||
#define kOfxColourspaceLog3g10RedwidegamutrgbIsBasic false
|
||||
#define kOfxColourspaceLog3g10RedwidegamutrgbIsCore false
|
||||
#define kOfxColourspaceLog3g10RedwidegamutrgbIsDisplay false
|
||||
|
||||
// lin_sgamut3
|
||||
// Convert Linear S-Gamut3 to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Sony:Input:Linear_SGamut3_to_ACES2065-1:1.0
|
||||
#define kOfxColourspaceLinSgamut3 "lin_sgamut3"
|
||||
#define kOfxColourspaceLinSgamut3Label "Linear S-Gamut3"
|
||||
#define kOfxColourspaceLinSgamut3Encoding "scene-linear"
|
||||
#define kOfxColourspaceLinSgamut3IsData false
|
||||
#define kOfxColourspaceLinSgamut3IsBasic false
|
||||
#define kOfxColourspaceLinSgamut3IsCore false
|
||||
#define kOfxColourspaceLinSgamut3IsDisplay false
|
||||
|
||||
// lin_sgamut3cine
|
||||
// Convert Linear S-Gamut3.Cine to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Sony:Input:Linear_SGamut3Cine_to_ACES2065-1:1.0
|
||||
#define kOfxColourspaceLinSgamut3cine "lin_sgamut3cine"
|
||||
#define kOfxColourspaceLinSgamut3cineLabel "Linear S-Gamut3.Cine"
|
||||
#define kOfxColourspaceLinSgamut3cineEncoding "scene-linear"
|
||||
#define kOfxColourspaceLinSgamut3cineIsData false
|
||||
#define kOfxColourspaceLinSgamut3cineIsBasic false
|
||||
#define kOfxColourspaceLinSgamut3cineIsCore false
|
||||
#define kOfxColourspaceLinSgamut3cineIsDisplay false
|
||||
|
||||
// lin_venice_sgamut3
|
||||
// Convert Linear Venice S-Gamut3 to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Sony:Input:Linear_Venice_SGamut3_to_ACES2065-1:1.0
|
||||
#define kOfxColourspaceLinVeniceSgamut3 "lin_venice_sgamut3"
|
||||
#define kOfxColourspaceLinVeniceSgamut3Label "Linear Venice S-Gamut3"
|
||||
#define kOfxColourspaceLinVeniceSgamut3Encoding "scene-linear"
|
||||
#define kOfxColourspaceLinVeniceSgamut3IsData false
|
||||
#define kOfxColourspaceLinVeniceSgamut3IsBasic false
|
||||
#define kOfxColourspaceLinVeniceSgamut3IsCore false
|
||||
#define kOfxColourspaceLinVeniceSgamut3IsDisplay false
|
||||
|
||||
// lin_venice_sgamut3cine
|
||||
// Convert Linear Venice S-Gamut3.Cine to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Sony:Input:Linear_Venice_SGamut3Cine_to_ACES2065-1:1.0
|
||||
#define kOfxColourspaceLinVeniceSgamut3cine "lin_venice_sgamut3cine"
|
||||
#define kOfxColourspaceLinVeniceSgamut3cineLabel "Linear Venice S-Gamut3.Cine"
|
||||
#define kOfxColourspaceLinVeniceSgamut3cineEncoding "scene-linear"
|
||||
#define kOfxColourspaceLinVeniceSgamut3cineIsData false
|
||||
#define kOfxColourspaceLinVeniceSgamut3cineIsBasic false
|
||||
#define kOfxColourspaceLinVeniceSgamut3cineIsCore false
|
||||
#define kOfxColourspaceLinVeniceSgamut3cineIsDisplay false
|
||||
|
||||
// slog3_sgamut3
|
||||
// Convert Sony S-Log3 S-Gamut3 to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Sony:Input:SLog3_SGamut3_to_ACES2065-1:1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:IDT.Sony.SLog3_SGamut3.a1.v1
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_SLog3_SGamut3.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.SLog3_SGamut3_to_ACES.a1.1.0
|
||||
#define kOfxColourspaceSlog3Sgamut3 "slog3_sgamut3"
|
||||
#define kOfxColourspaceSlog3Sgamut3Label "S-Log3 S-Gamut3"
|
||||
#define kOfxColourspaceSlog3Sgamut3Encoding "log"
|
||||
#define kOfxColourspaceSlog3Sgamut3IsData false
|
||||
#define kOfxColourspaceSlog3Sgamut3IsBasic false
|
||||
#define kOfxColourspaceSlog3Sgamut3IsCore false
|
||||
#define kOfxColourspaceSlog3Sgamut3IsDisplay false
|
||||
|
||||
// slog3_sgamut3cine
|
||||
// Convert Sony S-Log3 S-Gamut3.Cine to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Sony:Input:SLog3_SGamut3Cine_to_ACES2065-1:1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:IDT.Sony.SLog3_SGamut3Cine.a1.v1
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_SLog3_SGamut3Cine.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.SLog3_SGamut3Cine_to_ACES.a1.1.0
|
||||
#define kOfxColourspaceSlog3Sgamut3cine "slog3_sgamut3cine"
|
||||
#define kOfxColourspaceSlog3Sgamut3cineLabel "S-Log3 S-Gamut3.Cine"
|
||||
#define kOfxColourspaceSlog3Sgamut3cineEncoding "log"
|
||||
#define kOfxColourspaceSlog3Sgamut3cineIsData false
|
||||
#define kOfxColourspaceSlog3Sgamut3cineIsBasic false
|
||||
#define kOfxColourspaceSlog3Sgamut3cineIsCore false
|
||||
#define kOfxColourspaceSlog3Sgamut3cineIsDisplay false
|
||||
|
||||
// slog3_venice_sgamut3
|
||||
// Convert Sony S-Log3 Venice S-Gamut3 to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Sony:Input:SLog3_Venice_SGamut3_to_ACES2065-1:1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:IDT.Sony.Venice_SLog3_SGamut3.a1.v1
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_SLog3_Venice_SGamut3.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.SLog3_Venice_SGamut3_to_ACES.a1.1.0
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3 "slog3_venice_sgamut3"
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3Label "S-Log3 Venice S-Gamut3"
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3Encoding "log"
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3IsData false
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3IsBasic false
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3IsCore false
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3IsDisplay false
|
||||
|
||||
// slog3_venice_sgamut3cine
|
||||
// Convert Sony S-Log3 Venice S-Gamut3.Cine to ACES2065-1
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:Sony:Input:SLog3_Venice_SGamut3Cine_to_ACES2065-1:1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:IDT.Sony.Venice_SLog3_SGamut3Cine.a1.v1
|
||||
// AMF Components
|
||||
// --------------
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_SLog3_Venice_SGamut3Cine.a1.1.0
|
||||
// ACEStransformID: urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.SLog3_Venice_SGamut3Cine_to_ACES.a1.1.0
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3cine "slog3_venice_sgamut3cine"
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3cineLabel "S-Log3 Venice S-Gamut3.Cine"
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3cineEncoding "log"
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3cineIsData false
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3cineIsBasic false
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3cineIsCore false
|
||||
#define kOfxColourspaceSlog3VeniceSgamut3cineIsDisplay false
|
||||
|
||||
// camera_rec709
|
||||
// Convert ACES2065-1 to Rec.709 camera OETF Rec.709 primaries, D65 white point
|
||||
// CLFtransformID: urn:aswf:ocio:transformId:1.0:ITU:Utility:AP0_to_Camera_Rec709:1.0
|
||||
#define kOfxColourspaceCameraRec709 "camera_rec709"
|
||||
#define kOfxColourspaceCameraRec709Label "Camera Rec.709"
|
||||
#define kOfxColourspaceCameraRec709Encoding "sdr-video"
|
||||
#define kOfxColourspaceCameraRec709IsData false
|
||||
#define kOfxColourspaceCameraRec709IsBasic false
|
||||
#define kOfxColourspaceCameraRec709IsCore false
|
||||
#define kOfxColourspaceCameraRec709IsDisplay false
|
||||
|
||||
/** @brief Roles - standard names used for compatibility with common OCIO configs.
|
||||
*/
|
||||
|
||||
/** @brief aces_interchange
|
||||
Guaranteed to be ACES2065-1.
|
||||
*/
|
||||
#define kOfxColourspaceRoleAcesInterchange "aces_interchange"
|
||||
#define kOfxColourspaceRoleAcesInterchangeIsBasic false
|
||||
#define kOfxColourspaceRoleAcesInterchangeIsCore true
|
||||
|
||||
/** @brief cie_xyz_d65_interchange
|
||||
CIE XYZ colorimetry with the neutral axis at D65.
|
||||
*/
|
||||
#define kOfxColourspaceRoleCieXyzD65Interchange "cie_xyz_d65_interchange"
|
||||
#define kOfxColourspaceRoleCieXyzD65InterchangeIsBasic false
|
||||
#define kOfxColourspaceRoleCieXyzD65InterchangeIsCore true
|
||||
|
||||
/** @brief color_picking
|
||||
The colourspace to use for colour pickers, typically a display colourspace.
|
||||
*/
|
||||
#define kOfxColourspaceRoleColorPicking "color_picking"
|
||||
#define kOfxColourspaceRoleColorPickingIsBasic false
|
||||
#define kOfxColourspaceRoleColorPickingIsCore true
|
||||
|
||||
/** @brief color_timing
|
||||
A colourspace suitable for colour grading, typically a log colourspace.
|
||||
*/
|
||||
#define kOfxColourspaceRoleColorTiming "color_timing"
|
||||
#define kOfxColourspaceRoleColorTimingIsBasic false
|
||||
#define kOfxColourspaceRoleColorTimingIsCore true
|
||||
|
||||
/** @brief compositing_log
|
||||
Any scene-referred colourspace with a log transfer function.
|
||||
*/
|
||||
#define kOfxColourspaceRoleCompositingLog "compositing_log"
|
||||
#define kOfxColourspaceRoleCompositingLogIsBasic false
|
||||
#define kOfxColourspaceRoleCompositingLogIsCore true
|
||||
|
||||
/** @brief data
|
||||
Image values should not be treated as colour, e.g. motion vectors or masks. Mapped to the raw colourspace.
|
||||
*/
|
||||
#define kOfxColourspaceRoleData "data"
|
||||
#define kOfxColourspaceRoleDataIsBasic false
|
||||
#define kOfxColourspaceRoleDataIsCore true
|
||||
|
||||
/** @brief matte_paint
|
||||
A colourspace suitable for matte painting.
|
||||
*/
|
||||
#define kOfxColourspaceRoleMattePaint "matte_paint"
|
||||
#define kOfxColourspaceRoleMattePaintIsBasic false
|
||||
#define kOfxColourspaceRoleMattePaintIsCore true
|
||||
|
||||
/** @brief scene_linear
|
||||
Any scene-referred linear colourspace.
|
||||
*/
|
||||
#define kOfxColourspaceRoleSceneLinear "scene_linear"
|
||||
#define kOfxColourspaceRoleSceneLinearIsBasic false
|
||||
#define kOfxColourspaceRoleSceneLinearIsCore true
|
||||
|
||||
/** @brief texture_paint
|
||||
A colourspace suitable for texture painting, typically sRGB.
|
||||
*/
|
||||
#define kOfxColourspaceRoleTexturePaint "texture_paint"
|
||||
#define kOfxColourspaceRoleTexturePaintIsBasic false
|
||||
#define kOfxColourspaceRoleTexturePaintIsCore true
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
|
||||
|
||||
#ifndef _ofxColour_h_
|
||||
#define _ofxColour_h_
|
||||
|
||||
// Copyright OpenFX and contributors to the OpenFX project.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
/** @file ofxColour.h
|
||||
Contains the API for colourspace data exchange.
|
||||
*/
|
||||
|
||||
/** Define OFX_NO_DEFAULT_COLORSPACE_HEADER if you want to manage the
|
||||
colourspace headers yourself. If this is not defined, the latest ofx-native
|
||||
config will be included.
|
||||
*/
|
||||
#ifndef OFX_NO_DEFAULT_COLORSPACE_HEADER
|
||||
#include "ofx-native-v1.5_aces-v1.3_ocio-v2.3.h"
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** @brief What style of colour management does the host or plug-in offer?
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - host descriptor (read only), plugin descriptor (read/write), image effect instance (read only)
|
||||
- Default - kOfxImageEffectColourManagementStyleNone
|
||||
- Valid Values - This must be one of
|
||||
- ::kOfxImageEffectColourManagementNone - no colour management
|
||||
- ::kOfxImageEffectColourManagementBasic - only basic colourspaces from the config header may be used
|
||||
- ::kOfxImageEffectColourManagementCore - only core colourspaces from the config header may be used
|
||||
- ::kOfxImageEffectColourManagementFull - any colourspace from the config header may be used
|
||||
- ::kOfxImageEffectColourManagementOCIO - any OCIO config may be used (implies use of the OCIO library)
|
||||
|
||||
Hosts should set this property if they will provide colourspace information
|
||||
to plug-ins.
|
||||
Plug-ins should set this property if they can use host-provided colourspace
|
||||
information.
|
||||
Collectively, the full, core and basic styles are referred to as native
|
||||
colour management. OCIO is used as the reference for the colour management
|
||||
API, but is not required to implement the native styles.
|
||||
|
||||
The colourspace strings used in the native styles are from an OFX-specific
|
||||
OCIO config which is currently based on the OCIO ACES Studio built-in config,
|
||||
studio-config-v2.1.0_aces-v1.3_ocio-v2.3, and stored for OFX purposes in
|
||||
ofx-native-v1.5_aces-v1.3_ocio-v2.3.h (referred to as the config header).
|
||||
Additionally, there is a scheme for cross-referencing between clips, and a set
|
||||
of "basic colourspaces", which are designed to be generic names for a family of
|
||||
colourspaces. When a basic colourspace is used, this means the host is free to
|
||||
choose any colourspace with the same encoding and reference space (scene vs
|
||||
display).
|
||||
|
||||
The assumption is that OCIO > Full > Core > Basic, so the highest style
|
||||
supported by both host and plug-in should usually be chosen by the host.
|
||||
The chosen style must be set by the host using this property on an image
|
||||
effect instance.
|
||||
*/
|
||||
#define kOfxImageEffectPropColourManagementStyle "OfxImageEffectPropColourManagementStyle"
|
||||
|
||||
/* String used to indicate that no colour management is available. */
|
||||
#define kOfxImageEffectColourManagementNone "OfxImageEffectColourManagementNone"
|
||||
/* String used to indicate that basic colour management is available. */
|
||||
#define kOfxImageEffectColourManagementBasic "OfxImageEffectColourManagementBasic"
|
||||
/* String used to indicate that core colour management is available. */
|
||||
#define kOfxImageEffectColourManagementCore "OfxImageEffectColourManagementCore"
|
||||
/* String used to indicate that Full colour management is available. */
|
||||
#define kOfxImageEffectColourManagementFull "OfxImageEffectColourManagementFull"
|
||||
/* String used to indicate that OCIO colour management is available. */
|
||||
#define kOfxImageEffectColourManagementOCIO "OfxImageEffectColourManagementOCIO"
|
||||
|
||||
/** @brief What native mode configs are supported?
|
||||
|
||||
- Type - string X N
|
||||
- Property Set - host descriptor (read only), plugin descriptor (read/write)
|
||||
- Valid Values - A list of config identifiers. The only currently supported
|
||||
value is ofx-native-v1.5_aces-v1.3_ocio-v2.3.
|
||||
|
||||
While the API for colour management is expected to be stable, the set of
|
||||
colourspaces will evolve over time. This property must be set by both plug-ins
|
||||
and hosts specifying the list of native mode configs that are available on each
|
||||
side.
|
||||
*/
|
||||
#define kOfxImageEffectPropColourManagementAvailableConfigs "OfxImageEffectPropColourManagementAvailableConfigs"
|
||||
|
||||
/** @brief The native colour management config to be used for this instance
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - image effect instance (read only)
|
||||
- Valid Values - any string provided by the plug-in in
|
||||
kOfxImageEffectPropColourManagementAvailableConfigs
|
||||
|
||||
The host must set this property to indicate the native colour management config
|
||||
the plug-in should be used to look up colourspace strings. It is important to
|
||||
set this even in OCIO mode, to define the basic colourspaces.
|
||||
*/
|
||||
#define kOfxImageEffectPropColourManagementConfig "OfxImageEffectPropColourManagementConfig"
|
||||
|
||||
/** @brief The path to the OCIO config used for this instance
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - image effect instance (read only)
|
||||
- Valid Values - Filesystem path to the config or URI starting ocio://
|
||||
|
||||
A host must set this property on any effect instances where it has negotiated
|
||||
OCIO colour management (kOfxImageEffectColourManagementOCIO).
|
||||
Use of URIs for built-in configs, such as ocio://default is permitted.
|
||||
*/
|
||||
#define kOfxImageEffectPropOCIOConfig "OfxImageEffectPropOCIOConfig"
|
||||
|
||||
/** @brief The colourspace used for this clip
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - clip instance (read/write)
|
||||
- Valid Values - colourspace that is permitted under the style in use.
|
||||
For OCIO, any string acceptable to Config::getColorSpace()
|
||||
|
||||
Hosts should set this property to the colourspace of the input clip. Typically
|
||||
it will be set to the working colourspace of the host but could be any valid
|
||||
colourspace.
|
||||
|
||||
Both host and plug-in should use the value of
|
||||
kOfxImageClipPropPreferredColourspace where reasonable.
|
||||
|
||||
In OCIO mode, a basic colourspace may have been requested via
|
||||
kOfxImageClipPropPreferredColourspaces, but the actual colourspace used should
|
||||
be reported in this property. If an OCIO host has added the basic colourspaces
|
||||
to its config as roles or aliases, they would be permitted here.
|
||||
|
||||
Cross-referencing between clips is possible by setting this property to
|
||||
"OfxColourspace_<clip>". For example a plug-in may set this property during
|
||||
kOfxImageEffectActionGetOutputColourspace to "OfxColourspace_Source", telling
|
||||
he host that the colourspace of the output matches the "Source" input clip.
|
||||
In the basic style, plug-ins are recommended to use cross-referencing for
|
||||
their output clip unless kOfxColourspaceRaw is required.
|
||||
|
||||
If a clip sets OfxImageClipPropIsMask or it only supports
|
||||
OfxImageComponentAlpha, colour management is disabled and this property
|
||||
must be unset.
|
||||
*/
|
||||
#define kOfxImageClipPropColourspace "OfxImageClipPropColourspace"
|
||||
|
||||
/** @brief The preferred colourspace for this clip
|
||||
|
||||
- Type - string X N
|
||||
- Property Set - clip instance (read only) and ::kOfxImageEffectActionGetClipPreferences action out args property (read/write)
|
||||
- Valid Values - colourspace that is permitted under the style in use.
|
||||
For Basic, any colourspace from the config header where IsBasic is true.
|
||||
For Core, any colourspace from the config header where IsCore is true.
|
||||
For Full, any colourspace from the config header.
|
||||
For OCIO, any string acceptable to Config::getColorSpace(), or a basic colourspace.
|
||||
|
||||
Plug-ins may set this property during kOfxImageEffectActionGetClipPreferences
|
||||
to request images in a colourspace which is convenient for them. The
|
||||
property is an ordered set of colourspace identifiers, which may be any of the
|
||||
names or aliases supported by the colour management style in use. If plug-ins
|
||||
prefer more esoteric colourspaces, they are encouraged to also include basic
|
||||
colourspacesas a fallback. For example a colour grading plug-in which supports
|
||||
a specific camera and expects a log colourspace might list:
|
||||
|
||||
"arri_logc4", "arri_logc3_ei800", "ACEScct", "ofx_scene_log"
|
||||
|
||||
The host is free to choose any colourspace from this list, but should favour
|
||||
the first mutually agreeable colourspace, and set kOfxImageClipPropColourspace
|
||||
to tell the plug-in which colourspace has been selected. A host does not need
|
||||
to convert into the first choice just because it can, as this might be
|
||||
inefficient, and should avoid scene-to-display or display-to-scene conversions
|
||||
where possible.
|
||||
|
||||
In the event that the host cannot supply images in a requested colourspace,
|
||||
it may supply images in any valid colourspace. Plug-ins must check
|
||||
kOfxImageClipPropColourspace to see if their request was satisfied.
|
||||
|
||||
In both native and OCIO modes, it is recommended to include basic colourspaces
|
||||
in this list, because this gives the host more flexibility to avoid unnecessary
|
||||
colourspace conversions.
|
||||
|
||||
Hosts can invoke kOfxImageEffectActionGetOutputColourspace to set this on an
|
||||
output clip, which could be helpful in a generator context, and plug-ins should
|
||||
follow the same logic as hosts when deciding which colourspace to use.
|
||||
|
||||
It might be much less costly for a host to perform a conversion
|
||||
than a plug-in, so in the example of a plug-in which performs all internal
|
||||
processing in scene linear, it is sensible for the plug-in to universally
|
||||
assert that preference by preferring kOfxColourspaceOfxSceneLinear, and for
|
||||
the host to honour it if possible. However, if a plug-in is capable of
|
||||
adapting to any input colourspace, it should not set this preference.
|
||||
|
||||
Cross-referencing between clips is possible by setting this property to
|
||||
"OfxColourspace_<clip>". For example, a plug-in in a transition context may set
|
||||
this property on its "SourceTo" clip to "OfxColourspace_SourceFrom", telling
|
||||
the host it would like both input clips to be in the same colourspace.
|
||||
|
||||
If a plug-in has inputs which expect motion vectors, depth values or other
|
||||
non-colour channels, it should set the preferred colourspace to
|
||||
kOfxColourspaceRaw. Similarly, if a host requests outputs in a typical scene
|
||||
colourspace, but the plug-in is producing motion vectors, it should ignore
|
||||
the request and set kOfxImageClipPropColourspace to kOfxColourspaceRaw.
|
||||
*/
|
||||
#define kOfxImageClipPropPreferredColourspaces "OfxImageClipPropPreferredColourspaces"
|
||||
|
||||
/** @brief The display colourspace used in the plug-in's viewport
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - image effect instance (read only)
|
||||
- Valid Values - any colourspace from the config header
|
||||
|
||||
Used with native colour management styles, this property is relevant
|
||||
for plug-ins which have their own viewport in a custom window. Plug-ins should
|
||||
not expect this to be available during a render event.
|
||||
Hosts should set this property to a display colourspace which matches that
|
||||
used in its own viewport. For a multi-display system, choose the colourspace
|
||||
for the display device where a native window would appear by default.
|
||||
A host which supports OCIO should use the OCIO-specific display and view
|
||||
properties instead.
|
||||
|
||||
*/
|
||||
#define kOfxImageEffectPropDisplayColourspace "OfxImageEffectPropDisplayColourspace"
|
||||
|
||||
/** @brief The OCIO display to be used in the plug-in's viewport
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - image effect instance (read only)
|
||||
- Valid Values - OCIO display that is present in the config
|
||||
|
||||
This OCIO-specific property allows the host to specify which OCIO display should be used.
|
||||
If not defined, the default rules for choosing a display will be followed.
|
||||
*/
|
||||
#define kOfxImageEffectPropOCIODisplay "OfxImageEffectPropOCIODisplay"
|
||||
|
||||
/** @brief The OCIO view to be used in the plug-in's viewport
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - image effect instance (read only)
|
||||
- Valid Values - OCIO view for the display specified by kOfxImageEffectPropOCIODisplay
|
||||
|
||||
This OCIO-specific property allows the host to specify which OCIO view should be used.
|
||||
If not defined, the default rules for choosing a view will be followed.
|
||||
*/
|
||||
#define kOfxImageEffectPropOCIOView "OfxImageEffectPropOCIOView"
|
||||
|
||||
/** @brief
|
||||
|
||||
This action allows a host to ask an effect, given a list of preferred
|
||||
colourspaces, what colourspace will be used for its output clip. This should
|
||||
be called after first gathering the plug-in's preferred input colourspaces
|
||||
via OfxImageEffectActionGetClipPreferences.
|
||||
|
||||
Cross-references to input clip colourspaces are permitted, for example in a
|
||||
filter context, the host might request "OfxColourspace_Source".
|
||||
|
||||
If a host wants to rely on the output clip colourspace, it must call this
|
||||
action whenever the effect instance is changed. Do not assume that the output
|
||||
colourspace will be the same across all instances of an effect, or even for
|
||||
the lifetime of an effect instance, because a plug-in may change its output
|
||||
colourspace based on a user changing a parameter. Assume that the plug-in will
|
||||
check kOfxImageClipPropColourspace on its input clips during this action in
|
||||
order to decide the output clip colourspace, so the action must be called
|
||||
again if the host changes any of the other colour properties.
|
||||
|
||||
On a successful return from this action, the host must set
|
||||
kOfxImageClipPropColourspace on the instance's output clip to the value
|
||||
from outArgs.
|
||||
|
||||
@param handle handle to the instance, cast to an \ref OfxImageEffectHandle
|
||||
@param inArgs a property set containing the property
|
||||
- \ref kOfxImageClipPropPreferredColourspaces the host's list of preferred colourspaces
|
||||
|
||||
@param outArgs a property set containing the property
|
||||
- \ref kOfxImageClipPropColourspace the colourspace selected by the plug-in,
|
||||
which may be a cross-reference to an input clip.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped and the colourspace was set in
|
||||
the outArgs property set
|
||||
- \ref kOfxStatReplyDefault, the action was not trapped and the host
|
||||
should use the colourspace of the first input clip
|
||||
- \ref kOfxStatErrMemory, in which case the action may be called again after a memory purge
|
||||
- \ref kOfxStatFailed, something wrong, but no error code appropriate, plugin to post message
|
||||
- \ref kOfxStatErrFatal
|
||||
*/
|
||||
#define kOfxImageEffectActionGetOutputColourspace "OfxImageEffectActionGetOutputColourspace"
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,966 @@
|
||||
|
||||
|
||||
#ifndef _ofxCore_h_
|
||||
#define _ofxCore_h_
|
||||
|
||||
// Copyright OpenFX and contributors to the OpenFX project.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
|
||||
#include "stddef.h" // for size_t
|
||||
#include <limits.h> // for INT_MIN & INT_MAX
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** @file ofxCore.h
|
||||
Contains the core OFX architectural struct and function definitions. For more details on the basic OFX architecture, see \ref Architecture.
|
||||
*/
|
||||
|
||||
|
||||
/** @brief Platform independent export macro.
|
||||
*
|
||||
* This macro is to be used before any symbol that is to be
|
||||
* exported from a plug-in. This is OS/compiler dependent.
|
||||
*/
|
||||
#if defined(WIN32) || defined(WIN64)
|
||||
#define OfxExport extern __declspec(dllexport)
|
||||
#else
|
||||
#define OfxExport extern
|
||||
#endif
|
||||
|
||||
/** @brief Blind data structure to manipulate sets of properties through */
|
||||
typedef struct OfxPropertySetStruct *OfxPropertySetHandle;
|
||||
|
||||
/** @brief OFX status return type */
|
||||
typedef int OfxStatus;
|
||||
|
||||
/** @brief Generic host structure passed to OfxPlugin::setHost function
|
||||
|
||||
This structure contains what is needed by a plug-in to bootstrap its connection
|
||||
to the host.
|
||||
*/
|
||||
typedef struct OfxHost {
|
||||
/** @brief Global handle to the host. Extract relevant host properties from this.
|
||||
This pointer will be valid while the binary containing the plug-in is loaded.
|
||||
*/
|
||||
OfxPropertySetHandle host;
|
||||
|
||||
/** @brief The function which the plug-in uses to fetch suites from the host.
|
||||
|
||||
\arg \c host the host the suite is being fetched from this \em must be the \e host member of the OfxHost struct containing fetchSuite.
|
||||
\arg \c suiteName ASCII string labelling the host supplied API
|
||||
\arg \c suiteVersion version of that suite to fetch
|
||||
|
||||
Any API fetched will be valid while the binary containing the plug-in is loaded.
|
||||
|
||||
Repeated calls to fetchSuite with the same parameters will return the same pointer.
|
||||
|
||||
It is recommended that hosts should return the same host and suite pointers to all plugins
|
||||
in the same shared lib or bundle.
|
||||
|
||||
returns
|
||||
- NULL if the API is unknown (either the api or the version requested),
|
||||
- pointer to the relevant API if it was found
|
||||
*/
|
||||
const void *(*fetchSuite)(OfxPropertySetHandle host, const char *suiteName, int suiteVersion);
|
||||
} OfxHost;
|
||||
|
||||
|
||||
/** @brief Entry point for plug-ins
|
||||
|
||||
\arg \c action ASCII c string indicating which action to take
|
||||
\arg \c instance object to which action should be applied, this will need to be cast to the appropriate blind data type depending on the \e action
|
||||
\arg \c inData handle that contains action specific properties
|
||||
\arg \c outData handle where the plug-in should set various action specific properties
|
||||
|
||||
This is how the host generally communicates with a plug-in. Entry points are used to pass messages
|
||||
to various objects used within OFX. The main use is within the OfxPlugin struct.
|
||||
|
||||
The exact set of actions is determined by the plug-in API that is being implemented, however all plug-ins
|
||||
can perform several actions. For the list of actions consult \ref ActionsAll.
|
||||
*/
|
||||
typedef OfxStatus (OfxPluginEntryPoint)(const char *action, const void *handle, OfxPropertySetHandle inArgs, OfxPropertySetHandle outArgs);
|
||||
|
||||
/** @brief The structure that defines a plug-in to a host.
|
||||
*
|
||||
* This structure is the first element in any plug-in structure
|
||||
* using the OFX plug-in architecture. By examining its members
|
||||
* a host can determine the API that the plug-in implements,
|
||||
* the version of that API, its name and version.
|
||||
*
|
||||
* For details see \ref Architecture.
|
||||
*
|
||||
*/
|
||||
typedef struct OfxPlugin {
|
||||
/** Defines the type of the plug-in, this will tell the host what the plug-in does. e.g.: an image
|
||||
effects plug-in would be a "OfxImageEffectPlugin"
|
||||
*/
|
||||
const char *pluginApi;
|
||||
|
||||
/** Defines the version of the pluginApi that this plug-in implements */
|
||||
int apiVersion;
|
||||
|
||||
/** String that uniquely labels the plug-in among all plug-ins that implement an API.
|
||||
It need not necessarily be human sensible, however the preference is to use reverse
|
||||
internet domain name of the developer, followed by a '.' then by a name that represents
|
||||
the plug-in.. It must be a legal ASCII string and have no whitespace in the
|
||||
name and no non printing chars.
|
||||
For example "uk.co.somesoftwarehouse.myPlugin"
|
||||
*/
|
||||
const char *pluginIdentifier;
|
||||
|
||||
/** Major version of this plug-in, this gets incremented when backwards compatibility is broken. */
|
||||
unsigned int pluginVersionMajor;
|
||||
|
||||
/** Major version of this plug-in, this gets incremented when software is changed,
|
||||
but does not break backwards compatibility. */
|
||||
unsigned int pluginVersionMinor;
|
||||
|
||||
/** @brief Function the host uses to connect the plug-in to the host's api fetcher
|
||||
|
||||
\arg \c fetchApi pointer to host's API fetcher
|
||||
|
||||
Mandatory function.
|
||||
|
||||
The very first function called in a plug-in. The plug-in \em must \em not call any OFX functions within this, it must only set its local copy of the host pointer.
|
||||
|
||||
\pre
|
||||
- nothing else has been called
|
||||
|
||||
\post
|
||||
- the pointer suite is valid until the plug-in is unloaded
|
||||
|
||||
It is recommended that hosts should return the same host and suite pointers to all plugins
|
||||
in the same shared lib or bundle.
|
||||
*/
|
||||
void (*setHost)(OfxHost *host);
|
||||
|
||||
/** @brief Main entry point for plug-ins
|
||||
|
||||
Mandatory function.
|
||||
|
||||
The exact set of actions is determined by the plug-in API that is being implemented, however all plug-ins
|
||||
can perform several actions. For the list of actions consult \ref ActionsAll.
|
||||
|
||||
Preconditions
|
||||
- setHost has been called
|
||||
*/
|
||||
OfxPluginEntryPoint *mainEntry;
|
||||
} OfxPlugin;
|
||||
|
||||
/**
|
||||
\defgroup ActionsAll OFX Actions
|
||||
|
||||
These are the actions passed to a plug-in's 'main' function
|
||||
*/
|
||||
/*@{*/
|
||||
|
||||
/** @brief
|
||||
|
||||
This action is the first action passed to a plug-in after the
|
||||
binary containing the plug-in has been loaded. It is there to allow a
|
||||
plug-in to create any global data structures it may need and is also
|
||||
when the plug-in should fetch suites from the host.
|
||||
|
||||
The \ref handle, \ref inArgs and \ref outArgs arguments to the \ref mainEntry
|
||||
are redundant and should be set to NULL.
|
||||
|
||||
|
||||
|
||||
\pre
|
||||
- The plugin's \ref OfxPlugin::setHost function has been called
|
||||
|
||||
\post
|
||||
This action will not be called again while the binary containing the plug-in remains loaded.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped and all was well,
|
||||
- \ref kOfxStatReplyDefault, the action was ignored,
|
||||
- \ref kOfxStatFailed, the load action failed, no further actions will be passed to the plug-in.
|
||||
Interpret if possible kOfxStatFailed as plug-in indicating it does not want to load
|
||||
Do not create an entry in the host's UI for plug-in then.
|
||||
Plug-in also has the option to return 0 for OfxGetNumberOfPlugins or kOfxStatFailed if host supports OfxSetHost in which case kOfxActionLoad will never be called.
|
||||
- \ref kOfxStatErrFatal, fatal error in the plug-in.
|
||||
*/
|
||||
#define kOfxActionLoad "OfxActionLoad"
|
||||
|
||||
/** @brief
|
||||
|
||||
The kOfxActionDescribe is the second action passed to a plug-in. It is
|
||||
where a plugin defines how it behaves and the resources it needs to
|
||||
function.
|
||||
|
||||
Note that the handle passed in acts as a descriptor for, rather than an
|
||||
instance of the plugin. The handle is global and unique. The plug-in is
|
||||
at liberty to cache the handle away for future reference until the
|
||||
plug-in is unloaded.
|
||||
|
||||
Most importantly, the effect must set what image effect contexts it is
|
||||
capable of working in.
|
||||
|
||||
This action *must* be trapped, it is not optional.
|
||||
|
||||
|
||||
@param handle handle to the plug-in descriptor, cast to an \ref OfxImageEffectHandle
|
||||
@param inArgs is redundant and is set to NULL
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionLoad has been called
|
||||
|
||||
\post
|
||||
- \ref kOfxActionDescribe will not be called again, unless it fails and
|
||||
returns one of the error codes where the host is allowed to attempt
|
||||
the action again
|
||||
- the handle argument, being the global plug-in description handle, is
|
||||
a valid handle from the end of a sucessful describe action until the
|
||||
end of the \ref kOfxActionUnload action (ie: the plug-in can cache it away
|
||||
without worrying about it changing between actions).
|
||||
- \ref kOfxImageEffectActionDescribeInContext
|
||||
will be called once for each context that the host and plug-in
|
||||
mutually support. If a plug-in does not report to support any context supported by host,
|
||||
host should not enable the plug-in.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped and all was well
|
||||
- \ref kOfxStatErrMissingHostFeature, in which the plugin will be unloaded
|
||||
and ignored, plugin may post message
|
||||
- \ref kOfxStatErrMemory, in which case describe may be called again after a
|
||||
memory purge
|
||||
- \ref kOfxStatFailed, something wrong, but no error code appropriate,
|
||||
plugin to post message
|
||||
- \ref kOfxStatErrFatal
|
||||
|
||||
*/
|
||||
#define kOfxActionDescribe "OfxActionDescribe"
|
||||
|
||||
/** @brief
|
||||
|
||||
This action is the last action passed to the plug-in before the
|
||||
binary containing the plug-in is unloaded. It is there to allow a
|
||||
plug-in to destroy any global data structures it may have created.
|
||||
|
||||
The handle, inArgs and outArgs arguments to the main entry
|
||||
are redundant and should be set to NULL.
|
||||
|
||||
\pre
|
||||
- the \ref kOfxActionLoad action has been called
|
||||
- all instances of a plugin have been destroyed
|
||||
|
||||
\post
|
||||
- No other actions will be called.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped all was well
|
||||
- \ref kOfxStatReplyDefault, the action was ignored
|
||||
- \ref kOfxStatErrFatal, in which case we the program will be forced to quit
|
||||
|
||||
*/
|
||||
#define kOfxActionUnload "OfxActionUnload"
|
||||
|
||||
/** @brief
|
||||
|
||||
This action is an action that may be passed to a plug-in
|
||||
instance from time to time in low memory situations. Instances recieving
|
||||
this action should destroy any data structures they may have and release
|
||||
the associated memory, they can later reconstruct this from the effect's
|
||||
parameter set and associated information.
|
||||
|
||||
For Image Effects, it is generally a bad idea to call this after each
|
||||
render, but rather it should be called after
|
||||
\ref kOfxImageEffectActionEndSequenceRender
|
||||
Some effects, typically those flagged with the
|
||||
\ref kOfxImageEffectInstancePropSequentialRender
|
||||
property, may need to cache information from previously rendered frames
|
||||
to function correctly, or have data structures that are expensive to
|
||||
reconstruct at each frame (eg: a particle system). Ideally, such effect
|
||||
should free such structures during the
|
||||
\ref kOfxImageEffectActionEndSequenceRender action.
|
||||
|
||||
@param handle handle to the plug-in instance, cast to an \ref OfxImageEffectHandle
|
||||
@param inArgs is redundant and is set to NULL
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance has been called on the instance handle,
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped and all was well
|
||||
- \ref kOfxStatReplyDefault, the action was ignored
|
||||
- \ref kOfxStatErrFatal,
|
||||
- \ref kOfxStatFailed, something went wrong, but no error code appropriate,
|
||||
the plugin should to post a message
|
||||
*/
|
||||
#define kOfxActionPurgeCaches "OfxActionPurgeCaches"
|
||||
|
||||
/** @brief
|
||||
|
||||
This action is called when a plugin should synchronise any private data
|
||||
structures to its parameter set. This generally occurs when an effect is
|
||||
about to be saved or copied, but it could occur in other situations as
|
||||
well.
|
||||
|
||||
@param handle handle to the plug-in instance, cast to an \ref OfxImageEffectHandle
|
||||
@param inArgs is redundant and is set to NULL
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance has been called on the instance handle,
|
||||
|
||||
\post
|
||||
- Any private state data can be reconstructed from the parameter set,
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped and all was well
|
||||
- \ref kOfxStatReplyDefault, the action was ignored
|
||||
- \ref kOfxStatErrFatal,
|
||||
- \ref kOfxStatFailed, something went wrong, but no error code appropriate,
|
||||
the plugin should to post a message
|
||||
*/
|
||||
#define kOfxActionSyncPrivateData "OfxActionSyncPrivateData"
|
||||
|
||||
/** @brief
|
||||
|
||||
This action is the first action passed to a plug-in's
|
||||
instance after its creation. It is there to allow a plugin to create any
|
||||
per-instance data structures it may need.
|
||||
|
||||
@param handle handle to the plug-in instance, cast to an \ref OfxImageEffectHandle
|
||||
@param inArgs is redundant and is set to NULL
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionDescribe has been called
|
||||
- the instance is fully constructed, with all objects requested in the
|
||||
describe actions (eg, parameters and clips) have been constructed and
|
||||
have had their initial values set. This means that if the values are
|
||||
being loaded from an old setup, that load should have taken place
|
||||
before the create instance action is called.
|
||||
|
||||
\post
|
||||
- the instance pointer will be valid until the
|
||||
\ref kOfxActionDestroyInstance
|
||||
action is passed to the plug-in with the same instance handle
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped and all was well
|
||||
- \ref kOfxStatReplyDefault, the action was ignored, but all was well anyway
|
||||
- \ref kOfxStatErrFatal
|
||||
- \ref kOfxStatErrMemory, in which case this may be called again after a
|
||||
memory purge
|
||||
- \ref kOfxStatFailed, something went wrong, but no error code appropriate,
|
||||
the plugin should to post a message if possible and the host should
|
||||
destroy the instanace handle and not attempt to proceed further
|
||||
*/
|
||||
#define kOfxActionCreateInstance "OfxActionCreateInstance"
|
||||
|
||||
/** @brief
|
||||
|
||||
|
||||
This action is the last passed to a plug-in's instance before its
|
||||
destruction. It is there to allow a plugin to destroy any per-instance
|
||||
data structures it may have created.
|
||||
|
||||
@param handle
|
||||
handle to the plug-in instance, cast to an \ref OfxImageEffectHandle
|
||||
@param inArgs is redundant and is set to NULL
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance
|
||||
has been called on the handle,
|
||||
- the instance has not had any of its members destroyed yet,
|
||||
|
||||
\post
|
||||
- the instance pointer is no longer valid and any operation on it will
|
||||
be undefined
|
||||
|
||||
@returns
|
||||
To some extent, what is returned is moot, a bit like throwing an
|
||||
exception in a C++ destructor, so the host should continue destruction
|
||||
of the instance regardless.
|
||||
|
||||
- \ref kOfxStatOK, the action was trapped and all was well,
|
||||
- \ref kOfxStatReplyDefault, the action was ignored as the effect had nothing
|
||||
to do,
|
||||
- \ref kOfxStatErrFatal,
|
||||
- \ref kOfxStatFailed, something went wrong, but no error code appropriate,
|
||||
the plugin should to post a message.
|
||||
|
||||
*/
|
||||
#define kOfxActionDestroyInstance "OfxActionDestroyInstance"
|
||||
|
||||
/** @brief
|
||||
|
||||
This action signals that something has changed in a plugin's instance,
|
||||
either by user action, the host or the plugin itself. All change actions
|
||||
are bracketed by a pair of \ref kOfxActionBeginInstanceChanged and
|
||||
\ref kOfxActionEndInstanceChanged actions. The ``inArgs`` property set is
|
||||
used to determine what was the thing inside the instance that was
|
||||
changed.
|
||||
|
||||
@param handle handle to the plug-in instance, cast to an \ref OfxImageEffectHandle
|
||||
@param inArgs has the following properties
|
||||
- \ref kOfxPropType The type of the thing that changed which will be one of..
|
||||
|
||||
- \ref kOfxTypeParameter Indicating a parameter's value has changed
|
||||
in some way
|
||||
- \ref kOfxTypeClip A clip to an image effect has changed in some
|
||||
way (for Image Effect Plugins only)
|
||||
|
||||
- \ref kOfxPropName the name of the thing that was changed in the instance
|
||||
- \ref kOfxPropChangeReason what triggered the change, which will be one of...
|
||||
|
||||
- \ref kOfxChangeUserEdited - the user or host changed the instance
|
||||
somehow and caused a change to something, this includes
|
||||
undo/redos, resets and loading values from files or presets,
|
||||
|
||||
- \ref kOfxChangePluginEdited - the plugin itself has changed the
|
||||
value of the instance in some action
|
||||
- \ref kOfxChangeTime - the time has changed and this has affected the
|
||||
value of the object because it varies over time
|
||||
|
||||
- \ref kOfxPropTime
|
||||
- the effect time at which the chang occured (for Image Effect Plugins only)
|
||||
- \ref kOfxImageEffectPropRenderScale
|
||||
- the render scale currently being applied to any image fetched
|
||||
from a clip (for Image Effect Plugins only)
|
||||
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance has been called on the instance handle,
|
||||
- \ref kOfxActionBeginInstanceChanged has been called on the instance
|
||||
handle.
|
||||
|
||||
\post
|
||||
- \ref kOfxActionEndInstanceChanged will be called on the instance handle.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped and all was well
|
||||
- \ref kOfxStatReplyDefault, the action was ignored
|
||||
- \ref kOfxStatErrFatal,
|
||||
- \ref kOfxStatFailed, something went wrong, but no error code appropriate,
|
||||
the plugin should to post a message
|
||||
|
||||
*/
|
||||
#define kOfxActionInstanceChanged "OfxActionInstanceChanged"
|
||||
|
||||
/** @brief
|
||||
|
||||
The \ref kOfxActionBeginInstanceChanged and \ref kOfxActionEndInstanceChanged actions
|
||||
are used to bracket all \ref kOfxActionInstanceChanged actions, whether a
|
||||
single change or multiple changes. Some changes to a plugin instance can
|
||||
be grouped logically (eg: a 'reset all' button resetting all the
|
||||
instance's parameters), the begin/end instance changed actions allow a
|
||||
plugin to respond appropriately to a large set of changes. For example,
|
||||
a plugin that maintains a complex internal state can delay any changes
|
||||
to that state until all parameter changes have completed.
|
||||
|
||||
@param handle
|
||||
handle to the plug-in instance, cast to an \ref OfxImageEffectHandle
|
||||
@param inArgs has the following properties
|
||||
- \ref kOfxPropChangeReason what triggered the change, which will be one of...
|
||||
- \ref kOfxChangeUserEdited - the user or host changed the instance
|
||||
somehow and caused a change to something, this includes
|
||||
undo/redos, resets and loading values from files or presets,
|
||||
- \ref kOfxChangePluginEdited - the plugin itself has changed the
|
||||
value of the instance in some action
|
||||
- \ref kOfxChangeTime - the time has changed and this has affected the
|
||||
value of the object because it varies over time
|
||||
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\post
|
||||
- For \ref kOfxActionBeginInstanceChanged , \ref kOfxActionCreateInstance has been called on the instance handle.
|
||||
- For \ref kOfxActionEndInstanceChanged , \ref kOfxActionBeginInstanceChanged has been called on the instance handle.
|
||||
- \ref kOfxActionCreateInstance has been called on the instance handle.
|
||||
|
||||
\post
|
||||
- For \ref kOfxActionBeginInstanceChanged, \ref kOfxActionInstanceChanged will be called at least once on the instance handle.
|
||||
- \ref kOfxActionEndInstanceChanged will be called on the instance handle.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped and all was well
|
||||
- \ref kOfxStatReplyDefault, the action was ignored
|
||||
- \ref kOfxStatErrFatal,
|
||||
- \ref kOfxStatFailed, something went wrong, but no error code appropriate,
|
||||
the plugin should to post a message
|
||||
*/
|
||||
#define kOfxActionBeginInstanceChanged "OfxActionBeginInstanceChanged"
|
||||
|
||||
/** @brief Action called after the end of a set of \ref kOfxActionEndInstanceChanged actions, used with ::kOfxActionBeginInstanceChanged to bracket a grouped set of changes, see \ref kOfxActionBeginInstanceChanged*/
|
||||
#define kOfxActionEndInstanceChanged "OfxActionEndInstanceChanged"
|
||||
|
||||
/** @brief
|
||||
|
||||
This is called when an instance is *first* actively edited by a user,
|
||||
ie: and interface is open and parameter values and input clips can be
|
||||
modified. It is there so that effects can create private user interface
|
||||
structures when necassary. Note that some hosts can have multiple
|
||||
editors open on the same effect instance simulateously.
|
||||
|
||||
|
||||
@param handle handle to the plug-in instance, cast to an \ref OfxImageEffectHandle
|
||||
@param inArgs is redundant and is set to NULL
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance has been called on the instance handle,
|
||||
|
||||
\post
|
||||
- \ref kOfxActionEndInstanceEdit will be called when the last editor is
|
||||
closed on the instance
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped and all was well
|
||||
- \ref kOfxStatReplyDefault, the action was ignored
|
||||
- \ref kOfxStatErrFatal,
|
||||
- \ref kOfxStatFailed, something went wrong, but no error code appropriate,
|
||||
the plugin should to post a message
|
||||
*/
|
||||
#define kOfxActionBeginInstanceEdit "OfxActionBeginInstanceEdit"
|
||||
|
||||
/** @brief
|
||||
|
||||
This is called when the *last* user interface on an instance closed. It
|
||||
is there so that effects can destroy private user interface structures
|
||||
when necassary. Note that some hosts can have multiple editors open on
|
||||
the same effect instance simulateously, this will only be called when
|
||||
the last of those editors are closed.
|
||||
|
||||
@param handle handle to the plug-in instance, cast to an \ref OfxImageEffectHandle
|
||||
@param inArgs is redundant and is set to NULL
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionBeginInstanceEdit has been called on the instance handle,
|
||||
|
||||
\post
|
||||
- no user interface is open on the instance
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped and all was well
|
||||
- \ref kOfxStatReplyDefault, the action was ignored
|
||||
- \ref kOfxStatErrFatal,
|
||||
- \ref kOfxStatFailed, something went wrong, but no error code appropriate,
|
||||
the plugin should to post a message
|
||||
*/
|
||||
#define kOfxActionEndInstanceEdit "OfxActionEndInstanceEdit"
|
||||
|
||||
/*@}*/
|
||||
|
||||
/** @brief Returns the 'nth' plug-in implemented inside a binary
|
||||
*
|
||||
* Returns a pointer to the 'nth' plug-in implemented in the binary. A function of this type
|
||||
* must be implemented in and exported from each plug-in binary.
|
||||
*/
|
||||
OfxExport OfxPlugin *OfxGetPlugin(int nth);
|
||||
|
||||
/** @brief Defines the number of plug-ins implemented inside a binary
|
||||
*
|
||||
* A host calls this to determine how many plug-ins there are inside
|
||||
* a binary it has loaded. A function of this type
|
||||
* must be implemented in and exported from each plug-in binary.
|
||||
*/
|
||||
OfxExport int OfxGetNumberOfPlugins(void);
|
||||
|
||||
/** @brief First thing host should call
|
||||
*
|
||||
* This host call, added in 2020, is not specified in earlier implementation of the API.
|
||||
* Therefore host must check if the plugin implemented it and not assume symbol exists.
|
||||
* The order of calls is then: 1) OfxSetHost, 2) OfxGetNumberOfPlugins, 3) OfxGetPlugin
|
||||
* The host pointer is only assumed valid until OfxGetPlugin where it might get reset.
|
||||
* Plug-in can return kOfxStatFailed to indicate it has nothing to do here, it's not for this Host and it should be skipped silently.
|
||||
*/
|
||||
|
||||
OfxExport OfxStatus OfxSetHost(const OfxHost *host);
|
||||
|
||||
/**
|
||||
\defgroup PropertiesAll Ofx Properties
|
||||
|
||||
These strings are used to identify properties within OFX, they are broken up by the host suite or API they relate to.
|
||||
*/
|
||||
/*@{*/
|
||||
|
||||
/**
|
||||
\defgroup PropertiesGeneral General Properties
|
||||
|
||||
These properties are general properties and apply to may objects across OFX
|
||||
*/
|
||||
/*@{*/
|
||||
|
||||
/** @brief Property on the host descriptor, saying what API version of the API is being implemented
|
||||
|
||||
- Type - int X N
|
||||
- Property Set - host descriptor.
|
||||
|
||||
This is a version string that will specify which version of the API is being implemented by a host. It
|
||||
can have multiple values. For example "1.0", "1.2.4" etc.....
|
||||
|
||||
If this is not present, it is safe to assume that the version of the API is "1.0".
|
||||
*/
|
||||
#define kOfxPropAPIVersion "OfxPropAPIVersion"
|
||||
|
||||
/** @brief General property used to get/set the time of something.
|
||||
|
||||
- Type - double X 1
|
||||
- Default - 0, if a setable property
|
||||
- Property Set - commonly used as an argument to actions, input and output.
|
||||
*/
|
||||
#define kOfxPropTime "OfxPropTime"
|
||||
|
||||
/** @brief Indicates if a host is actively editing the effect with some GUI.
|
||||
|
||||
- Type - int X 1
|
||||
- Property Set - effect instance (read only)
|
||||
- Valid Values - 0 or 1
|
||||
|
||||
If false the effect currently has no interface, however this may be because the effect is loaded in a background render host, or it may be loaded on an interactive host that has not yet opened an editor for the effect.
|
||||
|
||||
The output of an effect should only ever depend on the state of its parameters, not on the interactive flag. The interactive flag is more a courtesy flag to let a plugin know that it has an interace. If a plugin want's to have its behaviour dependant on the interactive flag, it can always make a secret parameter which shadows the state if the flag.
|
||||
*/
|
||||
#define kOfxPropIsInteractive "OfxPropIsInteractive"
|
||||
|
||||
/** @brief The file path to the plugin.
|
||||
|
||||
- Type - C string X 1
|
||||
- Property Set - effect descriptor (read only)
|
||||
|
||||
This is a string that indicates the file path where the plug-in was found by the host. The path is in the native
|
||||
path format for the host OS (eg: UNIX directory separators are forward slashes, Windows ones are backslashes).
|
||||
|
||||
The path is to the bundle location, see \ref InstallationLocation.
|
||||
eg: '/usr/OFX/Plugins/AcmePlugins/AcmeFantasticPlugin.ofx.bundle'
|
||||
*/
|
||||
#define kOfxPluginPropFilePath "OfxPluginPropFilePath"
|
||||
|
||||
/** @brief A private data pointer that the plug-in can store its own data behind.
|
||||
|
||||
- Type - pointer X 1
|
||||
- Property Set - plugin instance (read/write),
|
||||
- Default - NULL
|
||||
|
||||
This data pointer is unique to each plug-in instance, so two instances of the same plug-in do not share the same data pointer. Use it to hang any needed private data structures.
|
||||
*/
|
||||
#define kOfxPropInstanceData "OfxPropInstanceData"
|
||||
|
||||
/** @brief General property, used to identify the kind of an object behind a handle
|
||||
|
||||
- Type - ASCII C string X 1
|
||||
- Property Set - any object handle (read only)
|
||||
- Valid Values - currently this can be...
|
||||
- ::kOfxTypeImageEffectHost
|
||||
- ::kOfxTypeImageEffect
|
||||
- ::kOfxTypeImageEffectInstance
|
||||
- ::kOfxTypeParameter
|
||||
- ::kOfxTypeParameterInstance
|
||||
- ::kOfxTypeClip
|
||||
- ::kOfxTypeImage
|
||||
*/
|
||||
#define kOfxPropType "OfxPropType"
|
||||
|
||||
/** @brief Unique name of an object.
|
||||
|
||||
- Type - ASCII C string X 1
|
||||
- Property Set - on many objects (descriptors and instances), see \ref PropertiesByObject (read only)
|
||||
|
||||
This property is used to label objects uniquely amoung objects of that type. It is typically set when a plugin creates a new object with a function that takes a name.
|
||||
*/
|
||||
#define kOfxPropName "OfxPropName"
|
||||
|
||||
/** @brief Identifies a specific version of a host or plugin.
|
||||
|
||||
- Type - int X N
|
||||
- Property Set - host descriptor (read only), plugin descriptor (read/write)
|
||||
- Default - "0"
|
||||
- Valid Values - positive integers
|
||||
|
||||
This is a multi dimensional integer property that represents the version of a host (host descriptor), or plugin (plugin descriptor). These represent a version number of the form '1.2.3.4', with each dimension adding another 'dot' on the right.
|
||||
|
||||
A version is considered to be more recent than another if its ordered set of values is lexicographically greater than another, reading left to right. (ie: 1.2.4 is smaller than 1.2.6). Also, if the number of dimensions is different, then the values of the missing dimensions are considered to be zero (so 1.2.4 is greater than 1.2).
|
||||
*/
|
||||
#define kOfxPropVersion "OfxPropVersion"
|
||||
|
||||
/** @brief Unique user readable version string of a plugin or host.
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - host descriptor (read only), plugin descriptor (read/write)
|
||||
- Default - none, the host needs to set this
|
||||
- Valid Values - ASCII string
|
||||
|
||||
This is purely for user feedback, a plugin or host should use ::kOfxPropVersion if they need
|
||||
to check for specific versions.
|
||||
*/
|
||||
#define kOfxPropVersionLabel "OfxPropVersionLabel"
|
||||
|
||||
/** @brief Description of the plug-in to a user.
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - plugin descriptor (read/write) and instance (read only)
|
||||
- Default - ""
|
||||
- Valid Values - UTF8 string
|
||||
|
||||
This is a string giving a potentially verbose description of the effect.
|
||||
*/
|
||||
#define kOfxPropPluginDescription "OfxPropPluginDescription"
|
||||
|
||||
/** @brief User visible name of an object.
|
||||
|
||||
- Type - UTF8 C string X 1
|
||||
- Property Set - on many objects (descriptors and instances), see \ref PropertiesByObject. Typically readable and writable in most cases.
|
||||
- Default - the ::kOfxPropName the object was created with.
|
||||
|
||||
The label is what a user sees on any interface in place of the object's name.
|
||||
|
||||
Note that resetting this will also reset ::kOfxPropShortLabel and ::kOfxPropLongLabel.
|
||||
*/
|
||||
#define kOfxPropLabel "OfxPropLabel"
|
||||
|
||||
/** @brief If set this tells the host to use an icon instead of a label for some object in the interface.
|
||||
|
||||
- Type - string X 2
|
||||
- Property Set - various descriptors in the API
|
||||
- Default - ""
|
||||
- Valid Values - ASCII string
|
||||
|
||||
The value is a path is defined relative to the Resource folder that points to an SVG or PNG file containing the icon.
|
||||
|
||||
The first dimension, if set, will the name of and SVG file, the second a PNG file.
|
||||
*/
|
||||
#define kOfxPropIcon "OfxPropIcon"
|
||||
|
||||
/** @brief Short user visible name of an object.
|
||||
|
||||
- Type - UTF8 C string X 1
|
||||
- Property Set - on many objects (descriptors and instances), see \ref PropertiesByObject. Typically readable and writable in most cases.
|
||||
- Default - initially ::kOfxPropName, but will be reset if ::kOfxPropLabel is changed.
|
||||
|
||||
This is a shorter version of the label, typically 13 character glyphs or less. Hosts should use this if they have limitted display space for their object labels.
|
||||
*/
|
||||
#define kOfxPropShortLabel "OfxPropShortLabel"
|
||||
|
||||
/** @brief Long user visible name of an object.
|
||||
|
||||
- Type - UTF8 C string X 1
|
||||
- Property Set - on many objects (descriptors and instances), see \ref PropertiesByObject. Typically readable and writable in most cases.
|
||||
- Default - initially ::kOfxPropName, but will be reset if ::kOfxPropLabel is changed.
|
||||
|
||||
This is a longer version of the label, typically 32 character glyphs or so. Hosts should use this if they have mucg display space for their object labels.
|
||||
*/
|
||||
#define kOfxPropLongLabel "OfxPropLongLabel"
|
||||
|
||||
/** @brief Indicates why a plug-in changed.
|
||||
|
||||
- Type - ASCII C string X 1
|
||||
- Property Set - inArgs parameter on the ::kOfxActionInstanceChanged action.
|
||||
- Valid Values - this can be...
|
||||
- ::kOfxChangeUserEdited - the user directly edited the instance somehow and caused a change to something, this includes undo/redos and resets
|
||||
- ::kOfxChangePluginEdited - the plug-in itself has changed the value of the object in some action
|
||||
- ::kOfxChangeTime - the time has changed and this has affected the value of the object because it varies over time
|
||||
|
||||
Argument property for the ::kOfxActionInstanceChanged action.
|
||||
*/
|
||||
#define kOfxPropChangeReason "OfxPropChangeReason"
|
||||
|
||||
/** @brief A pointer to an effect instance.
|
||||
|
||||
- Type - pointer X 1
|
||||
- Property Set - on an interact instance (read only)
|
||||
|
||||
This property is used to link an object to the effect. For example if the plug-in supplies an openGL overlay for an image effect,
|
||||
the interact instance will have one of these so that the plug-in can connect back to the effect the GUI links to.
|
||||
*/
|
||||
#define kOfxPropEffectInstance "OfxPropEffectInstance"
|
||||
|
||||
/** @brief A pointer to an operating system specific application handle.
|
||||
|
||||
- Type - pointer X 1
|
||||
- Property Set - host descriptor.
|
||||
|
||||
Some plug-in vendor want raw OS specific handles back from the host so they can do interesting things with host OS APIs. Typically this is to control windowing properly on Microsoft Windows. This property returns the appropriate 'root' window handle on the current operating system. So on Windows this would be the hWnd of the application main window.
|
||||
*/
|
||||
#define kOfxPropHostOSHandle "OfxPropHostOSHandle"
|
||||
|
||||
/*@}*/
|
||||
|
||||
/*@}*/
|
||||
|
||||
/** @brief String used as a value to ::kOfxPropChangeReason to indicate a user has changed something */
|
||||
#define kOfxChangeUserEdited "OfxChangeUserEdited"
|
||||
|
||||
/** @brief String used as a value to ::kOfxPropChangeReason to indicate the plug-in itself has changed something */
|
||||
#define kOfxChangePluginEdited "OfxChangePluginEdited"
|
||||
|
||||
/** @brief String used as a value to ::kOfxPropChangeReason to a time varying object has changed due to a time change */
|
||||
#define kOfxChangeTime "OfxChangeTime"
|
||||
|
||||
/** @brief How time is specified within the OFX API */
|
||||
typedef double OfxTime;
|
||||
|
||||
/** @brief Defines one dimensional integer bounds */
|
||||
typedef struct OfxRangeI {
|
||||
int min, max;
|
||||
} OfxRangeI;
|
||||
|
||||
/** @brief Defines one dimensional double bounds */
|
||||
typedef struct OfxRangeD {
|
||||
double min, max;
|
||||
} OfxRangeD;
|
||||
|
||||
/** @brief Defines two dimensional integer point */
|
||||
typedef struct OfxPointI {
|
||||
int x, y;
|
||||
} OfxPointI;
|
||||
|
||||
/** @brief Defines two dimensional double point */
|
||||
typedef struct OfxPointD {
|
||||
double x, y;
|
||||
} OfxPointD;
|
||||
|
||||
/** @brief Used to flag infinite rects. Set minimums to this to indicate infinite
|
||||
|
||||
This is effectively INT_MAX.
|
||||
*/
|
||||
#define kOfxFlagInfiniteMax INT_MAX
|
||||
|
||||
/** @brief Used to flag infinite rects. Set minimums to this to indicate infinite.
|
||||
|
||||
This is effectively INT_MIN
|
||||
*/
|
||||
#define kOfxFlagInfiniteMin INT_MIN
|
||||
|
||||
/** @brief Defines two dimensional integer region
|
||||
|
||||
Regions are x1 <= x < x2
|
||||
|
||||
Infinite regions are flagged by setting
|
||||
- x1 = \ref kOfxFlagInfiniteMin
|
||||
- y1 = \ref kOfxFlagInfiniteMin
|
||||
- x2 = \ref kOfxFlagInfiniteMax
|
||||
- y2 = \ref kOfxFlagInfiniteMax
|
||||
|
||||
*/
|
||||
typedef struct OfxRectI {
|
||||
int x1, y1, x2, y2;
|
||||
} OfxRectI;
|
||||
|
||||
/** @brief Defines two dimensional double region
|
||||
|
||||
Regions are x1 <= x < x2
|
||||
|
||||
Infinite regions are flagged by setting
|
||||
- x1 = \ref kOfxFlagInfiniteMin
|
||||
- y1 = \ref kOfxFlagInfiniteMin
|
||||
- x2 = \ref kOfxFlagInfiniteMax
|
||||
- y2 = \ref kOfxFlagInfiniteMax
|
||||
|
||||
*/
|
||||
typedef struct OfxRectD {
|
||||
double x1, y1, x2, y2;
|
||||
} OfxRectD;
|
||||
|
||||
/** @brief String used to label unset bitdepths */
|
||||
#define kOfxBitDepthNone "OfxBitDepthNone"
|
||||
|
||||
/** @brief String used to label unsigned 8 bit integer samples */
|
||||
#define kOfxBitDepthByte "OfxBitDepthByte"
|
||||
|
||||
/** @brief String used to label unsigned 16 bit integer samples */
|
||||
#define kOfxBitDepthShort "OfxBitDepthShort"
|
||||
|
||||
/** @brief String used to label half-float (16 bit floating point) samples
|
||||
* \version Added in Version 1.4. Was in ofxOpenGLRender.h before.
|
||||
*/
|
||||
#define kOfxBitDepthHalf "OfxBitDepthHalf"
|
||||
|
||||
/** @brief String used to label signed 32 bit floating point samples */
|
||||
#define kOfxBitDepthFloat "OfxBitDepthFloat"
|
||||
|
||||
/**
|
||||
\defgroup StatusCodes Status Codes
|
||||
|
||||
These strings are used to identify error states within ofx, they are returned
|
||||
by various host suite functions, as well as plug-in functions. The valid return codes
|
||||
for each function are documented with that function.
|
||||
*/
|
||||
/*@{*/
|
||||
|
||||
/**
|
||||
\defgroup StatusCodesGeneral General Status Codes
|
||||
|
||||
General status codes start at 1 and continue until 999
|
||||
|
||||
*/
|
||||
/*@{*/
|
||||
|
||||
/** @brief Status code indicating all was fine */
|
||||
#define kOfxStatOK 0
|
||||
|
||||
/** @brief Status error code for a failed operation. */
|
||||
#define kOfxStatFailed ((int)1)
|
||||
|
||||
/** @brief Status error code for a fatal error
|
||||
|
||||
Only returned in the case where the plug-in or host cannot continue to function and needs to be restarted.
|
||||
*/
|
||||
#define kOfxStatErrFatal ((int)2)
|
||||
|
||||
/** @brief Status error code for an operation on or request for an unknown object */
|
||||
#define kOfxStatErrUnknown ((int)3)
|
||||
|
||||
/** @brief Status error code returned by plug-ins when they are missing host functionality, either an API or some optional functionality (eg: custom params).
|
||||
|
||||
Plug-Ins returning this should post an appropriate error message stating what they are missing.
|
||||
*/
|
||||
#define kOfxStatErrMissingHostFeature ((int) 4)
|
||||
|
||||
/** @brief Status error code for an unsupported feature/operation */
|
||||
#define kOfxStatErrUnsupported ((int) 5)
|
||||
|
||||
/** @brief Status error code for an operation attempting to create something that exists */
|
||||
#define kOfxStatErrExists ((int) 6)
|
||||
|
||||
/** @brief Status error code for an incorrect format */
|
||||
#define kOfxStatErrFormat ((int) 7)
|
||||
|
||||
/** @brief Status error code indicating that something failed due to memory shortage */
|
||||
#define kOfxStatErrMemory ((int) 8)
|
||||
|
||||
/** @brief Status error code for an operation on a bad handle */
|
||||
#define kOfxStatErrBadHandle ((int) 9)
|
||||
|
||||
/** @brief Status error code indicating that a given index was invalid or unavailable */
|
||||
#define kOfxStatErrBadIndex ((int)10)
|
||||
|
||||
/** @brief Status error code indicating that something failed due an illegal value */
|
||||
#define kOfxStatErrValue ((int) 11)
|
||||
|
||||
/** @brief OfxStatus returned indicating a 'yes' */
|
||||
#define kOfxStatReplyYes ((int) 12)
|
||||
|
||||
/** @brief OfxStatus returned indicating a 'no' */
|
||||
#define kOfxStatReplyNo ((int) 13)
|
||||
|
||||
/** @brief OfxStatus returned indicating that a default action should be performed */
|
||||
#define kOfxStatReplyDefault ((int) 14)
|
||||
|
||||
/*@}*/
|
||||
|
||||
/*@}*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @mainpage OFX : Open Plug-Ins For Special Effects
|
||||
|
||||
This page represents the automatically extracted HTML documentation of the source headers for the OFX Image Effect API.
|
||||
The documentation was extracted by doxygen (http://www.doxygen.org).
|
||||
A more complete reference manual is https://openfx.readthedocs.io .
|
||||
|
||||
*/
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,842 @@
|
||||
|
||||
|
||||
#pragma once
|
||||
#ifndef __OFXGPURENDER_H__
|
||||
#define __OFXGPURENDER_H__
|
||||
|
||||
// Copyright OpenFX and contributors to the OpenFX project.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
/** @file ofxGPURender.h
|
||||
|
||||
This file contains an optional suite for performing GPU-accelerated
|
||||
rendering of OpenFX Image Effect Plug-ins. For details see
|
||||
\ref ofxGPURender.
|
||||
|
||||
It allows hosts and plug-ins to support OpenGL, OpenCL, CUDA, and Metal.
|
||||
Additional GPU APIs, such a Vulkan, could use similar techniques.
|
||||
*/
|
||||
|
||||
#include "ofxImageEffect.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** @defgroup OpenGLRenderSuite OpenGL Render Suite
|
||||
* @{
|
||||
*/
|
||||
|
||||
/** @brief The name of the OpenGL render suite, used to fetch from a host
|
||||
via OfxHost::fetchSuite
|
||||
*/
|
||||
#define kOfxOpenGLRenderSuite "OfxImageEffectOpenGLRenderSuite"
|
||||
//#define kOfxOpenGLRenderSuite_ext "OfxImageEffectOpenGLRenderSuite_ext"
|
||||
|
||||
|
||||
#ifndef kOfxBitDepthHalf
|
||||
/** @brief String used to label the OpenGL half float (16 bit floating
|
||||
point) sample format */
|
||||
#define kOfxBitDepthHalf "OfxBitDepthHalf"
|
||||
#endif
|
||||
|
||||
/** @brief Indicates whether a host or plug-in can support OpenGL accelerated
|
||||
rendering
|
||||
|
||||
- Type - C string X 1
|
||||
- Property Set - plug-in descriptor (read/write), host descriptor (read
|
||||
only) - plug-in instance change (read/write)
|
||||
- Default - "false" for a plug-in
|
||||
- Valid Values - This must be one of
|
||||
- "false" - in which case the host or plug-in does not support OpenGL
|
||||
accelerated rendering
|
||||
- "true" - which means a host or plug-in can support OpenGL accelerated
|
||||
rendering, in the case of plug-ins this also means that it
|
||||
is capable of CPU based rendering in the absence of a GPU
|
||||
- "needed" - only for plug-ins, this means that an plug-in has to have
|
||||
OpenGL support, without which it cannot work.
|
||||
|
||||
V1.4: It is now expected from host reporting v1.4 that the plug-in can during instance change switch from true to false and false to true.
|
||||
|
||||
*/
|
||||
#define kOfxImageEffectPropOpenGLRenderSupported "OfxImageEffectPropOpenGLRenderSupported"
|
||||
|
||||
|
||||
/** @brief Indicates the bit depths supported by a plug-in during OpenGL renders.
|
||||
|
||||
This is analogous to ::kOfxImageEffectPropSupportedPixelDepths. When a
|
||||
plug-in sets this property, the host will try to provide buffers/textures
|
||||
in one of the supported formats. Additionally, the target buffers where
|
||||
the plug-in renders to will be set to one of the supported formats.
|
||||
|
||||
Unlike ::kOfxImageEffectPropSupportedPixelDepths, this property is
|
||||
optional. Shader-based effects might not really care about any
|
||||
format specifics when using OpenGL textures, so they can leave this unset
|
||||
and allow the host the decide the format.
|
||||
|
||||
|
||||
- Type - string X N
|
||||
- Property Set - plug-in descriptor (read only)
|
||||
- Default - none set
|
||||
- Valid Values - This must be one of
|
||||
- ::kOfxBitDepthNone (implying a clip is unconnected, not valid for an
|
||||
image)
|
||||
- ::kOfxBitDepthByte
|
||||
- ::kOfxBitDepthShort
|
||||
- ::kOfxBitDepthHalf
|
||||
- ::kOfxBitDepthFloat
|
||||
*/
|
||||
#define kOfxOpenGLPropPixelDepth "OfxOpenGLPropPixelDepth"
|
||||
|
||||
|
||||
/** @brief Indicates that a plug-in SHOULD use OpenGL acceleration in
|
||||
the current action
|
||||
|
||||
When a plug-in and host have established they can both use OpenGL renders
|
||||
then when this property has been set the host expects the plug-in to render
|
||||
its result into the buffer it has setup before calling the render. The
|
||||
plug-in can then also safely use the 'OfxImageEffectOpenGLRenderSuite'
|
||||
|
||||
- Type - int X 1
|
||||
- Property Set - inArgs property set of the following actions...
|
||||
- ::kOfxImageEffectActionRender
|
||||
- ::kOfxImageEffectActionBeginSequenceRender
|
||||
- ::kOfxImageEffectActionEndSequenceRender
|
||||
- Valid Values
|
||||
- 0 indicates that the plug-in cannot use the OpenGL suite
|
||||
- 1 indicates that the plug-in should render into the texture,
|
||||
and may use the OpenGL suite functions.
|
||||
|
||||
\note Once this property is set, the host and plug-in have agreed to
|
||||
use OpenGL, so the effect SHOULD access all its images through the
|
||||
OpenGL suite.
|
||||
|
||||
v1.4: kOfxImageEffectPropOpenGLEnabled should probably be checked in Instance Changed prior to try to read image via clipLoadTexture
|
||||
|
||||
*/
|
||||
#define kOfxImageEffectPropOpenGLEnabled "OfxImageEffectPropOpenGLEnabled"
|
||||
|
||||
|
||||
/** @brief Indicates the texture index of an image turned into an OpenGL
|
||||
texture by the host
|
||||
|
||||
- Type - int X 1
|
||||
- Property Set - texture handle returned by
|
||||
` OfxImageEffectOpenGLRenderSuiteV1::clipLoadTexture (read only)
|
||||
|
||||
This value should be cast to a GLuint and used as the texture index when
|
||||
performing OpenGL texture operations.
|
||||
|
||||
The property set of the following actions should contain this property:
|
||||
- ::kOfxImageEffectActionRender
|
||||
- ::kOfxImageEffectActionBeginSequenceRender
|
||||
- ::kOfxImageEffectActionEndSequenceRender
|
||||
*/
|
||||
#define kOfxImageEffectPropOpenGLTextureIndex "OfxImageEffectPropOpenGLTextureIndex"
|
||||
|
||||
|
||||
/** @brief Indicates the texture target enumerator of an image turned into
|
||||
an OpenGL texture by the host
|
||||
|
||||
- Type - int X 1
|
||||
- Property Set - texture handle returned by
|
||||
OfxImageEffectOpenGLRenderSuiteV1::clipLoadTexture (read only)
|
||||
This value should be cast to a GLenum and used as the texture target
|
||||
when performing OpenGL texture operations.
|
||||
|
||||
The property set of the following actions should contain this property:
|
||||
- ::kOfxImageEffectActionRender
|
||||
- ::kOfxImageEffectActionBeginSequenceRender
|
||||
- ::kOfxImageEffectActionEndSequenceRender
|
||||
*/
|
||||
#define kOfxImageEffectPropOpenGLTextureTarget "OfxImageEffectPropOpenGLTextureTarget"
|
||||
|
||||
|
||||
/** @name StatusReturnValues
|
||||
OfxStatus returns indicating that a OpenGL render error has occurred:
|
||||
|
||||
- If a plug-in returns ::kOfxStatGLRenderFailed, the host should retry the
|
||||
render with OpenGL rendering disabled.
|
||||
|
||||
- If a plug-in returns ::kOfxStatGLOutOfMemory, the host may choose to free
|
||||
resources on the GPU and retry the OpenGL render, rather than immediately
|
||||
falling back to CPU rendering.
|
||||
*/
|
||||
/**
|
||||
* @{
|
||||
*/
|
||||
/** @brief GPU render ran out of memory */
|
||||
#define kOfxStatGPUOutOfMemory ((int) 1001)
|
||||
/** @brief OpenGL render ran out of memory (same as ``kOfxStatGPUOutOfMemory``) */
|
||||
#define kOfxStatGLOutOfMemory ((int) 1001)
|
||||
/** @brief GPU render failed in a non-memory-related way */
|
||||
#define kOfxStatGPURenderFailed ((int) 1002)
|
||||
/** @brief OpenGL render failed in a non-memory-related way (same as ``kOfxStatGPURenderFailed``) */
|
||||
#define kOfxStatGLRenderFailed ((int) 1002) /* for backward compatibility */
|
||||
/** @} */
|
||||
|
||||
/** @brief OFX suite that provides image to texture conversion for OpenGL
|
||||
processing
|
||||
*/
|
||||
typedef struct OfxImageEffectOpenGLRenderSuiteV1
|
||||
{
|
||||
/** @brief loads an image from an OFX clip as a texture into OpenGL
|
||||
|
||||
\arg \c clip clip to load the image from
|
||||
\arg \c time effect time to load the image from
|
||||
\arg \c format requested texture format (As in
|
||||
none,byte,word,half,float, etc..)
|
||||
When set to NULL, the host decides the format based on the
|
||||
plug-in's ::kOfxOpenGLPropPixelDepth setting.
|
||||
\arg \c region region of the image to load (optional, set to NULL to
|
||||
get a 'default' region)
|
||||
this is in the \ref CanonicalCoordinates.
|
||||
\arg \c textureHandle property set containing information about the
|
||||
texture
|
||||
|
||||
An image is fetched from a clip at the indicated time for the given region
|
||||
and loaded into an OpenGL texture. When a specific format is requested, the
|
||||
host ensures it gives the requested format.
|
||||
When the clip specified is the "Output" clip, the format is ignored and
|
||||
the host must bind the resulting texture as the current color buffer
|
||||
(render target). This may also be done prior to calling the
|
||||
::kOfxImageEffectActionRender action.
|
||||
If the \em region parameter is set to non-NULL, then it will be clipped to
|
||||
the clip's Region of Definition for the given time.
|
||||
The returned image will be \em at \em least as big as this region.
|
||||
If the region parameter is not set or is NULL, then the region fetched will be at
|
||||
least the Region of Interest the effect has previously specified, clipped to
|
||||
the clip's Region of Definition.
|
||||
Information about the texture, including the texture index, is returned in
|
||||
the \em textureHandle argument.
|
||||
The properties on this handle will be...
|
||||
- ::kOfxImageEffectPropOpenGLTextureIndex
|
||||
- ::kOfxImageEffectPropOpenGLTextureTarget
|
||||
- ::kOfxImageEffectPropPixelDepth
|
||||
- ::kOfxImageEffectPropComponents
|
||||
- ::kOfxImageEffectPropPreMultiplication
|
||||
- ::kOfxImageEffectPropRenderScale
|
||||
- ::kOfxImagePropPixelAspectRatio
|
||||
- ::kOfxImagePropBounds
|
||||
- ::kOfxImagePropRegionOfDefinition
|
||||
- ::kOfxImagePropRowBytes
|
||||
- ::kOfxImagePropField
|
||||
- ::kOfxImagePropUniqueIdentifier
|
||||
|
||||
With the exception of the OpenGL specifics, these properties are the same
|
||||
as the properties in an image handle returned by clipGetImage in the image
|
||||
effect suite.
|
||||
\pre
|
||||
- clip was returned by clipGetHandle
|
||||
- Format property in the texture handle
|
||||
|
||||
\post
|
||||
- texture handle to be disposed of by clipFreeTexture before the action
|
||||
returns
|
||||
- when the clip specified is the "Output" clip, the format is ignored and
|
||||
the host must bind the resulting texture as the current color buffer
|
||||
(render target).
|
||||
This may also be done prior to calling the render action.
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK - the image was successfully fetched and returned
|
||||
in the handle,
|
||||
- ::kOfxStatFailed - the image could not be fetched because it does
|
||||
not exist in the clip at the indicated
|
||||
time and/or region, the plug-in should continue
|
||||
operation, but assume the image was black and
|
||||
transparent.
|
||||
- ::kOfxStatErrBadHandle - the clip handle was invalid,
|
||||
- ::kOfxStatErrMemory - not enough OpenGL memory was available for the
|
||||
effect to load the texture.
|
||||
The plug-in should abort the GL render and
|
||||
return ::kOfxStatErrMemory, after which the host can
|
||||
decide to retry the operation with CPU based processing.
|
||||
|
||||
\note
|
||||
- this is the OpenGL equivalent of clipGetImage from OfxImageEffectSuiteV1
|
||||
|
||||
|
||||
*/
|
||||
|
||||
OfxStatus (*clipLoadTexture)(OfxImageClipHandle clip,
|
||||
OfxTime time,
|
||||
const char *format,
|
||||
const OfxRectD *region,
|
||||
OfxPropertySetHandle *textureHandle);
|
||||
|
||||
/** @brief Releases the texture handle previously returned by
|
||||
clipLoadTexture
|
||||
|
||||
For input clips, this also deletes the texture from OpenGL.
|
||||
This should also be called on the output clip; for the Output
|
||||
clip, it just releases the handle but does not delete the
|
||||
texture (since the host will need to read it).
|
||||
|
||||
\pre
|
||||
- textureHandle was returned by clipGetImage
|
||||
|
||||
\post
|
||||
- all operations on textureHandle will be invalid, and the OpenGL texture
|
||||
it referred to has been deleted (for source clips)
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK - the image was successfully fetched and returned in the
|
||||
handle,
|
||||
- ::kOfxStatFailed - general failure for some reason,
|
||||
- ::kOfxStatErrBadHandle - the image handle was invalid,
|
||||
*/
|
||||
OfxStatus (*clipFreeTexture)(OfxPropertySetHandle textureHandle);
|
||||
|
||||
|
||||
/** @brief Request the host to minimize its GPU resource load
|
||||
|
||||
When a plug-in fails to allocate GPU resources, it can call this function to
|
||||
request the host to flush its GPU resources if it holds any.
|
||||
After the function the plug-in can try again to allocate resources which then
|
||||
might succeed if the host actually has released anything.
|
||||
|
||||
\pre
|
||||
\post
|
||||
- No changes to the plug-in GL state should have been made.
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK - the host has actually released some
|
||||
resources,
|
||||
- ::kOfxStatReplyDefault - nothing the host could do..
|
||||
*/
|
||||
OfxStatus (*flushResources)( );
|
||||
|
||||
} OfxImageEffectOpenGLRenderSuiteV1;
|
||||
|
||||
|
||||
/** @brief Action called when an effect has just been attached to an OpenGL
|
||||
context.
|
||||
|
||||
The purpose of this action is to allow a plug-in to set up any data it may need
|
||||
to do OpenGL rendering in an instance. For example...
|
||||
- allocate a lookup table on a GPU,
|
||||
- create an OpenCL or CUDA context that is bound to the host's OpenGL
|
||||
context so it can share buffers.
|
||||
|
||||
The plug-in will be responsible for deallocating any such shared resource in the
|
||||
\ref ::kOfxActionOpenGLContextDetached action.
|
||||
|
||||
A host cannot call ::kOfxActionOpenGLContextAttached on the same instance
|
||||
without an intervening ::kOfxActionOpenGLContextDetached. A host can have a
|
||||
plug-in swap OpenGL contexts by issuing a attach/detach for the first context
|
||||
then another attach for the next context.
|
||||
|
||||
The arguments to the action are...
|
||||
\arg \c handle handle to the plug-in instance, cast to an
|
||||
\ref OfxImageEffectHandle
|
||||
\arg \c inArgs is redundant and set to NULL
|
||||
\arg \c outArgs is redundant and set to NULL
|
||||
|
||||
A plug-in can return...
|
||||
- ::kOfxStatOK, the action was trapped and all was well
|
||||
- ::kOfxStatReplyDefault, the action was ignored, but all was well anyway
|
||||
- ::kOfxStatErrMemory, in which case this may be called again after a memory
|
||||
purge
|
||||
- ::kOfxStatFailed, something went wrong, but no error code appropriate,
|
||||
the plug-in should to post a message if possible and the host should not
|
||||
attempt to run the plug-in in OpenGL render mode.
|
||||
*/
|
||||
#define kOfxActionOpenGLContextAttached "OfxActionOpenGLContextAttached"
|
||||
|
||||
/** @brief Action called when an effect is about to be detached from an
|
||||
OpenGL context
|
||||
|
||||
The purpose of this action is to allow a plug-in to deallocate any resource
|
||||
allocated in \ref ::kOfxActionOpenGLContextAttached just before the host
|
||||
decouples a plug-in from an OpenGL context.
|
||||
The host must call this with the same OpenGL context active as it
|
||||
called with the corresponding ::kOfxActionOpenGLContextAttached.
|
||||
|
||||
The arguments to the action are...
|
||||
\arg \c handle handle to the plug-in instance, cast to an
|
||||
\ref OfxImageEffectHandle
|
||||
\arg \c inArgs is redundant and set to NULL
|
||||
\arg \c outArgs is redundant and set to NULL
|
||||
|
||||
A plug-in can return...
|
||||
- ::kOfxStatOK, the action was trapped and all was well
|
||||
- ::kOfxStatReplyDefault, the action was ignored, but all was well anyway
|
||||
- ::kOfxStatErrMemory, in which case this may be called again after a memory
|
||||
purge
|
||||
- ::kOfxStatFailed, something went wrong, but no error code appropriate,
|
||||
the plug-in should to post a message if possible and the host should not
|
||||
attempt to run the plug-in in OpenGL render mode.
|
||||
*/
|
||||
#define kOfxActionOpenGLContextDetached "kOfxActionOpenGLContextDetached"
|
||||
|
||||
|
||||
/** @page ofxOpenGLRender OpenGL Acceleration of Rendering
|
||||
|
||||
@section ofxOpenGLRenderIntro Introduction
|
||||
|
||||
The OfxOpenGLRenderSuite allows image effects to use OpenGL commands
|
||||
(hopefully backed by a GPU) to accelerate rendering
|
||||
of their outputs. The basic scheme is simple....
|
||||
- An effect indicates it wants to use OpenGL acceleration by setting the
|
||||
::kOfxImageEffectPropOpenGLRenderSupported flag on its descriptor
|
||||
- A host indicates it supports OpenGL acceleration by setting
|
||||
::kOfxImageEffectPropOpenGLRenderSupported on its descriptor
|
||||
- In an effect's ::kOfxImageEffectActionGetClipPreferences action, an
|
||||
effect indicates what clips it will be loading images from onto the GPU's
|
||||
memory during an effect's ::kOfxImageEffectActionRender action.
|
||||
|
||||
@section ofxOpenGLRenderHouseKeeping OpenGL House Keeping
|
||||
|
||||
If a host supports OpenGL rendering then it flags this with the string
|
||||
property ::kOfxImageEffectPropOpenGLRenderSupported on its descriptor property
|
||||
set. Effects that cannot run without OpenGL support should examine this in
|
||||
::kOfxActionDescribe action and return a ::kOfxStatErrMissingHostFeature
|
||||
status flag if it is not set to "true".
|
||||
|
||||
Effects flag to a host that they support OpenGL rendering by setting the
|
||||
string property ::kOfxImageEffectPropOpenGLRenderSupported on their effect
|
||||
descriptor during the ::kOfxActionDescribe action. Effects can work in three
|
||||
ways....
|
||||
- purely on CPUs without any OpenGL support at all, in which case they
|
||||
should set ::kOfxImageEffectPropOpenGLRenderSupported to be "false" (the
|
||||
default),
|
||||
- on CPUs but with optional OpenGL support, in which case they should set
|
||||
::kOfxImageEffectPropOpenGLRenderSupported to be "true",
|
||||
- only with OpenGL support, in which case they should set
|
||||
::kOfxImageEffectPropOpenGLRenderSupported to be "needed".
|
||||
|
||||
Hosts can examine this flag and respond to it appropriately.
|
||||
|
||||
Effects can use OpenGL accelerated rendering during the following
|
||||
action...
|
||||
- ::kOfxImageEffectActionRender
|
||||
|
||||
If an effect has indicated that it optionally supports OpenGL acceleration,
|
||||
it should check the property ::kOfxImageEffectPropOpenGLEnabled
|
||||
passed as an in argument to the following actions,
|
||||
- ::kOfxImageEffectActionRender
|
||||
- ::kOfxImageEffectActionBeginSequenceRender
|
||||
- ::kOfxImageEffectActionEndSequenceRender
|
||||
|
||||
If this property is set to 0, then it should not attempt to use any calls to
|
||||
the OpenGL suite or OpenGL calls whilst rendering.
|
||||
|
||||
|
||||
@section ofxOpenGLRenderGettingTextures Getting Images as Textures
|
||||
|
||||
An effect could fetch an image into memory from a host via the standard
|
||||
Image Effect suite "clipGetImage" call, then create an OpenGL
|
||||
texture from that. However as several buffer copies and various other bits
|
||||
of house keeping may need to happen to do this, it is more
|
||||
efficient for a host to create the texture directly.
|
||||
|
||||
The OfxOpenGLRenderSuiteV1::clipLoadTexture function does this. The
|
||||
arguments and semantics are similar to the
|
||||
OfxImageEffectSuiteV2::clipGetImage function, with a few minor changes.
|
||||
|
||||
The effect is passed back a property handle describing the texture. Once the
|
||||
texture is finished with, this should be disposed
|
||||
of via the OfxOpenGLRenderSuiteV1::clipFreeTexture function, which will also
|
||||
delete the associated OpenGL texture (for source clips).
|
||||
|
||||
The returned handle has a set of properties on it, analogous to the
|
||||
properties returned on the image handle by
|
||||
OfxImageEffectSuiteV2::clipGetImage. These are:
|
||||
- ::kOfxImageEffectPropOpenGLTextureIndex
|
||||
- ::kOfxImageEffectPropOpenGLTextureTarget
|
||||
- ::kOfxImageEffectPropPixelDepth
|
||||
- ::kOfxImageEffectPropComponents
|
||||
- ::kOfxImageEffectPropPreMultiplication
|
||||
- ::kOfxImageEffectPropRenderScale
|
||||
- ::kOfxImagePropPixelAspectRatio
|
||||
- ::kOfxImagePropBounds
|
||||
- ::kOfxImagePropRegionOfDefinition
|
||||
- ::kOfxImagePropRowBytes
|
||||
- ::kOfxImagePropField
|
||||
- ::kOfxImagePropUniqueIdentifier
|
||||
|
||||
The main difference between this and an image handle is that the
|
||||
::kOfxImagePropData property is replaced by the
|
||||
kOfxImageEffectPropOpenGLTextureIndex property.
|
||||
This integer property should be cast to a GLuint and is the index to use for
|
||||
the OpenGL texture.
|
||||
Next to texture handle the texture target enumerator is given in
|
||||
kOfxImageEffectPropOpenGLTextureTarget
|
||||
|
||||
Note, because the image is being directly loaded into a texture by the host
|
||||
it need not obey the Clip Preferences action to remap the image to the pixel
|
||||
depth the effect requested.
|
||||
|
||||
@section ofxOpenGLRenderOutput Render Output Directly with OpenGL
|
||||
|
||||
Effects can use the graphics context as they see fit. They may be doing
|
||||
several render passes with fetch back from the card to main memory
|
||||
via 'render to texture' mechanisms interleaved with passes performed on the
|
||||
CPU. The effect must leave output on the graphics card in the provided output
|
||||
image texture buffer.
|
||||
|
||||
The host will create a default OpenGL viewport that is the size of the
|
||||
render window passed to the render action. The following
|
||||
code snippet shows how the viewport should be rooted at the bottom left of
|
||||
the output texture.
|
||||
|
||||
\verbatim
|
||||
// set up the OpenGL context for the render to texture
|
||||
...
|
||||
|
||||
// figure the size of the render window
|
||||
int dx = renderWindow.x2 - renderWindow.x1;
|
||||
int dy = renderWindow.y2 - renderWindow.y2;
|
||||
|
||||
// setup the output viewport
|
||||
glViewport(0, 0, dx, dy);
|
||||
|
||||
\endverbatim
|
||||
|
||||
Prior to calling the render action the host may also choose to
|
||||
bind the output texture as the current color buffer (render target), or they
|
||||
may defer doing this until clipLoadTexture is called for the output clip.
|
||||
|
||||
After this, it is completely up to the effect to choose what OpenGL
|
||||
operations to render with, including projections and so on.
|
||||
|
||||
@section ofxOpenGLRenderContext OpenGL Current Context
|
||||
|
||||
The host is only required to make the OpenGL context current (e.g.,
|
||||
using wglMakeCurrent, for Windows) during the following actions:
|
||||
|
||||
- ::kOfxImageEffectActionRender
|
||||
- ::kOfxImageEffectActionBeginSequenceRender
|
||||
- ::kOfxImageEffectActionEndSequenceRender
|
||||
- ::kOfxActionOpenGLContextAttached
|
||||
- ::kOfxActionOpenGLContextDetached
|
||||
|
||||
For the first 3 actions, Render through EndSequenceRender, the host is only
|
||||
required to set the OpenGL context if ::kOfxImageEffectPropOpenGLEnabled is
|
||||
set. In other words, a plug-in should not expect the OpenGL context to be
|
||||
current for other OFX calls, such as ::kOfxImageEffectActionDescribeInContext.
|
||||
|
||||
*/
|
||||
|
||||
/** @}*/ // end of OpenGLRender doc group
|
||||
|
||||
/**
|
||||
* @defgroup CudaRender CUDA Rendering
|
||||
* @version CUDA rendering was added in version 1.5.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
/** @brief Indicates whether a host or plug-in can support CUDA render
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - plug-in descriptor (read/write), host descriptor (read only)
|
||||
- Default - "false" for a plug-in
|
||||
- Valid Values - This must be one of
|
||||
- "false" - the host or plug-in does not support CUDA render
|
||||
- "true" - the host or plug-in can support CUDA render
|
||||
*/
|
||||
#define kOfxImageEffectPropCudaRenderSupported "OfxImageEffectPropCudaRenderSupported"
|
||||
|
||||
/** @brief Indicates that a plug-in SHOULD use CUDA render in
|
||||
the current action
|
||||
|
||||
If a plug-in and host have both set
|
||||
kOfxImageEffectPropCudaRenderSupported="true" then the host MAY set
|
||||
this property to indicate that it is passing images as CUDA memory
|
||||
pointers.
|
||||
|
||||
- Type - int X 1
|
||||
- Property Set - inArgs property set of the following actions...
|
||||
- ::kOfxImageEffectActionRender
|
||||
- ::kOfxImageEffectActionBeginSequenceRender
|
||||
- ::kOfxImageEffectActionEndSequenceRender
|
||||
- Valid Values
|
||||
- 0 indicates that the kOfxImagePropData of each image of each clip
|
||||
is a CPU memory pointer.
|
||||
- 1 indicates that the kOfxImagePropData of each image of each clip
|
||||
is a CUDA memory pointer.
|
||||
*/
|
||||
#define kOfxImageEffectPropCudaEnabled "OfxImageEffectPropCudaEnabled"
|
||||
|
||||
/** @brief Indicates whether a host or plug-in can support CUDA streams
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - plug-in descriptor (read/write), host descriptor (read only)
|
||||
- Default - "false" for a plug-in
|
||||
- Valid Values - This must be one of
|
||||
- "false" - in which case the host or plug-in does not support CUDA streams
|
||||
- "true" - which means a host or plug-in can support CUDA streams
|
||||
|
||||
*/
|
||||
#define kOfxImageEffectPropCudaStreamSupported "OfxImageEffectPropCudaStreamSupported"
|
||||
|
||||
/** @brief The CUDA stream to be used for rendering
|
||||
|
||||
- Type - pointer X 1
|
||||
- Property Set - inArgs property set of the following actions...
|
||||
- ::kOfxImageEffectActionRender
|
||||
- ::kOfxImageEffectActionBeginSequenceRender
|
||||
- ::kOfxImageEffectActionEndSequenceRender
|
||||
|
||||
This property will only be set if the host and plug-in both support CUDA streams.
|
||||
|
||||
If set:
|
||||
|
||||
- this property contains a pointer to the stream of CUDA render (cudaStream_t).
|
||||
In order to use it, reinterpret_cast<cudaStream_t>(pointer) is needed.
|
||||
|
||||
- the plug-in SHOULD ensure that its render action enqueues any
|
||||
asynchronous CUDA operations onto the supplied queue.
|
||||
|
||||
- the plug-in SHOULD NOT wait for final asynchronous operations to
|
||||
complete before returning from the render action, and SHOULD NOT
|
||||
call cudaDeviceSynchronize() at any time.
|
||||
|
||||
If not set:
|
||||
|
||||
- the plug-in SHOULD ensure that any asynchronous operations it
|
||||
enqueues have completed before returning from the render action.
|
||||
*/
|
||||
#define kOfxImageEffectPropCudaStream "OfxImageEffectPropCudaStream"
|
||||
|
||||
/** @}*/ // end CudaRender doc group
|
||||
|
||||
/**
|
||||
* @defgroup MetalRender Apple Metal Rendering
|
||||
* @version Metal rendering was added in version 1.5.
|
||||
* @{
|
||||
*/
|
||||
/** @brief Indicates whether a host or plug-in can support Metal render
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - plug-in descriptor (read/write), host descriptor (read only)
|
||||
- Default - "false" for a plug-in
|
||||
- Valid Values - This must be one of
|
||||
- "false" - the host or plug-in does not support Metal render
|
||||
- "true" - the host or plug-in can support Metal render
|
||||
*/
|
||||
#define kOfxImageEffectPropMetalRenderSupported "OfxImageEffectPropMetalRenderSupported"
|
||||
|
||||
/** @brief Indicates that a plug-in SHOULD use Metal render in
|
||||
the current action
|
||||
|
||||
If a plug-in and host have both set
|
||||
kOfxImageEffectPropMetalRenderSupported="true" then the host MAY
|
||||
set this property to indicate that it is passing images as Metal
|
||||
buffers.
|
||||
|
||||
- Type - int X 1
|
||||
- Property Set - inArgs property set of the following actions...
|
||||
- ::kOfxImageEffectActionRender
|
||||
- ::kOfxImageEffectActionBeginSequenceRender
|
||||
- ::kOfxImageEffectActionEndSequenceRender
|
||||
- Valid Values
|
||||
- 0 indicates that the kOfxImagePropData of each image of each clip
|
||||
is a CPU memory pointer.
|
||||
- 1 indicates that the kOfxImagePropData of each image of each clip
|
||||
is a Metal id<MTLBuffer>.
|
||||
*/
|
||||
#define kOfxImageEffectPropMetalEnabled "OfxImageEffectPropMetalEnabled"
|
||||
|
||||
/** @brief The command queue of Metal render
|
||||
|
||||
- Type - pointer X 1
|
||||
- Property Set - inArgs property set of the following actions...
|
||||
- ::kOfxImageEffectActionRender
|
||||
- ::kOfxImageEffectActionBeginSequenceRender
|
||||
- ::kOfxImageEffectActionEndSequenceRender
|
||||
|
||||
This property contains a pointer to the command queue to be used for
|
||||
Metal rendering (id<MTLCommandQueue>). In order to use it,
|
||||
reinterpret_cast<id<MTLCommandQueue>>(pointer) is needed.
|
||||
|
||||
The plug-in SHOULD ensure that its render action enqueues any
|
||||
asynchronous Metal operations onto the supplied queue.
|
||||
|
||||
The plug-in SHOULD NOT wait for final asynchronous operations to
|
||||
complete before returning from the render action.
|
||||
*/
|
||||
#define kOfxImageEffectPropMetalCommandQueue "OfxImageEffectPropMetalCommandQueue"
|
||||
/** @}*/ // end MetalRender doc group
|
||||
|
||||
/**
|
||||
* @defgroup OpenClRender OpenCL Rendering
|
||||
* @version OpenCL rendering was added in version 1.5.
|
||||
* @{
|
||||
*/
|
||||
/** @brief Indicates whether a host or plug-in can support OpenCL Buffers render
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - plug-in descriptor (read/write), host descriptor (read only)
|
||||
- Default - "false" for a plug-in
|
||||
- Valid Values - This must be one of
|
||||
- "false" - the host or plug-in does not support OpenCL Buffers render
|
||||
- "true" - the host or plug-in can support OpenCL Buffers render
|
||||
*/
|
||||
#define kOfxImageEffectPropOpenCLRenderSupported "OfxImageEffectPropOpenCLRenderSupported"
|
||||
|
||||
/** @brief Indicates whether a host or plug-in can support OpenCL Images render
|
||||
|
||||
- Type - string X 1
|
||||
- Property Set - plug-in descriptor (read/write), host descriptor (read only)
|
||||
- Default - "false" for a plug-in
|
||||
- Valid Values - This must be one of
|
||||
- "false" - in which case the host or plug-in does not support OpenCL Images render
|
||||
- "true" - which means a host or plug-in can support OpenCL Images render
|
||||
*/
|
||||
#define kOfxImageEffectPropOpenCLSupported "OfxImageEffectPropOpenCLSupported"
|
||||
|
||||
/** @brief Indicates that a plug-in SHOULD use OpenCL render in
|
||||
the current action
|
||||
|
||||
If a plug-in and host have both set
|
||||
kOfxImageEffectPropOpenCLRenderSupported="true" or have both
|
||||
set kOfxImageEffectPropOpenCLSupported="true" then the host MAY
|
||||
set this property to indicate that it is passing images as OpenCL
|
||||
Buffers or Images.
|
||||
|
||||
When rendering using OpenCL Buffers, the cl_mem of the buffers are retrieved using ::kOfxImagePropData.
|
||||
When rendering using OpenCL Images, the cl_mem of the images are retrieved using ::kOfxImageEffectPropOpenCLImage.
|
||||
If both ::kOfxImageEffectPropOpenCLSupported (Buffers) and ::kOfxImageEffectPropOpenCLRenderSupported (Images) are
|
||||
enabled by the plug-in, it should use ::kOfxImageEffectPropOpenCLImage to determine which is being used by the host.
|
||||
|
||||
- Type - int X 1
|
||||
- Property Set - inArgs property set of the following actions...
|
||||
- ::kOfxImageEffectActionRender
|
||||
- ::kOfxImageEffectActionBeginSequenceRender
|
||||
- ::kOfxImageEffectActionEndSequenceRender
|
||||
- Valid Values
|
||||
- 0 indicates that a plug-in SHOULD use OpenCL render in
|
||||
the render action
|
||||
- 1 indicates that a plug-in SHOULD NOT use OpenCL render in
|
||||
the render action
|
||||
*/
|
||||
#define kOfxImageEffectPropOpenCLEnabled "OfxImageEffectPropOpenCLEnabled"
|
||||
|
||||
/** @brief Indicates the OpenCL command queue that should be used for rendering
|
||||
|
||||
- Type - pointer X 1
|
||||
- Property Set - inArgs property set of the following actions...
|
||||
- ::kOfxImageEffectActionRender
|
||||
- ::kOfxImageEffectActionBeginSequenceRender
|
||||
- ::kOfxImageEffectActionEndSequenceRender
|
||||
|
||||
This property contains a pointer to the command queue to be used for
|
||||
OpenCL rendering (cl_command_queue). In order to use it,
|
||||
reinterpret_cast<cl_command_queue>(pointer) is needed.
|
||||
|
||||
The plug-in SHOULD ensure that its render action enqueues any
|
||||
asynchronous OpenCL operations onto the supplied queue.
|
||||
|
||||
The plug-in SHOULD NOT wait for final asynchronous operations to
|
||||
complete before returning from the render action.
|
||||
*/
|
||||
#define kOfxImageEffectPropOpenCLCommandQueue "OfxImageEffectPropOpenCLCommandQueue"
|
||||
|
||||
/** @brief Indicates the image handle of an image supplied as an OpenCL Image by the host
|
||||
|
||||
- Type - pointer X 1
|
||||
- Property Set - image handle returned by clipGetImage
|
||||
|
||||
This value should be cast to a cl_mem and used as the image handle when performing
|
||||
OpenCL Images operations. The property should be used (not ::kOfxImagePropData) when
|
||||
rendering with OpenCL Images (::kOfxImageEffectPropOpenCLSupported), and should be used
|
||||
to determine whether Images or Buffers should be used if a plug-in supports both
|
||||
::kOfxImageEffectPropOpenCLSupported and ::kOfxImageEffectPropOpenCLRenderSupported.
|
||||
Note: the kOfxImagePropRowBytes property is not required to be set by the host, since
|
||||
OpenCL Images do not have the concept of row bytes.
|
||||
*/
|
||||
#define kOfxImageEffectPropOpenCLImage "OfxImageEffectPropOpenCLImage"
|
||||
|
||||
|
||||
#define kOfxOpenCLProgramSuite "OfxOpenCLProgramSuite"
|
||||
|
||||
/** @brief OFX suite that allows a plug-in to get OpenCL programs compiled
|
||||
|
||||
This is an optional suite the host can provide for building OpenCL programs for the plug-in,
|
||||
as an alternative to calling clCreateProgramWithSource / clBuildProgram. There are two advantages to
|
||||
doing this: The host can add flags (such as -cl-denorms-are-zero) to the build call, and may also
|
||||
cache program binaries for performance (however, if the source of the program or the OpenCL
|
||||
environment changes, the host must recompile so some mechanism such as hashing must be used).
|
||||
*/
|
||||
typedef struct OfxOpenCLProgramSuiteV1 {
|
||||
/** @brief Compiles the OpenCL program */
|
||||
OfxStatus(*compileProgram)(const char *pszProgramSource,
|
||||
int fOptional, // if non-zero, host may skip compiling on this call
|
||||
void *pResult); // cast to cl_program*
|
||||
} OfxOpenCLProgramSuiteV1;
|
||||
|
||||
|
||||
/** @page ofxOpenCLRender OpenCL Acceleration of Rendering
|
||||
|
||||
@section ofxOpenCLRenderIntro Introduction
|
||||
|
||||
The OpenCL extension enables plug-ins to use OpenCL commands (typically backed by a GPU) to accelerate rendering of their outputs. The basic scheme is simple....
|
||||
- an plug-in indicates it wants to use OpenCL acceleration by setting the ::kOfxImageEffectPropOpenCLSupported (Images) and/or ::kOfxImageEffectPropOpenCLRenderSupported (Buffers) flags on it's descriptor.
|
||||
- a host indicates it supports OpenCL acceleration by setting ::kOfxImageEffectPropOpenCLSupported (Images) and/or ::kOfxImageEffectPropOpenCLRenderSupported (Buffers) on it's descriptor.
|
||||
- the host decides when to use OpenCL, and sets the ::kOfxImageEffectPropOpenCLEnabled property on the BeginRender/Render/EndRender calls to indicate this.
|
||||
- when OpenCL Images are being used (::kOfxImageEffectPropOpenCLSupported) the clip image property ::kOfxImageEffectPropOpenCLImage will be set and non-null.
|
||||
- when OpenCL Buffers are being used (::kOfxImageEffectPropOpenCLRenderSupported) the clip image property ::kOfxImagePropData will be set and non-null.
|
||||
|
||||
@section ofxOpenCLRenderDiscoveryAndEnabling Discovery and Enabling
|
||||
|
||||
If a host supports OpenCL rendering then it flags with the string property ::kOfxImageEffectPropOpenCLSupported (Images) and/or
|
||||
::kOfxImageEffectPropOpenCLRenderSupported (Buffers) on its descriptor property set. Effects that cannot run without OpenCL support
|
||||
should examine this in ::kOfxActionDescribe action and return a ::kOfxStatErrMissingHostFeature status flag if it is not set to "true".
|
||||
|
||||
Effects flag to a host that they support OpenCL rendering by setting the string property ::kOfxImageEffectPropOpenCLSupported (Images)
|
||||
and/or ::kOfxImageEffectPropOpenCLRenderSupported (Buffers) on their effect descriptor during the ::kOfxActionDescribe action.
|
||||
Effects can work in two ways....
|
||||
- purely on CPUs without any OpenCL support at all, in which case they should set ::kOfxImageEffectPropOpenCLSupported (Images) and ::kOfxImageEffectPropOpenCLRenderSupported (Buffers) to be "false" (the default),
|
||||
- on CPUs but with optional OpenCL support, in which case they should set ::kOfxImageEffectPropOpenCLSupported (Images) and/or ::kOfxImageEffectPropOpenCLRenderSupported (Buffers) to be "true"
|
||||
|
||||
Host may support just OpenCL Images, just OpenCL Buffers, or both, as indicated by which of these two properties they set "true".
|
||||
Likewise plug-ins may support just OpenCL Images, just OpenCL Buffers, or both, as indicated by which of these two properties they set "true".
|
||||
If both host and plug-in support both, it is up to the host which it uses. Typically, it will be based on what it uses natively (to avoid an extra copy operation).
|
||||
If a plug-in supports both, it must use ::kOfxImageEffectPropOpenCLImage to determine if Images or Buffers are being used for a given render action.
|
||||
|
||||
Effects can use OpenCL render only during the following action:
|
||||
- ::kOfxImageEffectActionRender
|
||||
|
||||
If a plug-in has indicated that it optionally supports OpenCL acceleration, it should check the property ::kOfxImageEffectPropOpenCLEnabled
|
||||
passed as an in argument to the following actions,
|
||||
- ::kOfxImageEffectActionRender
|
||||
- ::kOfxImageEffectActionBeginSequenceRender
|
||||
- ::kOfxImageEffectActionEndSequenceRender
|
||||
|
||||
If this property is set to 0, then it must not attempt to use OpenCL while rendering.
|
||||
If this property is set to 1, then it must use OpenCL buffers or images while rendering.
|
||||
|
||||
If a call using OpenCL rendering fails, the host may re-attempt using CPU buffers instead, but this is not required, and might not be efficient.
|
||||
|
||||
@section ofxOpenCLRenderVersion OpenCL platform and device versions and feature support
|
||||
|
||||
Assume an in-order command queue. Do not assume a profiling command queue.
|
||||
|
||||
Effects should target OpenCL 1.1 API and OpenCL C kernel language support. Only minimum required features required
|
||||
in OpenCL 1.1 should be used (for example, see "5.3.2.1 Minimum List of Supported Image Formats" for the list
|
||||
of image types which can be expected to be supported across all devices). If you have specific requirements
|
||||
for features beyond these minimums, you will need to check the device (e.g., using clGetDeviceInfo with
|
||||
CL_DEVICE_EXTENSIONS) to see if your feature is available, and have a fallback if it's not.
|
||||
|
||||
Temporary buffers and images should not be kept past the render action. A separate extension for host managed caching is in the works.
|
||||
|
||||
Do not retain OpenCL objects without a matching release within the render action.
|
||||
|
||||
@section ofxOpenCLRenderMultipleDevices Multiple OpenCL Devices
|
||||
|
||||
This is very important: The host may support multiple OpenCL devices. Therefore the plug-in should keep a separate set of kernels per OpenCL content (e.g., using a map).
|
||||
The OpenCL context can be found from the command queue using clGetCommandQueueInfo with CL_QUEUE_CONTEXT.
|
||||
Failure to do this will cause crashes or incorrect results when the host switches to another OpenCL device.
|
||||
|
||||
*/
|
||||
|
||||
/** @}*/ // end OpenCLRender doc group
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /*__OFXGPURENDER_H__ */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,550 @@
|
||||
|
||||
|
||||
#ifndef _ofxInteract_h_
|
||||
#define _ofxInteract_h_
|
||||
|
||||
#include "ofxCore.h"
|
||||
|
||||
// Copyright OpenFX and contributors to the OpenFX project.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** @file ofxInteract.h
|
||||
Contains the API for ofx plugin defined GUIs and interaction.
|
||||
*/
|
||||
|
||||
#define kOfxInteractSuite "OfxInteractSuite"
|
||||
|
||||
|
||||
/** @brief Blind declaration of an OFX interactive gui
|
||||
*/
|
||||
typedef struct OfxInteract *OfxInteractHandle;
|
||||
|
||||
/**
|
||||
\addtogroup PropertiesAll
|
||||
*/
|
||||
/*@{*/
|
||||
/**
|
||||
\defgroup PropertiesInteract Interact Property Definitions
|
||||
|
||||
These are the list of properties used by the Interact API documented in \ref CustomInteractionPage.
|
||||
*/
|
||||
/*@{*/
|
||||
/** @brief The set of parameters on which a value change will trigger a redraw for an interact.
|
||||
|
||||
- Type - string X N
|
||||
- Property Set - interact instance property (read/write)
|
||||
- Default - no values set
|
||||
- Valid Values - the name of any parameter associated with this interact.
|
||||
|
||||
If the interact is representing the state of some set of OFX parameters, then is will
|
||||
need to be redrawn if any of those parameters' values change. This multi-dimensional property
|
||||
links such parameters to the interact.
|
||||
|
||||
The interact can be slaved to multiple parameters (setting index 0, then index 1 etc...)
|
||||
*/
|
||||
#define kOfxInteractPropSlaveToParam "OfxInteractPropSlaveToParam"
|
||||
|
||||
/** @brief The size of a real screen pixel under the interact's canonical projection.
|
||||
|
||||
- Type - double X 2
|
||||
- Property Set - interact instance and actions (read only)
|
||||
|
||||
*/
|
||||
#define kOfxInteractPropPixelScale "OfxInteractPropPixelScale"
|
||||
|
||||
|
||||
|
||||
/** @brief The background colour of the application behind an interact instance
|
||||
|
||||
- Type - double X 3
|
||||
- Property Set - read only on the interact instance and in argument to the ::kOfxInteractActionDraw action
|
||||
- Valid Values - from 0 to 1
|
||||
|
||||
The components are in the order red, green then blue.
|
||||
|
||||
*/
|
||||
#define kOfxInteractPropBackgroundColour "OfxInteractPropBackgroundColour"
|
||||
|
||||
/** @brief The suggested colour to draw a widget in an interact, typically for overlays.
|
||||
|
||||
- Type - double X 3
|
||||
- Property Set - read only on the interact instance
|
||||
- Default - 1.0
|
||||
- Valid Values - greater than or equal to 0.0
|
||||
|
||||
Some applications allow the user to specify colours of any overlay via a colour picker, this
|
||||
property represents the value of that colour. Plugins are at liberty to use this or not when
|
||||
they draw an overlay.
|
||||
|
||||
If a host does not support such a colour, it should return kOfxStatReplyDefault
|
||||
*/
|
||||
#define kOfxInteractPropSuggestedColour "OfxInteractPropSuggestedColour"
|
||||
|
||||
/** @brief The position of the pen in an interact.
|
||||
|
||||
- Type - double X 2
|
||||
- Property Set - read only in argument to the ::kOfxInteractActionPenMotion, ::kOfxInteractActionPenDown and ::kOfxInteractActionPenUp actions
|
||||
|
||||
This value passes the postion of the pen into an interact. This is in the interact's canonical coordinates.
|
||||
*/
|
||||
#define kOfxInteractPropPenPosition "OfxInteractPropPenPosition"
|
||||
|
||||
/** @brief The position of the pen in an interact in viewport coordinates.
|
||||
|
||||
- Type - int X 2
|
||||
- Property Set - read only in argument to the ::kOfxInteractActionPenMotion, ::kOfxInteractActionPenDown and ::kOfxInteractActionPenUp actions
|
||||
|
||||
This value passes the postion of the pen into an interact. This is in the interact's openGL viewport coordinates, with 0,0 being at the bottom left.
|
||||
*/
|
||||
#define kOfxInteractPropPenViewportPosition "OfxInteractPropPenViewportPosition"
|
||||
|
||||
/** @brief The pressure of the pen in an interact.
|
||||
|
||||
- Type - double X 1
|
||||
- Property Set - read only in argument to the ::kOfxInteractActionPenMotion, ::kOfxInteractActionPenDown and ::kOfxInteractActionPenUp actions
|
||||
- Valid Values - from 0 (no pressure) to 1 (maximum pressure)
|
||||
|
||||
This is used to indicate the status of the 'pen' in an interact. If a pen has only two states (eg: a mouse button), these should map to 0.0 and 1.0.
|
||||
*/
|
||||
#define kOfxInteractPropPenPressure "OfxInteractPropPenPressure"
|
||||
|
||||
/** @brief Indicates whether the dits per component in the interact's openGL frame buffer
|
||||
|
||||
- Type - int X 1
|
||||
- Property Set - interact instance and descriptor (read only)
|
||||
|
||||
*/
|
||||
#define kOfxInteractPropBitDepth "OfxInteractPropBitDepth"
|
||||
|
||||
/** @brief Indicates whether the interact's frame buffer has an alpha component or not
|
||||
|
||||
- Type - int X 1
|
||||
- Property Set - interact instance and descriptor (read only)
|
||||
- Valid Values - This must be one of
|
||||
- 0 indicates no alpha component
|
||||
- 1 indicates an alpha component
|
||||
*/
|
||||
#define kOfxInteractPropHasAlpha "OfxInteractPropHasAlpha"
|
||||
|
||||
/*@}*/
|
||||
/*@}*/
|
||||
|
||||
/**
|
||||
\addtogroup ActionsAll
|
||||
*/
|
||||
/*@{*/
|
||||
/**
|
||||
\defgroup InteractActions Interact Actions
|
||||
|
||||
These are the list of actions passed to an interact's entry point function. For more details on how to deal with actions, see \ref InteractActions.
|
||||
*/
|
||||
/*@{*/
|
||||
|
||||
/** @brief
|
||||
|
||||
This action is the first action passed to an interact. It is
|
||||
where an interact defines how it behaves and the resources it needs to
|
||||
function.
|
||||
If not trapped, the default action is for the host to carry on as normal
|
||||
Note that the handle passed in acts as a descriptor for, rather than an
|
||||
instance of the interact.
|
||||
|
||||
@param handle handle to the interact descriptor, cast to an \ref OfxInteractHandle
|
||||
@param inArgs is redundant and is set to NULL
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
|
||||
\pre
|
||||
- The plugin has been loaded and the effect described.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK the action was trapped and all was well
|
||||
- \ref kOfxStatErrMemory in which case describe may be called again after a memory purge
|
||||
- \ref kOfxStatFailed something was wrong, the host should ignore the interact
|
||||
- \ref kOfxStatErrFatal
|
||||
*/
|
||||
#define kOfxActionDescribeInteract kOfxActionDescribe
|
||||
|
||||
|
||||
/** @brief
|
||||
|
||||
This action is the first action passed to an interact instance after its creation.
|
||||
It is there to allow a plugin to create any per-instance data structures it may need.
|
||||
|
||||
@param handle handle to the interact instance, cast to an \ref OfxInteractHandle
|
||||
@param inArgs is redundant and is set to NULL
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionDescribe has been called on this interact
|
||||
|
||||
\post
|
||||
- the instance pointer will be valid until the \ref kOfxActionDestroyInstance
|
||||
action is passed to the plug-in with the same instance handle
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK the action was trapped and all was well
|
||||
- \ref kOfxStatReplyDefault the action was ignored, but all was well anyway
|
||||
- \ref kOfxStatErrFatal
|
||||
- \ref kOfxStatErrMemory in which case this may be called again after a memory purge
|
||||
- \ref kOfxStatFailed in which case the host should ignore this interact
|
||||
*/
|
||||
#define kOfxActionCreateInstanceInteract kOfxActionCreateInstance
|
||||
|
||||
/**@brief
|
||||
|
||||
This action is the last passed to an interact's instance before its
|
||||
destruction. It is there to allow a plugin to destroy any per-instance
|
||||
data structures it may have created.
|
||||
|
||||
@param handle handle to the interact instance, cast to an \ref OfxInteractHandle
|
||||
@param inArgs is redundant and is set to NULL
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance
|
||||
has been called on the handle,
|
||||
- the instance has not had any of its members destroyed yet
|
||||
|
||||
\post
|
||||
- the instance pointer is no longer valid and any operation on it will be undefined
|
||||
|
||||
@returns
|
||||
To some extent, what is returned is moot, a bit like throwing an
|
||||
exception in a C++ destructor, so the host should continue destruction
|
||||
of the instance regardless
|
||||
- \ref kOfxStatOK the action was trapped and all was well
|
||||
- \ref kOfxStatReplyDefault the action was ignored as the effect had nothing to do
|
||||
- \ref kOfxStatErrFatal
|
||||
- \ref kOfxStatFailed something went wrong, but no error code appropriate.
|
||||
*/
|
||||
#define kOfxActionDestroyInstanceInteract kOfxActionDestroyInstance
|
||||
|
||||
/** @brief
|
||||
|
||||
This action is issued to an interact whenever the host needs the plugin
|
||||
to redraw the given interact.
|
||||
|
||||
The interact should either issue OpenGL calls to draw itself, or use DrawSuite calls.
|
||||
|
||||
If this is called via kOfxImageEffectPluginPropOverlayInteractV2, drawing MUST use DrawSuite.
|
||||
|
||||
If this is called via kOfxImageEffectPluginPropOverlayInteractV1, drawing SHOULD use OpenGL. Some existing plugins may use DrawSuite via kOfxImageEffectPluginPropOverlayInteractV1 if it's supported by the host, but this is discouraged.
|
||||
|
||||
Note that the interact may (in the case of custom parameter GUIS) or may
|
||||
not (in the case of image effect overlays) be required to swap buffers,
|
||||
that is up to the kind of interact.
|
||||
|
||||
@param handle handle to an interact instance, cast to an \ref OfxInteractHandle
|
||||
@param inArgs has the following properties on an image effect plugin
|
||||
- \ref kOfxPropEffectInstance a handle to the effect for which the interact has been,
|
||||
- \ref kOfxInteractPropPixelScale the scale factor to convert cannonical pixels to screen pixels
|
||||
- \ref kOfxInteractPropBackgroundColour the background colour of the application behind the current view
|
||||
- \ref kOfxPropTime the effect time at which changed occured
|
||||
- \ref kOfxImageEffectPropRenderScale the render scale applied to any image fetched
|
||||
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance has been called on the instance handle
|
||||
- the openGL context for this interact has been set
|
||||
- the projection matrix will correspond to the interact's cannonical view
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK the action was trapped and all was well
|
||||
- \ref kOfxStatReplyDefault the action was ignored
|
||||
- \ref kOfxStatErrFatal
|
||||
- \ref kOfxStatFailed something went wrong, the host should ignore this interact in future
|
||||
*/
|
||||
#define kOfxInteractActionDraw "OfxInteractActionDraw"
|
||||
|
||||
/** @brief
|
||||
|
||||
This action is issued whenever the pen moves an the interact's has
|
||||
focus. It should be issued whether the pen is currently up or down.
|
||||
No openGL calls should be issued by the plug-in during this action.
|
||||
|
||||
|
||||
@param handle handle to an interact instance, cast to an \ref OfxInteractHandle
|
||||
@param inArgs has the following properties on an image effect plugin
|
||||
- \ref kOfxPropEffectInstance a handle to the effect for which the interact has been,
|
||||
- \ref kOfxInteractPropPixelScale the scale factor to convert cannonical pixels to screen pixels
|
||||
- \ref kOfxInteractPropBackgroundColour the background colour of the application behind the current view
|
||||
- \ref kOfxPropTime the effect time at which changed occured
|
||||
- \ref kOfxImageEffectPropRenderScale the render scale applied to any image fetched
|
||||
- \ref kOfxInteractPropPenPosition postion of the pen in,
|
||||
- \ref kOfxInteractPropPenViewportPosition position of the pen in,
|
||||
- \ref kOfxInteractPropPenPressure the pressure of the pen,
|
||||
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance
|
||||
has been called on the instance handle
|
||||
- the current instance handle has had
|
||||
\ref kOfxInteractActionGainFocus called on it
|
||||
|
||||
\post
|
||||
- if the instance returns \ref kOfxStatOK the host should not
|
||||
pass the pen motion to any other interactive object it may own that
|
||||
shares the same view.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK the action was trapped and the host should not pass the event to other objects it may own
|
||||
- \ref kOfxStatReplyDefault the action was not trapped and the host can deal with it if it wants
|
||||
*/
|
||||
#define kOfxInteractActionPenMotion "OfxInteractActionPenMotion"
|
||||
|
||||
/**@brief
|
||||
|
||||
This action is issued when a pen transitions for the 'up' to the 'down'
|
||||
state.
|
||||
No openGL calls should be issued by the plug-in during this action.
|
||||
|
||||
|
||||
@param handle handle to an interact instance, cast to an \ref OfxInteractHandle
|
||||
@param inArgs has the following properties on an image effect plugin,
|
||||
- \ref kOfxPropEffectInstance a handle to the effect for which the interact has been,
|
||||
- \ref kOfxInteractPropPixelScale the scale factor to convert cannonical pixels to screen pixels
|
||||
- \ref kOfxInteractPropBackgroundColour the background colour of the application behind the current view
|
||||
- \ref kOfxPropTime the effect time at which changed occured
|
||||
- \ref kOfxImageEffectPropRenderScale the render scale applied to any image fetched
|
||||
- \ref kOfxInteractPropPenPosition position of the pen in
|
||||
- \ref kOfxInteractPropPenViewportPosition position of the pen in
|
||||
- \ref kOfxInteractPropPenPressure the pressure of the pen
|
||||
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance
|
||||
has been called on the instance handle,
|
||||
- the current instance handle has had
|
||||
\ref kOfxInteractActionGainFocus
|
||||
called on it
|
||||
|
||||
\post
|
||||
- if the instance returns \ref kOfxStatOK, the host should not
|
||||
pass the pen motion to any other interactive object it may own that
|
||||
shares the same view.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped and the host should not pass the event to other objects it may own
|
||||
- \ref kOfxStatReplyDefault , the action was not trapped and the host can deal with it if it wants
|
||||
*/
|
||||
#define kOfxInteractActionPenDown "OfxInteractActionPenDown"
|
||||
|
||||
/**@brief
|
||||
|
||||
This action is issued when a pen transitions for the 'down' to the 'up'
|
||||
state.
|
||||
No openGL calls should be issued by the plug-in during this action.
|
||||
|
||||
@param handle handle to an interact instance, cast to an \ref OfxInteractHandle
|
||||
@param inArgs has the following properties on an image effect plugin,
|
||||
- \ref kOfxPropEffectInstance a handle to the effect for which the interact has been,
|
||||
- \ref kOfxInteractPropPixelScale the scale factor to convert cannonical pixels to screen pixels
|
||||
- \ref kOfxInteractPropBackgroundColour the background colour of the application behind the current view
|
||||
- \ref kOfxPropTime the effect time at which changed occured
|
||||
- \ref kOfxImageEffectPropRenderScale the render scale applied to any image fetched
|
||||
- \ref kOfxInteractPropPenPosition position of the pen in
|
||||
- \ref kOfxInteractPropPenViewportPosition position of the pen in
|
||||
- \ref kOfxInteractPropPenPressure the pressure of the pen
|
||||
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance
|
||||
has been called on the instance handle,
|
||||
- the current instance handle has had
|
||||
\ref kOfxInteractActionGainFocus called on it
|
||||
|
||||
\post
|
||||
- if the instance returns \ref kOfxStatOK, the host should not
|
||||
pass the pen motion to any other interactive object it may own that
|
||||
shares the same view.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK, the action was trapped and the host should not pass the event to other objects it may own
|
||||
- \ref kOfxStatReplyDefault , the action was not trapped and the host can deal with it if it wants
|
||||
*/
|
||||
#define kOfxInteractActionPenUp "OfxInteractActionPenUp"
|
||||
|
||||
/**@brief
|
||||
|
||||
This action is issued when a key on the keyboard is depressed.
|
||||
No openGL calls should be issued by the plug-in during this action.
|
||||
|
||||
@param handle handle to an interact instance, cast to an \ref OfxInteractHandle
|
||||
@param inArgs has the following properties on an image effect plugin
|
||||
- \ref kOfxPropEffectInstance a handle to the effect for which the interact has been,
|
||||
- \ref kOfxPropKeySym single integer value representing the key that was manipulated,
|
||||
this may not have a UTF8 representation (eg: a return key)
|
||||
- \ref kOfxPropKeyString UTF8 string representing a character key that was pressed, some
|
||||
keys have no UTF8 encoding, in which case this is ""
|
||||
- \ref kOfxPropTime the effect time at which changed occured
|
||||
- \ref kOfxImageEffectPropRenderScale the render scale applied to any image fetched
|
||||
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance
|
||||
has been called on the instance handle,
|
||||
- the current instance handle has had
|
||||
\ref kOfxInteractActionGainFocus called on it
|
||||
|
||||
\post
|
||||
- if the instance returns \ref kOfxStatOK, the host should not
|
||||
pass the pen motion to any other interactive object it may own that
|
||||
shares the same focus.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK , the action was trapped and the host should not pass the event to other objects it may own
|
||||
- \ref kOfxStatReplyDefault , the action was not trapped and the host can deal with it if it wants
|
||||
*/
|
||||
#define kOfxInteractActionKeyDown "OfxInteractActionKeyDown"
|
||||
|
||||
/**@brief
|
||||
This action is issued when a key on the keyboard is released.
|
||||
No openGL calls should be issued by the plug-in during this action.
|
||||
|
||||
@param handle handle to an interact instance, cast to an \ref OfxInteractHandle
|
||||
@param inArgs has the following properties on an image effect plugin
|
||||
- \ref kOfxPropEffectInstance a handle to the effect for which the interact has been,
|
||||
- \ref kOfxPropKeySym single integer value representing the key that was manipulated,
|
||||
this may not have a UTF8 representation (eg: a return key)
|
||||
- \ref kOfxPropKeyString UTF8 string representing a character key that was pressed, some
|
||||
keys have no UTF8 encoding, in which case this is ""
|
||||
- \ref kOfxPropTime the effect time at which changed occured
|
||||
- \ref kOfxImageEffectPropRenderScale the render scale applied to any image fetched
|
||||
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance
|
||||
has been called on the instance handle,
|
||||
- the current instance handle has had
|
||||
\ref kOfxInteractActionGainFocus called on it
|
||||
|
||||
\post
|
||||
- if the instance returns \ref kOfxStatOK, the host should not
|
||||
pass the pen motion to any other interactive object it may own that
|
||||
shares the same focus.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK , the action was trapped and the host should not pass the event to other objects it may own
|
||||
- \ref kOfxStatReplyDefault , the action was not trapped and the host can deal with it if it wants
|
||||
*/
|
||||
#define kOfxInteractActionKeyUp "OfxInteractActionKeyUp"
|
||||
|
||||
/**@brief
|
||||
This action is issued when a key on the keyboard is repeated.
|
||||
No openGL calls should be issued by the plug-in during this action.
|
||||
|
||||
@param handle handle to an interact instance, cast to an \ref OfxInteractHandle
|
||||
@param inArgs has the following properties on an image effect plugin
|
||||
- \ref kOfxPropEffectInstance a handle to the effect for which the interact has been,
|
||||
- \ref kOfxPropKeySym single integer value representing the key that was manipulated,
|
||||
this may not have a UTF8 representation (eg: a return key)
|
||||
- \ref kOfxPropKeyString UTF8 string representing a character key that was pressed, some
|
||||
keys have no UTF8 encoding, in which case this is ""
|
||||
- \ref kOfxPropTime the effect time at which changed occured
|
||||
- \ref kOfxImageEffectPropRenderScale the render scale applied to any image fetched
|
||||
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance
|
||||
has been called on the instance handle,
|
||||
- the current instance handle has had
|
||||
\ref kOfxInteractActionGainFocus called on it
|
||||
|
||||
\post
|
||||
- if the instance returns \ref kOfxStatOK, the host should not
|
||||
pass the pen motion to any other interactive object it may own that
|
||||
shares the same focus.
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK , the action was trapped and the host should not pass the event to other objects it may own
|
||||
- \ref kOfxStatReplyDefault , the action was not trapped and the host can deal with it if it wants
|
||||
*/
|
||||
#define kOfxInteractActionKeyRepeat "OfxInteractActionKeyRepeat"
|
||||
|
||||
/**@brief
|
||||
This action is issued when an interact gains input focus.
|
||||
No openGL calls should be issued by the plug-in during this action.
|
||||
|
||||
@param handle handle to an interact instance, cast to an \ref OfxInteractHandle
|
||||
@param inArgs has the following properties on an image effect plugin
|
||||
- \ref kOfxPropEffectInstance a handle to the effect for which the interact is being used on,
|
||||
- \ref kOfxInteractPropPixelScale the scale factor to convert cannonical pixels to screen pixels,
|
||||
- \ref kOfxInteractPropBackgroundColour the background colour of the application behind the current view
|
||||
- \ref kOfxPropTime the effect time at which changed occured
|
||||
- \ref kOfxImageEffectPropRenderScale the render scale applied to any image fetched
|
||||
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance
|
||||
has been called on the instance handle,
|
||||
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK the action was trapped
|
||||
- \ref kOfxStatReplyDefault the action was not trapped
|
||||
*/
|
||||
#define kOfxInteractActionGainFocus "OfxInteractActionGainFocus"
|
||||
|
||||
/**@brief
|
||||
This action is issued when an interact loses input focus.
|
||||
No openGL calls should be issued by the plug-in during this action.
|
||||
|
||||
@param handle handle to an interact instance, cast to an \ref OfxInteractHandle
|
||||
@param inArgs has the following properties on an image effect plugin
|
||||
- \ref kOfxPropEffectInstance a handle to the effect for which the interact is being used on,
|
||||
- \ref kOfxInteractPropPixelScale the scale factor to convert cannonical pixels to screen pixels,
|
||||
- \ref kOfxInteractPropBackgroundColour the background colour of the application behind the current view
|
||||
- \ref kOfxPropTime the effect time at which changed occured
|
||||
- \ref kOfxImageEffectPropRenderScale the render scale applied to any image fetched
|
||||
|
||||
@param outArgs is redundant and is set to NULL
|
||||
|
||||
\pre
|
||||
- \ref kOfxActionCreateInstance
|
||||
has been called on the instance handle,
|
||||
|
||||
|
||||
@returns
|
||||
- \ref kOfxStatOK the action was trapped
|
||||
- \ref kOfxStatReplyDefault the action was not trapped
|
||||
*/
|
||||
#define kOfxInteractActionLoseFocus "OfxInteractActionLoseFocus"
|
||||
|
||||
/*@}*/
|
||||
/*@}*/
|
||||
|
||||
/** @brief OFX suite that allows an effect to interact with an openGL window so as to provide custom interfaces.
|
||||
|
||||
*/
|
||||
typedef struct OfxInteractSuiteV1 {
|
||||
/** @brief Requests an openGL buffer swap on the interact instance */
|
||||
OfxStatus (*interactSwapBuffers)(OfxInteractHandle interactInstance);
|
||||
|
||||
/** @brief Requests a redraw of the interact instance */
|
||||
OfxStatus (*interactRedraw)(OfxInteractHandle interactInstance);
|
||||
|
||||
/** @brief Gets the property set handle for this interact handle */
|
||||
OfxStatus (*interactGetPropertySet)(OfxInteractHandle interactInstance,
|
||||
OfxPropertySetHandle *property);
|
||||
} OfxInteractSuiteV1;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,68 @@
|
||||
|
||||
|
||||
#ifndef _ofxMemory_h_
|
||||
#define _ofxMemory_h_
|
||||
|
||||
// Copyright OpenFX and contributors to the OpenFX project.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define kOfxMemorySuite "OfxMemorySuite"
|
||||
|
||||
/** @brief The OFX suite that implements general purpose memory management.
|
||||
|
||||
Use this suite for ordinary memory management functions, where you would normally use malloc/free or new/delete on ordinary objects.
|
||||
|
||||
For images, you should use the memory allocation functions in the image effect suite, as many hosts have specific image memory pools.
|
||||
|
||||
\note C++ plugin developers will need to redefine new and delete as skins ontop of this suite.
|
||||
*/
|
||||
typedef struct OfxMemorySuiteV1 {
|
||||
/** @brief Allocate memory.
|
||||
|
||||
\arg \c handle - effect instance to assosciate with this memory allocation, or NULL.
|
||||
\arg \c nBytes number of bytes to allocate
|
||||
\arg \c allocatedData pointer to the return value. Allocated memory will be alligned for any use.
|
||||
|
||||
This function has the host allocate memory using its own memory resources
|
||||
and returns that to the plugin.
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK the memory was sucessfully allocated
|
||||
- ::kOfxStatErrMemory the request could not be met and no memory was allocated
|
||||
|
||||
*/
|
||||
OfxStatus (*memoryAlloc)(void *handle,
|
||||
size_t nBytes,
|
||||
void **allocatedData);
|
||||
|
||||
/** @brief Frees memory.
|
||||
|
||||
\arg \c allocatedData pointer to memory previously returned by OfxMemorySuiteV1::memoryAlloc
|
||||
|
||||
This function frees any memory that was previously allocated via OfxMemorySuiteV1::memoryAlloc.
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK the memory was sucessfully freed
|
||||
- ::kOfxStatErrBadHandle \e allocatedData was not a valid pointer returned by OfxMemorySuiteV1::memoryAlloc
|
||||
|
||||
*/
|
||||
OfxStatus (*memoryFree)(void *allocatedData);
|
||||
} OfxMemorySuiteV1;
|
||||
|
||||
|
||||
/** @file ofxMemory.h
|
||||
This file contains the API for general purpose memory allocation from a host.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,171 @@
|
||||
|
||||
|
||||
#ifndef _ofxMessage_h_
|
||||
#define _ofxMessage_h_
|
||||
|
||||
#include "ofxCore.h"
|
||||
|
||||
// Copyright OpenFX and contributors to the OpenFX project.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define kOfxMessageSuite "OfxMessageSuite"
|
||||
|
||||
|
||||
/** @brief String used to type fatal error messages
|
||||
|
||||
Fatal error messages should only be posted by a plugin when it can no longer continue operation.
|
||||
*/
|
||||
#define kOfxMessageFatal "OfxMessageFatal"
|
||||
|
||||
/** @brief String used to type error messages
|
||||
|
||||
Ordinary error messages should be posted when there is an error in operation that is recoverable by
|
||||
user intervention.
|
||||
*/
|
||||
#define kOfxMessageError "OfxMessageError"
|
||||
|
||||
/** @brief String used to type warning messages
|
||||
|
||||
Warnings indicate states that allow for operations to proceed, but are not necessarily optimal.
|
||||
*/
|
||||
#define kOfxMessageWarning "OfxMessageWarning"
|
||||
|
||||
/** @brief String used to type simple ordinary messages
|
||||
|
||||
Ordinary messages simply convey information from the plugin directly to the user.
|
||||
*/
|
||||
#define kOfxMessageMessage "OfxMessageMessage"
|
||||
|
||||
/** @brief String used to type log messages
|
||||
|
||||
Log messages are written out to a log and not to the end user.
|
||||
*/
|
||||
#define kOfxMessageLog "OfxMessageLog"
|
||||
|
||||
/** @brief String used to type yes/no messages
|
||||
|
||||
The host is to enter a modal state which waits for the user to respond yes or no.
|
||||
The OfxMessageSuiteV1::message function which posted the message will only return after
|
||||
the user responds. When asking a question, the OfxStatus code returned by the message function will be,
|
||||
- kOfxStatReplyYes - if the user replied 'yes' to the question
|
||||
- kOfxStatReplyNo - if the user replied 'no' to the question
|
||||
- some error code if an error was encounterred
|
||||
|
||||
It is an error to post a question message if the plugin is not in an interactive session.
|
||||
*/
|
||||
#define kOfxMessageQuestion "OfxMessageQuestion"
|
||||
|
||||
/** @brief The OFX suite that allows a plug-in to pass messages back to a user. The V2 suite extends on this
|
||||
in a backwards compatible manner.
|
||||
*/
|
||||
typedef struct OfxMessageSuiteV1 {
|
||||
|
||||
/** @brief Post a message on the host, using printf style varargs
|
||||
|
||||
\arg \c handle effect handle (descriptor or instance) the message should be associated with, may be NULL
|
||||
\arg \c messageType string describing the kind of message to post, one of the kOfxMessageType* constants
|
||||
\arg \c messageId plugin specified id to associate with this message. If overriding the message in XML resource, the message is identified with this, this may be NULL, or "", in which case no override will occur,
|
||||
\arg \c format printf style format string
|
||||
\arg \c ... printf style varargs list to print
|
||||
|
||||
\returns
|
||||
- ::kOfxStatOK - if the message was sucessfully posted
|
||||
- ::kOfxStatReplyYes - if the message was of type kOfxMessageQuestion and the user reply yes
|
||||
- ::kOfxStatReplyNo - if the message was of type kOfxMessageQuestion and the user reply no
|
||||
- ::kOfxStatFailed - if the message could not be posted for some reason
|
||||
|
||||
*/
|
||||
OfxStatus (*message)(void *handle,
|
||||
const char *messageType,
|
||||
const char *messageId,
|
||||
const char *format,
|
||||
...);
|
||||
|
||||
} OfxMessageSuiteV1;
|
||||
|
||||
/** @brief The OFX suite that allows a plug-in to pass messages back to a user.
|
||||
|
||||
This extends OfxMessageSuiteV1, and should be considered a replacement to version 1.
|
||||
|
||||
Note that this suite has been extended in backwards compatible manner, so that a host can return this struct
|
||||
for both V1 and V2.
|
||||
*/
|
||||
typedef struct OfxMessageSuiteV2 {
|
||||
|
||||
/** @brief Post a transient message on the host, using printf style varargs. Same as the V1 message suite call.
|
||||
|
||||
\arg \c handle effect handle (descriptor or instance) the message should be associated with, may be null
|
||||
\arg \c messageType string describing the kind of message to post, one of the kOfxMessageType* constants
|
||||
\arg \c messageId plugin specified id to associate with this message. If overriding the message in XML resource, the message is identified with this, this may be NULL, or "", in which case no override will occur,
|
||||
\arg \c format printf style format string
|
||||
\arg \c ... printf style varargs list to print
|
||||
|
||||
\returns
|
||||
- ::kOfxStatOK - if the message was sucessfully posted
|
||||
- ::kOfxStatReplyYes - if the message was of type kOfxMessageQuestion and the user reply yes
|
||||
- ::kOfxStatReplyNo - if the message was of type kOfxMessageQuestion and the user reply no
|
||||
- ::kOfxStatFailed - if the message could not be posted for some reason
|
||||
*/
|
||||
OfxStatus (*message)(void *handle,
|
||||
const char *messageType,
|
||||
const char *messageId,
|
||||
const char *format,
|
||||
...);
|
||||
|
||||
/** @brief Post a persistent message on an effect, using printf style varargs, and set error states. New for V2 message suite.
|
||||
|
||||
\arg \c handle effect instance handle the message should be associated with, may NOT be null,
|
||||
\arg \c messageType string describing the kind of message to post, should be one of...
|
||||
- kOfxMessageError
|
||||
- kOfxMessageWarning
|
||||
- kOfxMessageMessage
|
||||
\arg \c messageId plugin specified id to associate with this message. If overriding the message in XML resource, the message is identified with this, this may be NULL, or "", in which case no override will occur,
|
||||
\arg \c format printf style format string
|
||||
\arg \c ... printf style varargs list to print
|
||||
|
||||
\returns
|
||||
- ::kOfxStatOK - if the message was sucessfully posted
|
||||
- ::kOfxStatErrBadHandle - the handle was rubbish
|
||||
- ::kOfxStatFailed - if the message could not be posted for some reason
|
||||
|
||||
Persistent messages are associated with an effect handle until explicitly cleared by an effect. So if an error message is posted the error state, and associated message will persist and be displayed on the effect appropriately. (eg: draw a node in red on a node based compostor and display the message when clicked on).
|
||||
|
||||
If \e messageType is error or warning, associated error states should be flagged on host applications. Posting an error message implies that the host cannot proceeed, a warning allows the host to proceed, whilst a simple message should have no stop anything.
|
||||
*/
|
||||
OfxStatus (*setPersistentMessage)(void *handle,
|
||||
const char *messageType,
|
||||
const char *messageId,
|
||||
const char *format,
|
||||
...);
|
||||
|
||||
|
||||
/** @brief Clears any persistent message on an effect handle that was set by OfxMessageSuiteV2::setPersistentMessage. New for V2 message suite.
|
||||
|
||||
\arg \c handle effect instance handle messages should be cleared from.
|
||||
\arg \c handle effect handle (descriptor or instance)
|
||||
|
||||
\returns
|
||||
- ::kOfxStatOK - if the message was sucessfully cleared
|
||||
- ::kOfxStatErrBadHandle - the handle was rubbish
|
||||
- ::kOfxStatFailed - if the message could not be cleared for some reason
|
||||
|
||||
Clearing a message will clear any associated error state.
|
||||
*/
|
||||
OfxStatus (*clearPersistentMessage)(void *handle);
|
||||
|
||||
} OfxMessageSuiteV2;
|
||||
|
||||
/** @file ofxMessage.h
|
||||
This file contains the Host API for end user message communication.
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,168 @@
|
||||
|
||||
|
||||
#ifndef _ofxMultiThread_h_
|
||||
#define _ofxMultiThread_h_
|
||||
|
||||
// Copyright OpenFX and contributors to the OpenFX project.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
|
||||
#include "ofxCore.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** @file ofxMultiThread.h
|
||||
|
||||
This file contains the Host Suite for threading
|
||||
*/
|
||||
|
||||
#define kOfxMultiThreadSuite "OfxMultiThreadSuite"
|
||||
|
||||
/** @brief Mutex blind data handle
|
||||
*/
|
||||
typedef struct OfxMutex *OfxMutexHandle;
|
||||
|
||||
/** @brief The function type to passed to the multi threading routines
|
||||
|
||||
\arg \c threadIndex unique index of this thread, will be between 0 and threadMax
|
||||
\arg \c threadMax to total number of threads executing this function
|
||||
\arg \c customArg the argument passed into multiThread
|
||||
|
||||
A function of this type is passed to OfxMultiThreadSuiteV1::multiThread to be launched in multiple threads.
|
||||
*/
|
||||
typedef void (OfxThreadFunctionV1)(unsigned int threadIndex,
|
||||
unsigned int threadMax,
|
||||
void *customArg);
|
||||
|
||||
/** @brief OFX suite that provides simple SMP style multi-processing
|
||||
*/
|
||||
typedef struct OfxMultiThreadSuiteV1 {
|
||||
/**@brief Function to spawn SMP threads
|
||||
|
||||
\arg \c func function to call in each thread.
|
||||
\arg \c nThreads number of threads to launch
|
||||
\arg \c customArg paramter to pass to customArg of func in each thread.
|
||||
|
||||
This function will spawn nThreads separate threads of computation (typically one per CPU)
|
||||
to allow something to perform symmetric multi processing. Each thread will call 'func' passing
|
||||
in the index of the thread and the number of threads actually launched.
|
||||
|
||||
multiThread will not return until all the spawned threads have returned. It is up to the host
|
||||
how it waits for all the threads to return (busy wait, blocking, whatever).
|
||||
|
||||
\e nThreads can be more than the value returned by multiThreadNumCPUs, however the threads will
|
||||
be limitted to the number of CPUs returned by multiThreadNumCPUs.
|
||||
|
||||
This function cannot be called recursively.
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK, the function func has executed and returned sucessfully
|
||||
- ::kOfxStatFailed, the threading function failed to launch
|
||||
- ::kOfxStatErrExists, failed in an attempt to call multiThread recursively,
|
||||
|
||||
*/
|
||||
OfxStatus (*multiThread)(OfxThreadFunctionV1 func,
|
||||
unsigned int nThreads,
|
||||
void *customArg);
|
||||
|
||||
/**@brief Function which indicates the number of CPUs available for SMP processing
|
||||
|
||||
\arg \c nCPUs pointer to an integer where the result is returned
|
||||
|
||||
This value may be less than the actual number of CPUs on a machine, as the host may reserve other CPUs for itself.
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK, all was OK and the maximum number of threads is in nThreads.
|
||||
- ::kOfxStatFailed, the function failed to get the number of CPUs
|
||||
*/
|
||||
OfxStatus (*multiThreadNumCPUs)(unsigned int *nCPUs);
|
||||
|
||||
/**@brief Function which indicates the index of the current thread
|
||||
|
||||
\arg \c threadIndex pointer to an integer where the result is returned
|
||||
|
||||
This function returns the thread index, which is the same as the \e threadIndex argument passed to the ::OfxThreadFunctionV1.
|
||||
|
||||
If there are no threads currently spawned, then this function will set threadIndex to 0
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK, all was OK and the maximum number of threads is in nThreads.
|
||||
- ::kOfxStatFailed, the function failed to return an index
|
||||
*/
|
||||
OfxStatus (*multiThreadIndex)(unsigned int *threadIndex);
|
||||
|
||||
/**@brief Function to enquire if the calling thread was spawned by multiThread
|
||||
|
||||
@returns
|
||||
- 0 if the thread is not one spawned by multiThread
|
||||
- 1 if the thread was spawned by multiThread
|
||||
*/
|
||||
int (*multiThreadIsSpawnedThread)(void);
|
||||
|
||||
/** @brief Create a mutex
|
||||
|
||||
\arg \c mutex where the new handle is returned
|
||||
\arg \c count initial lock count on the mutex. This can be negative.
|
||||
|
||||
Creates a new mutex with lockCount locks on the mutex intially set.
|
||||
|
||||
@returns
|
||||
- kOfxStatOK - mutex is now valid and ready to go
|
||||
*/
|
||||
OfxStatus (*mutexCreate)(OfxMutexHandle *mutex, int lockCount);
|
||||
|
||||
/** @brief Destroy a mutex
|
||||
|
||||
Destroys a mutex intially created by mutexCreate.
|
||||
|
||||
@returns
|
||||
- kOfxStatOK - if it destroyed the mutex
|
||||
- kOfxStatErrBadHandle - if the handle was bad
|
||||
*/
|
||||
OfxStatus (*mutexDestroy)(const OfxMutexHandle mutex);
|
||||
|
||||
/** @brief Blocking lock on the mutex
|
||||
|
||||
This trys to lock a mutex and blocks the thread it is in until the lock suceeds.
|
||||
|
||||
A sucessful lock causes the mutex's lock count to be increased by one and to block any other calls to lock the mutex until it is unlocked.
|
||||
|
||||
@returns
|
||||
- kOfxStatOK - if it got the lock
|
||||
- kOfxStatErrBadHandle - if the handle was bad
|
||||
*/
|
||||
OfxStatus (*mutexLock)(const OfxMutexHandle mutex);
|
||||
|
||||
/** @brief Unlock the mutex
|
||||
|
||||
This unlocks a mutex. Unlocking a mutex decreases its lock count by one.
|
||||
|
||||
@returns
|
||||
- kOfxStatOK if it released the lock
|
||||
- kOfxStatErrBadHandle if the handle was bad
|
||||
*/
|
||||
OfxStatus (*mutexUnLock)(const OfxMutexHandle mutex);
|
||||
|
||||
/** @brief Non blocking attempt to lock the mutex
|
||||
|
||||
This attempts to lock a mutex, if it cannot, it returns and says so, rather than blocking.
|
||||
|
||||
A sucessful lock causes the mutex's lock count to be increased by one, if the lock did not suceed, the call returns immediately and the lock count remains unchanged.
|
||||
|
||||
@returns
|
||||
- kOfxStatOK - if it got the lock
|
||||
- kOfxStatFailed - if it did not get the lock
|
||||
- kOfxStatErrBadHandle - if the handle was bad
|
||||
*/
|
||||
OfxStatus (*mutexTryLock)(const OfxMutexHandle mutex);
|
||||
|
||||
} OfxMultiThreadSuiteV1;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
|
||||
|
||||
#ifndef _ofxProgressSuite_h_
|
||||
#define _ofxProgressSuite_h_
|
||||
|
||||
/** @brief suite for displaying a progress bar */
|
||||
#define kOfxProgressSuite "OfxProgressSuite"
|
||||
|
||||
/** @brief A suite that provides progress feedback from a plugin to an application
|
||||
|
||||
A plugin instance can initiate, update and close a progress indicator with
|
||||
this suite.
|
||||
|
||||
This is an optional suite in the Image Effect API.
|
||||
|
||||
API V1.4: Amends the documentation of progress suite V1 so that it is
|
||||
expected that it can be raised in a modal manner and have a "cancel"
|
||||
button when invoked in instanceChanged. Plugins that perform analysis
|
||||
post an appropriate message, raise the progress monitor in a modal manner
|
||||
and should poll to see if processing has been aborted. Any cancellation
|
||||
should be handled gracefully by the plugin (eg: reset analysis parameters
|
||||
to default values), clear allocated memory...
|
||||
|
||||
Many hosts already operate as described above. kOfxStatReplyNo should be
|
||||
returned to the plugin during progressUpdate when the user presses
|
||||
cancel.
|
||||
|
||||
Suite V2: Adds an ID that can be looked up for internationalisation and
|
||||
so on. When a new version is introduced, because plug-ins need to support
|
||||
old versions, and plug-in's new releases are not necessary in synch with
|
||||
hosts (or users don't immediately update), best practice is to support
|
||||
the 2 suite versions. That is, the plugin should check if V2 exists; if
|
||||
not then check if V1 exists. This way a graceful transition is
|
||||
guaranteed. So plugin should fetchSuite passing 2,
|
||||
(OfxProgressSuiteV2*) fetchSuite(mHost->mHost->host, kOfxProgressSuite,2);
|
||||
and if no success pass (OfxProgressSuiteV1*)
|
||||
fetchSuite(mHost->mHost->host, kOfxProgressSuite,1);
|
||||
*/
|
||||
typedef struct OfxProgressSuiteV1 {
|
||||
|
||||
/** @brief Initiate a progress bar display.
|
||||
|
||||
Call this to initiate the display of a progress bar.
|
||||
|
||||
\arg \c effectInstance the instance of the plugin this progress bar is
|
||||
associated with. It cannot be NULL.
|
||||
\arg \c label a text label to display in any message portion of the
|
||||
progress object's user interface. A UTF8 string.
|
||||
|
||||
\pre - There is no currently ongoing progress display for this instance.
|
||||
|
||||
\returns
|
||||
- ::kOfxStatOK - the handle is now valid for use
|
||||
- ::kOfxStatFailed - the progress object failed for some reason
|
||||
- ::kOfxStatErrBadHandle - effectInstance was invalid
|
||||
*/
|
||||
OfxStatus (*progressStart)(void *effectInstance,
|
||||
const char *label);
|
||||
|
||||
/** @brief Indicate how much of the processing task has been completed and reports on any abort status.
|
||||
|
||||
\arg \c effectInstance the instance of the plugin this progress bar is
|
||||
associated with. It cannot be NULL.
|
||||
\arg \c progress a number between 0.0 and 1.0 indicating what proportion of the current task has been processed.
|
||||
|
||||
\returns
|
||||
- ::kOfxStatOK - the progress object was successfully updated and the task should continue
|
||||
- ::kOfxStatReplyNo - the progress object was successfully updated and the task should abort
|
||||
- ::kOfxStatErrBadHandle - the progress handle was invalid,
|
||||
*/
|
||||
OfxStatus (*progressUpdate)(void *effectInstance, double progress);
|
||||
|
||||
/** @brief Signal that we are finished with the progress meter.
|
||||
|
||||
Call this when you are done with the progress meter and no
|
||||
longer need it displayed.
|
||||
|
||||
\arg \c effectInstance the instance of the plugin this progress bar is
|
||||
associated with. It cannot be NULL.
|
||||
|
||||
\post - you can no longer call progressUpdate on the instance
|
||||
|
||||
\returns
|
||||
- ::kOfxStatOK - the progress object was successfully closed
|
||||
- ::kOfxStatErrBadHandle - the progress handle was invalid,
|
||||
*/
|
||||
OfxStatus (*progressEnd)(void *effectInstance);
|
||||
|
||||
} OfxProgressSuiteV1 ;
|
||||
|
||||
|
||||
|
||||
typedef struct OfxProgressSuiteV2 {
|
||||
|
||||
/** @brief Initiate a progress bar display.
|
||||
|
||||
Call this to initiate the display of a progress bar.
|
||||
|
||||
\arg \c effectInstance the instance of the plugin this progress bar is
|
||||
associated with. It cannot be NULL.
|
||||
\arg \c message a text label to display in any message portion of the
|
||||
progress object's user interface. A UTF8 string.
|
||||
\arg \c messageId plugin-specified id to associate with this message.
|
||||
If overriding the message in an XML resource, the message
|
||||
is identified with this, this may be NULL, or "", in
|
||||
which case no override will occur.
|
||||
New in V2 of this suite.
|
||||
|
||||
\pre - There is no currently ongoing progress display for this instance.
|
||||
|
||||
\returns
|
||||
- ::kOfxStatOK - the handle is now valid for use
|
||||
- ::kOfxStatFailed - the progress object failed for some reason
|
||||
- ::kOfxStatErrBadHandle - effectInstance was invalid
|
||||
*/
|
||||
OfxStatus (*progressStart)(void *effectInstance,
|
||||
const char *message,
|
||||
const char *messageid);
|
||||
/** @brief Indicate how much of the processing task has been completed and reports on any abort status.
|
||||
|
||||
\arg \c effectInstance the instance of the plugin this progress bar is
|
||||
associated with. It cannot be NULL.
|
||||
\arg \c progress a number between 0.0 and 1.0 indicating what proportion of the current task has been processed.
|
||||
|
||||
\returns
|
||||
- ::kOfxStatOK - the progress object was successfully updated and the task should continue
|
||||
- ::kOfxStatReplyNo - the progress object was successfully updated and the task should abort
|
||||
- ::kOfxStatErrBadHandle - the progress handle was invalid,
|
||||
*/
|
||||
OfxStatus (*progressUpdate)(void *effectInstance, double progress);
|
||||
|
||||
/** @brief Signal that we are finished with the progress meter.
|
||||
|
||||
Call this when you are done with the progress meter and no
|
||||
longer need it displayed.
|
||||
|
||||
\arg \c effectInstance the instance of the plugin this progress bar is
|
||||
associated with. It cannot be NULL.
|
||||
|
||||
\post - you can no longer call progressUpdate on the instance
|
||||
|
||||
\returns
|
||||
- ::kOfxStatOK - the progress object was successfully closed
|
||||
- ::kOfxStatErrBadHandle - the progress handle was invalid,
|
||||
*/
|
||||
OfxStatus (*progressEnd)(void *effectInstance);
|
||||
|
||||
} OfxProgressSuiteV2 ;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,321 @@
|
||||
|
||||
|
||||
#ifndef _ofxPropertyHost_h_
|
||||
#define _ofxPropertyHost_h_
|
||||
|
||||
// Copyright OpenFX and contributors to the OpenFX project.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
|
||||
#include "ofxCore.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** @file ofxProperty.h
|
||||
Contains the API for manipulating generic properties. For more details see \ref PropertiesPage.
|
||||
*/
|
||||
|
||||
#define kOfxPropertySuite "OfxPropertySuite"
|
||||
|
||||
/** @brief The OFX suite used to access properties on OFX objects.
|
||||
|
||||
*/
|
||||
typedef struct OfxPropertySuiteV1 {
|
||||
/** @brief Set a single value in a pointer property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c index for multidimenstional properties and is dimension of the one we are setting
|
||||
\arg \c value value of the property we are setting
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
- ::kOfxStatErrValue
|
||||
*/
|
||||
OfxStatus (*propSetPointer)(OfxPropertySetHandle properties, const char *property, int index, void *value);
|
||||
|
||||
/** @brief Set a single value in a string property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c index for multidimenstional properties and is dimension of the one we are setting
|
||||
\arg \c value value of the property we are setting
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
- ::kOfxStatErrValue
|
||||
*/
|
||||
OfxStatus (*propSetString) (OfxPropertySetHandle properties, const char *property, int index, const char *value);
|
||||
|
||||
/** @brief Set a single value in a double property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c index for multidimenstional properties and is dimension of the one we are setting
|
||||
\arg \c value value of the property we are setting
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
- ::kOfxStatErrValue
|
||||
*/
|
||||
OfxStatus (*propSetDouble) (OfxPropertySetHandle properties, const char *property, int index, double value);
|
||||
|
||||
/** @brief Set a single value in an int property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c index for multidimenstional properties and is dimension of the one we are setting
|
||||
\arg \c value value of the property we are setting
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
- ::kOfxStatErrValue
|
||||
*/
|
||||
OfxStatus (*propSetInt) (OfxPropertySetHandle properties, const char *property, int index, int value);
|
||||
|
||||
/** @brief Set multiple values of the pointer property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c count number of values we are setting in that property (ie: indicies 0..count-1)
|
||||
\arg \c value pointer to an array of property values
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
- ::kOfxStatErrValue
|
||||
*/
|
||||
OfxStatus (*propSetPointerN)(OfxPropertySetHandle properties, const char *property, int count, void *const*value);
|
||||
|
||||
/** @brief Set multiple values of a string property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c count number of values we are setting in that property (ie: indicies 0..count-1)
|
||||
\arg \c value pointer to an array of property values
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
- ::kOfxStatErrValue
|
||||
*/
|
||||
OfxStatus (*propSetStringN) (OfxPropertySetHandle properties, const char *property, int count, const char *const*value);
|
||||
|
||||
/** @brief Set multiple values of a double property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c count number of values we are setting in that property (ie: indicies 0..count-1)
|
||||
\arg \c value pointer to an array of property values
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
- ::kOfxStatErrValue
|
||||
|
||||
*/
|
||||
OfxStatus (*propSetDoubleN) (OfxPropertySetHandle properties, const char *property, int count, const double *value);
|
||||
|
||||
/** @brief Set multiple values of an int property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c count number of values we are setting in that property (ie: indicies 0..count-1)
|
||||
\arg \c value pointer to an array of property values
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
- ::kOfxStatErrValue
|
||||
|
||||
*/
|
||||
OfxStatus (*propSetIntN) (OfxPropertySetHandle properties, const char *property, int count, const int *value);
|
||||
|
||||
/** @brief Get a single value from a pointer property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c index refers to the index of a multi-dimensional property
|
||||
\arg \c value pointer the return location
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
*/
|
||||
OfxStatus (*propGetPointer)(OfxPropertySetHandle properties, const char *property, int index, void **value);
|
||||
|
||||
/** @brief Get a single value of a string property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c index refers to the index of a multi-dimensional property
|
||||
\arg \c value pointer the return location
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
*/
|
||||
OfxStatus (*propGetString) (OfxPropertySetHandle properties, const char *property, int index, char **value);
|
||||
|
||||
/** @brief Get a single value of a double property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c index refers to the index of a multi-dimensional property
|
||||
\arg \c value pointer the return location
|
||||
|
||||
See the note \ref ArchitectureStrings for how to deal with strings.
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
*/
|
||||
OfxStatus (*propGetDouble) (OfxPropertySetHandle properties, const char *property, int index, double *value);
|
||||
|
||||
/** @brief Get a single value of an int property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c index refers to the index of a multi-dimensional property
|
||||
\arg \c value pointer the return location
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
*/
|
||||
OfxStatus (*propGetInt) (OfxPropertySetHandle properties, const char *property, int index, int *value);
|
||||
|
||||
/** @brief Get multiple values of a pointer property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c count number of values we are getting of that property (ie: indicies 0..count-1)
|
||||
\arg \c value pointer to an array of where we will return the property values
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
*/
|
||||
OfxStatus (*propGetPointerN)(OfxPropertySetHandle properties, const char *property, int count, void **value);
|
||||
|
||||
/** @brief Get multiple values of a string property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c count number of values we are getting of that property (ie: indicies 0..count-1)
|
||||
\arg \c value pointer to an array of where we will return the property values
|
||||
|
||||
See the note \ref ArchitectureStrings for how to deal with strings.
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
*/
|
||||
OfxStatus (*propGetStringN) (OfxPropertySetHandle properties, const char *property, int count, char **value);
|
||||
|
||||
/** @brief Get multiple values of a double property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c count number of values we are getting of that property (ie: indicies 0..count-1)
|
||||
\arg \c value pointer to an array of where we will return the property values
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
*/
|
||||
OfxStatus (*propGetDoubleN) (OfxPropertySetHandle properties, const char *property, int count, double *value);
|
||||
|
||||
/** @brief Get multiple values of an int property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property
|
||||
\arg \c count number of values we are getting of that property (ie: indicies 0..count-1)
|
||||
\arg \c value pointer to an array of where we will return the property values
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
- ::kOfxStatErrBadIndex
|
||||
*/
|
||||
OfxStatus (*propGetIntN) (OfxPropertySetHandle properties, const char *property, int count, int *value);
|
||||
|
||||
/** @brief Resets all dimensions of a property to its default value
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property we are resetting
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
*/
|
||||
OfxStatus (*propReset) (OfxPropertySetHandle properties, const char *property);
|
||||
|
||||
/** @brief Gets the dimension of the property
|
||||
|
||||
\arg \c properties handle of the thing holding the property
|
||||
\arg \c property string labelling the property we are resetting
|
||||
\arg \c count pointer to an integer where the value is returned
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK
|
||||
- ::kOfxStatErrBadHandle
|
||||
- ::kOfxStatErrUnknown
|
||||
*/
|
||||
OfxStatus (*propGetDimension) (OfxPropertySetHandle properties, const char *property, int *count);
|
||||
} OfxPropertySuiteV1;
|
||||
|
||||
/**
|
||||
\addtogroup ErrorCodes
|
||||
*/
|
||||
/*@{*/
|
||||
|
||||
|
||||
/*@}*/
|
||||
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! bridge:oak 其余模块的 C ABI 调用——直接 Rust 调用(单库化,见
|
||||
//! `docs/zh/plans/riir/single-lib.md`)。
|
||||
//!
|
||||
//! 每个子模块(node/render/undo)调用对应 crate 的 `ffi` 导出(同名
|
||||
//! `#[no_mangle]` 符号仍从 dylib 导出供外部 C ABI 使用;内部调用绕过
|
||||
//! 它们)。句柄全部是共享的 [`crate::handle::CHandle`]。
|
||||
//!
|
||||
//! ## 双态实现(所有子模块统一)
|
||||
//!
|
||||
//! - 默认:直接调用 oaknode/oakrender/oakundo 的 ffi;
|
||||
//! - `--features test-stubs`:桥走库内状态桩(各子模块的
|
||||
//! [`node::stub`]/[`render::stub`]/[`undo::stub`],纯 Rust、无
|
||||
//! `#[no_mangle]`,与真实 crate 的导出不冲突)——像素路径可在无
|
||||
//! 真实 GL/GPU 的环境下跑通。两种形态的调用面完全一致。
|
||||
|
||||
pub mod node;
|
||||
pub mod render;
|
||||
pub mod undo;
|
||||
@@ -0,0 +1,425 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! oaknode C ABI 导入(include/node/node.h 中 param 桥用到的子集)。
|
||||
//!
|
||||
//! ## Value 布局冻结(M11 第 1 期)
|
||||
//!
|
||||
//! [`Value`] 即 include/node/node.h:93 的 `oaknode_value` POD,字段
|
||||
//! 逐字一致(type/num/den/f[4];`type` 取值见 [`node_value_type`])。
|
||||
//! 字符串族输入(k_file/k_text/k_font/k_str_combo,node.h:48-52)没有
|
||||
//! POD 表示——走 `*_input_string_*` 专用函数(本桥的
|
||||
//! [`set_input_string_undoable`]);`OAKNODE_VALUE_STRING` 的 POD 里
|
||||
//! 不携带字符串数据。`crate::ffi::OakNodeValue` 与此同布局(出口层
|
||||
//! 的镜像,两处独立声明避免模块环)。
|
||||
//!
|
||||
//! ## 双态实现(同 [`crate::bridge::render`])
|
||||
//!
|
||||
//! - 默认:直接 Rust 调用 oaknode 的 `ffi`(单库化,见
|
||||
//! `docs/zh/plans/riir/single-lib.md`);
|
||||
//! - `--features test-stubs`:库内状态桩([`stub`],纯 Rust、无
|
||||
//! `#[no_mangle]`,与真实 oaknode 的导出不冲突)——节点值、undo
|
||||
//! 命令全链路可在 cargo test 跑通。两种形态的调用面完全一致。
|
||||
|
||||
use std::ffi::c_char;
|
||||
|
||||
use crate::bridge::undo::CommandHandle;
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// oaknode 节点句柄(值型)。
|
||||
pub type NodeHandle = crate::handle::CHandle;
|
||||
|
||||
/// oaknode_value_type 的取值(node.h:74;与
|
||||
/// `crate::ffi::node_value_type` 逐值一致)。
|
||||
pub mod node_value_type {
|
||||
/// OAKNODE_VALUE_NONE。
|
||||
pub const NONE: i32 = 0;
|
||||
/// OAKNODE_VALUE_INT。
|
||||
pub const INT: i32 = 1;
|
||||
/// OAKNODE_VALUE_FLOAT。
|
||||
pub const FLOAT: i32 = 2;
|
||||
/// OAKNODE_VALUE_BOOL。
|
||||
pub const BOOL: i32 = 3;
|
||||
/// OAKNODE_VALUE_RATIONAL。
|
||||
pub const RATIONAL: i32 = 4;
|
||||
/// OAKNODE_VALUE_COLOR。
|
||||
pub const COLOR: i32 = 5;
|
||||
/// OAKNODE_VALUE_VEC2。
|
||||
pub const VEC2: i32 = 6;
|
||||
/// OAKNODE_VALUE_VEC3。
|
||||
pub const VEC3: i32 = 7;
|
||||
/// OAKNODE_VALUE_VEC4。
|
||||
pub const VEC4: i32 = 8;
|
||||
/// OAKNODE_VALUE_COMBO。
|
||||
pub const COMBO: i32 = 9;
|
||||
/// OAKNODE_VALUE_STRING。
|
||||
pub const STRING: i32 = 10;
|
||||
}
|
||||
|
||||
/// oaknode_value(include/node/node.h:93,字段逐字一致)。
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Value {
|
||||
/// 类型([`node_value_type`])。
|
||||
pub r#type: i32,
|
||||
/// INT/COMBO 值、BOOL 0/1、RATIONAL 分子。
|
||||
pub num: i64,
|
||||
/// RATIONAL 分母。
|
||||
pub den: i64,
|
||||
/// FLOAT f[0];VEC2/3/4 f[0..n-1];COLOR r,g,b,a。
|
||||
pub f: [f64; 4],
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// 类型化构造:整数 / choice 索引。
|
||||
pub const fn int(v: i64) -> Self {
|
||||
Self {
|
||||
r#type: node_value_type::INT,
|
||||
num: v,
|
||||
den: 0,
|
||||
f: [0.0; 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型化构造:浮点。
|
||||
pub const fn float(v: f64) -> Self {
|
||||
Self {
|
||||
r#type: node_value_type::FLOAT,
|
||||
num: 0,
|
||||
den: 0,
|
||||
f: [v, 0.0, 0.0, 0.0],
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型化构造:布尔。
|
||||
pub const fn bool_(v: bool) -> Self {
|
||||
Self {
|
||||
r#type: node_value_type::BOOL,
|
||||
num: v as i64,
|
||||
den: 0,
|
||||
f: [0.0; 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型化构造:choice(COMBO)。
|
||||
pub const fn combo(v: i64) -> Self {
|
||||
Self {
|
||||
r#type: node_value_type::COMBO,
|
||||
num: v,
|
||||
den: 0,
|
||||
f: [0.0; 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型化构造:颜色。
|
||||
pub const fn color(r: f64, g: f64, b: f64, a: f64) -> Self {
|
||||
Self {
|
||||
r#type: node_value_type::COLOR,
|
||||
num: 0,
|
||||
den: 0,
|
||||
f: [r, g, b, a],
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型化构造:vec2/3/4(长度按 f 数组尾部 0 判定)。
|
||||
pub const fn vec(v: &[f64]) -> Self {
|
||||
let t = match v.len() {
|
||||
2 => node_value_type::VEC2,
|
||||
3 => node_value_type::VEC3,
|
||||
_ => node_value_type::VEC4,
|
||||
};
|
||||
let mut f = [0.0; 4];
|
||||
let mut i = 0;
|
||||
while i < v.len() && i < 4 {
|
||||
f[i] = v[i];
|
||||
i += 1;
|
||||
}
|
||||
Self {
|
||||
r#type: t,
|
||||
num: 0,
|
||||
den: 0,
|
||||
f,
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型化构造:字符串族(POD 不携带数据;值经
|
||||
/// [`set_input_string_undoable`] 传递)。
|
||||
pub const fn string() -> Self {
|
||||
Self {
|
||||
r#type: node_value_type::STRING,
|
||||
num: 0,
|
||||
den: 0,
|
||||
f: [0.0; 4],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 桥调用面 ------------------------------------------------------------
|
||||
|
||||
/// 按身份取节点句柄(M9 身份注册表;`oaknode_node_from_identity`)。
|
||||
/// 身份未登记 / 符号缺失 → 空句柄。
|
||||
pub(crate) unsafe fn node_from_identity(id: usize) -> NodeHandle {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::node_from_identity_impl(id) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oaknode::ffi::node::oaknode_node_from_identity(id) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 写输入值的标准值并产出一条 undo 命令(`*out` 收到拥有型命令句柄;
|
||||
/// `oaknode_node_set_input_undoable`,node.h:327)。字符串族输入走
|
||||
/// [`set_input_string_undoable`]。失败(含符号缺失)返回负错误码。
|
||||
///
|
||||
/// # Safety
|
||||
/// `input`/`value`/`out` 必须指向有效内存;`node` 是有效句柄。
|
||||
pub(crate) unsafe fn set_input_undoable(
|
||||
node: NodeHandle,
|
||||
input: *const c_char,
|
||||
value: *const Value,
|
||||
out: *mut CommandHandle,
|
||||
) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::set_input_undoable_impl(node, input, value, out) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe {
|
||||
oaknode::ffi::node::oaknode_node_set_input_undoable(
|
||||
node,
|
||||
input,
|
||||
value as *const oaknode::value::OakNodeValue,
|
||||
out,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 写字符串族输入的标准值并产出一条 undo 命令
|
||||
/// (`oaknode_node_set_input_string_undoable`,node.h:346)。
|
||||
///
|
||||
/// # Safety
|
||||
/// `input`/`value`/`out` 必须指向有效内存;`node` 是有效句柄。
|
||||
pub(crate) unsafe fn set_input_string_undoable(
|
||||
node: NodeHandle,
|
||||
input: *const c_char,
|
||||
value: *const c_char,
|
||||
out: *mut CommandHandle,
|
||||
) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::set_input_string_undoable_impl(node, input, value, out) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe {
|
||||
oaknode::ffi::node::oaknode_node_set_input_string_undoable(
|
||||
node,
|
||||
input,
|
||||
value,
|
||||
out,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 测试桩(--features test-stubs)--------------------------------------
|
||||
|
||||
/// oaknode 测试桩:库内符号 + 节点值状态。
|
||||
///
|
||||
/// 节点句柄的 ctx 约定(桩内约定):`ctx = 身份 id`。undo 命令的
|
||||
/// 登记与 undo/redo 语义在 [`crate::bridge::undo::stub`](两边共享
|
||||
/// 同一张命令表)。
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub mod stub {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::CStr;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// 一个输入的标准值(数值走 [`Value`] POD;字符串族走 `string`)。
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct StubInput {
|
||||
/// 数值值(字符串族为 STRING 类型空 POD)。
|
||||
pub value: Value,
|
||||
/// 字符串值(仅字符串族输入)。
|
||||
pub string: Option<String>,
|
||||
}
|
||||
|
||||
/// 全部桩节点:身份 → 输入名 → 值。
|
||||
static NODES: LazyLock<Mutex<HashMap<usize, HashMap<String, StubInput>>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
fn lock() -> std::sync::MutexGuard<'static, HashMap<usize, HashMap<String, StubInput>>> {
|
||||
NODES.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// 重置全部桩节点(测试隔离)。
|
||||
pub fn reset() {
|
||||
lock().clear();
|
||||
}
|
||||
|
||||
/// 登记一个可被 [`super::node_from_identity`] 找到的节点。
|
||||
pub fn register_node(id: usize) {
|
||||
lock().entry(id).or_default();
|
||||
}
|
||||
|
||||
/// 直接设置输入值(测试前置)。
|
||||
pub fn set_input(id: usize, input: &str, value: Value) {
|
||||
lock().entry(id).or_default().insert(
|
||||
input.to_string(),
|
||||
StubInput {
|
||||
value,
|
||||
string: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 直接设置字符串输入值(测试前置)。
|
||||
pub fn set_input_string(id: usize, input: &str, value: &str) {
|
||||
lock().entry(id).or_default().insert(
|
||||
input.to_string(),
|
||||
StubInput {
|
||||
value: Value::string(),
|
||||
string: Some(value.to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 读输入当前值(断言用)。
|
||||
pub fn input(id: usize, input: &str) -> Option<StubInput> {
|
||||
lock().get(&id)?.get(input).cloned()
|
||||
}
|
||||
|
||||
/// 命令回写用的应用入口(redo/undo 落值;由
|
||||
/// [`crate::bridge::undo::stub`] 调用)。
|
||||
pub(crate) fn apply(node: usize, input: &str, value: Value, string: Option<String>) {
|
||||
let mut m = lock();
|
||||
if let Some(n) = m.get_mut(&node) {
|
||||
n.insert(
|
||||
input.to_string(),
|
||||
StubInput {
|
||||
value,
|
||||
string,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 读输入当前值(命令创建时取 prev)。
|
||||
pub(crate) fn current(node: usize, input: &str) -> StubInput {
|
||||
lock().get(&node).and_then(|n| n.get(input)).cloned().unwrap_or(StubInput {
|
||||
value: Value::default(),
|
||||
string: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) unsafe fn node_from_identity_impl(id: usize) -> NodeHandle {
|
||||
if lock().contains_key(&id) {
|
||||
CHandle {
|
||||
ctx: id as *mut std::ffi::c_void,
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: crate::handle::OAKPLUGIN_ABI_VERSION,
|
||||
}
|
||||
} else {
|
||||
CHandle::null()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) unsafe fn set_input_undoable_impl(
|
||||
node: NodeHandle,
|
||||
input: *const c_char,
|
||||
value: *const Value,
|
||||
out: *mut CommandHandle,
|
||||
) -> i32 {
|
||||
if node.is_null() || input.is_null() || value.is_null() || out.is_null() {
|
||||
return -30001;
|
||||
}
|
||||
let id = node.ctx as usize;
|
||||
let name = unsafe { CStr::from_ptr(input) }.to_str().map_err(|_| ()).unwrap_or_default();
|
||||
// 输入名必须已存在(node.h: 未知输入 id → OAKNODE_E_NOT_FOUND)。
|
||||
if !lock().get(&id).is_some_and(|n| n.contains_key(name)) {
|
||||
return -30004;
|
||||
}
|
||||
let next = unsafe { *value };
|
||||
let prev = current(id, &name).value;
|
||||
let h = crate::bridge::undo::stub::create_numeric(id, name.to_string(), prev, next);
|
||||
unsafe { *out = h };
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn set_input_string_undoable_impl(
|
||||
node: NodeHandle,
|
||||
input: *const c_char,
|
||||
value: *const c_char,
|
||||
out: *mut CommandHandle,
|
||||
) -> i32 {
|
||||
if node.is_null() || input.is_null() || value.is_null() || out.is_null() {
|
||||
return -30001;
|
||||
}
|
||||
let id = node.ctx as usize;
|
||||
let name = unsafe { CStr::from_ptr(input) }.to_str().map_err(|_| ()).unwrap_or_default();
|
||||
// 输入名必须已存在(node.h: 未知输入 id → OAKNODE_E_NOT_FOUND)。
|
||||
if !lock().get(&id).is_some_and(|n| n.contains_key(name)) {
|
||||
return -30004;
|
||||
}
|
||||
let next = unsafe { CStr::from_ptr(value) }.to_string_lossy().into_owned();
|
||||
let prev = current(id, &name).string.unwrap_or_default();
|
||||
let h = crate::bridge::undo::stub::create_string(
|
||||
id,
|
||||
name.to_string(),
|
||||
prev.clone(),
|
||||
next.clone(),
|
||||
);
|
||||
unsafe { *out = h };
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 默认模式单测(无桩;空/NULL 句柄经真实 crate 的可解释失败路径)----------------------
|
||||
|
||||
#[cfg(all(test, not(feature = "test-stubs")))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 空/NULL 句柄经真实 oaknode 容错:未登记身份 → 空句柄;空句柄
|
||||
/// 与空指针参数被拒(OAKNODE_E_INVALID)。
|
||||
#[test]
|
||||
fn empty_handle_error_paths() {
|
||||
let mut cmd = CommandHandle::null();
|
||||
unsafe {
|
||||
assert!(node_from_identity(0xDEAD).is_null(), "未登记身份 → 空句柄");
|
||||
assert_eq!(
|
||||
set_input_undoable(NodeHandle::null(), std::ptr::null(), std::ptr::null(), &mut cmd),
|
||||
-30001
|
||||
);
|
||||
assert_eq!(
|
||||
set_input_string_undoable(
|
||||
NodeHandle::null(),
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
&mut cmd
|
||||
),
|
||||
-30001
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,885 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! oakrender C ABI 导入(clip↔纹理桥用到的子集)。
|
||||
//!
|
||||
//! 声明以 `include/render/renderer.h` 为准(骨架的
|
||||
//! `oakrender_texture_get_frame`/`oakrender_texture_wrap_native`/
|
||||
//! `oakrender_texture_is_dummy` 与头文件不符,弃用;真实符号为
|
||||
//! `oakrender_display_texture_*` 与 `oakrender_codec_frame_*`)。
|
||||
//! `oakrender_display_texture_wrap_native` 是 C++ 专属符号
|
||||
//! (TexturePtr 引用),Rust 不可调用——输出纹理由 oakrender 侧
|
||||
//! 创建并经句柄传入,宿主只写其帧。
|
||||
//!
|
||||
//! ## 双态实现
|
||||
//!
|
||||
//! - 默认:直接 Rust 调用 oakrender 的 `ffi`(单库化,见
|
||||
//! `docs/zh/plans/riir/single-lib.md`);
|
||||
//! - `--features test-stubs`:库内状态桩 + 状态访问器([`stub`],纯
|
||||
//! Rust、无 `#[no_mangle]`,与真实 oakrender 的导出不冲突)——像素
|
||||
//! 路径在 cargo test 里全链路可跑。两种形态的调用面
|
||||
//! (`texture_get_frame` 等)完全一致。
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
/// `oakrender_video_params` POD — single-lib unification: aliases the
|
||||
/// oakrender crate's struct (identical layout; include/render/renderer.h:78).
|
||||
pub type VideoParams = oakrender::ffi::OakRenderVideoParams;
|
||||
|
||||
|
||||
/// olive::PixelFormat::Format 的 f32 值。
|
||||
pub const PIXEL_FORMAT_F32: i32 = 4;
|
||||
/// olive::PixelFormat::Format 的 u8 值。
|
||||
pub const PIXEL_FORMAT_U8: i32 = 0;
|
||||
|
||||
/// oakrender 渲染器句柄(`OakRenderRenderer`,值型)。
|
||||
pub type RendererHandle = crate::handle::CHandle;
|
||||
|
||||
/// oakrender 纹理句柄(`OakRenderTexture`,值型)。
|
||||
pub type TextureHandle = crate::handle::CHandle;
|
||||
|
||||
/// oakrender 帧句柄(`OakCodecFrame`,值型;布局与 CHandle 一致)。
|
||||
pub type FrameHandle = crate::handle::CHandle;
|
||||
|
||||
// ---- 桥调用面 ------------------------------------------------------------
|
||||
|
||||
/// 纹理的 CPU 帧(Texture::frame());`*out` 收到保留引用。
|
||||
pub(crate) unsafe fn texture_get_frame(texture: TextureHandle, out: *mut FrameHandle) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::texture_get_frame(texture, out) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_texture_get_frame(texture, out) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 纹理是否占位(dummy);符号缺失 → 1(视为占位)。
|
||||
pub(crate) unsafe fn texture_is_dummy(texture: TextureHandle) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::texture_is_dummy(texture) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_texture_is_dummy(texture) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 帧宽(空帧为 0)。
|
||||
pub(crate) unsafe fn frame_width(frame: FrameHandle) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::frame_width(frame) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_width(frame) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 帧高。
|
||||
pub(crate) unsafe fn frame_height(frame: FrameHandle) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::frame_height(frame) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_height(frame) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 借用的像素数据指针(最终 release 前有效)。
|
||||
pub(crate) unsafe fn frame_data(frame: FrameHandle) -> *mut c_void {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::frame_data(frame) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_data(frame) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 帧的视频参数。
|
||||
pub(crate) unsafe fn frame_get_params(frame: FrameHandle, out: *mut VideoParams) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::frame_get_params(frame, out) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_get_params(frame, out) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 按参数分配像素缓冲。
|
||||
pub(crate) unsafe fn frame_allocate(frame: FrameHandle) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::frame_allocate(frame) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_allocate(frame) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放一次帧引用并清空句柄(NULL/空句柄 no-op)。
|
||||
pub(crate) unsafe fn frame_free(frame: *mut FrameHandle) {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::frame_free(frame) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_free(frame) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 帧的行跨度(字节;空帧 0)。M11 第 2 期新增导入
|
||||
/// (`oakrender_codec_frame_linesize_bytes`,renderer.h:311):CPU
|
||||
/// 拷贝路径(fetch/store/驱动输出装配)用它兼容真实 oakrender 的
|
||||
/// 行填充,不再假设紧凑行布局。
|
||||
pub(crate) unsafe fn frame_linesize_bytes(frame: FrameHandle) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::frame_linesize_bytes(frame) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_linesize_bytes(frame) }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 渲染器族(GL 路径;M11 §4)--------------------------------------------
|
||||
|
||||
/// 按后端名创建渲染器(`oakrender_display_renderer_create_dynamic`,
|
||||
/// renderer.h:155)。空指针/空串 → 空句柄。
|
||||
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
|
||||
pub(crate) unsafe fn renderer_create_dynamic(backend: *const std::ffi::c_char) -> RendererHandle {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::renderer_create_dynamic(backend) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_create_dynamic(backend) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化渲染器(`oakrender_display_renderer_init`,renderer.h:175)。
|
||||
/// `gl_context` 为借用指针(NULL = 后端默认上下文路径)。
|
||||
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
|
||||
pub(crate) unsafe fn renderer_init(
|
||||
renderer: RendererHandle,
|
||||
gl_context: *mut std::ffi::c_void,
|
||||
) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::renderer_init(renderer, gl_context) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_init(renderer, gl_context) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 渲染器是否 OpenGL 后端(`oakrender_display_renderer_is_open_gl`,
|
||||
/// renderer.h:189)。
|
||||
pub(crate) unsafe fn renderer_is_open_gl(renderer: RendererHandle) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::renderer_is_open_gl(renderer) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_is_open_gl(renderer) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放一次渲染器引用并清空句柄(`oakrender_display_renderer_destroy`,
|
||||
/// renderer.h:183)。
|
||||
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
|
||||
pub(crate) unsafe fn renderer_destroy(renderer: *mut RendererHandle) {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::renderer_destroy(renderer) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_destroy(renderer) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 在渲染器上创建纹理(`oakrender_display_texture_create`,
|
||||
/// renderer.h:204)。`pixels` 可空(未初始化);`linesize` 为行跨度
|
||||
/// 字节数(0 = 紧凑行;pixels 为空时 0)。
|
||||
///
|
||||
/// 单位约定(M11 第 2 期):**字节**——以 renderer.h:206 的明文
|
||||
/// 契约为准(`Stride of pixels in bytes`)。C++ 调用点传像素行跨度,
|
||||
/// 由 oakrender 侧实现 C ABI 时换算;本 crate 侧一律传字节。
|
||||
pub(crate) unsafe fn texture_create(
|
||||
renderer: RendererHandle,
|
||||
params: *const VideoParams,
|
||||
pixels: *const std::ffi::c_void,
|
||||
linesize: i32,
|
||||
) -> TextureHandle {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::texture_create(renderer, params, pixels, linesize) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe {
|
||||
oakrender::ffi::renderer::oakrender_display_texture_create(
|
||||
renderer, params, pixels, linesize,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 纹理的原生 GL id(`oakrender_display_texture_id`,renderer.h:245;
|
||||
/// 空/占位/无 id 纹理为 0)。
|
||||
pub(crate) unsafe fn texture_id(texture: TextureHandle) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::texture_id(texture) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_texture_id(texture) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 纹理参数(`oakrender_display_texture_get_params`,renderer.h:233)。
|
||||
pub(crate) unsafe fn texture_get_params(texture: TextureHandle, out: *mut VideoParams) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::texture_get_params(texture, out) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_texture_get_params(texture, out) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 从纹理下载像素(`oakrender_display_texture_download`,
|
||||
/// renderer.h:228;`linesize` 行跨度字节数,0 = 紧凑行)。
|
||||
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
|
||||
pub(crate) unsafe fn texture_download(
|
||||
texture: TextureHandle,
|
||||
pixels: *mut std::ffi::c_void,
|
||||
linesize: i32,
|
||||
) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::texture_download(texture, pixels, linesize) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_texture_download(texture, pixels, linesize) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 再取一次引用(`oakrender_display_texture_retain`,renderer.h:215)。
|
||||
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
|
||||
pub(crate) unsafe fn texture_retain(texture: TextureHandle) -> TextureHandle {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::texture_retain(texture) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_texture_retain(texture) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放一次纹理引用并清空句柄(`oakrender_display_texture_free`,
|
||||
/// renderer.h:223)。
|
||||
pub(crate) unsafe fn texture_free(texture: *mut TextureHandle) {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::texture_free(texture) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_texture_free(texture) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传像素到纹理(`oakrender_display_texture_upload`,renderer.h:225;
|
||||
/// `linesize` 行跨度字节数,0 = 紧凑行)。
|
||||
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
|
||||
pub(crate) unsafe fn texture_upload(
|
||||
texture: TextureHandle,
|
||||
pixels: *const std::ffi::c_void,
|
||||
linesize: i32,
|
||||
) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::texture_upload(texture, pixels, linesize) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_texture_upload(texture, pixels, linesize) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 按 GL id 从渲染器下载像素(`oakrender_display_renderer_download_from_texture`,
|
||||
/// renderer.h:333;`linesize` 行跨度字节数,0 = 紧凑行)。
|
||||
#[allow(dead_code)] // 契约完整导入:GL 测试/后续路径按需使用(renderer.h 同签名)
|
||||
pub(crate) unsafe fn renderer_download_from_texture(
|
||||
renderer: RendererHandle,
|
||||
texture_id: i32,
|
||||
params: *const VideoParams,
|
||||
dst: *mut std::ffi::c_void,
|
||||
linesize: i32,
|
||||
) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::renderer_download_from_texture(renderer, texture_id, params, dst, linesize) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe {
|
||||
oakrender::ffi::renderer::oakrender_display_renderer_download_from_texture(
|
||||
renderer, texture_id, params, dst, linesize,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 测试桩(--features test-stubs)--------------------------------------
|
||||
|
||||
/// oakrender 测试桩:库内 no_mangle 符号 + 状态访问器。
|
||||
///
|
||||
/// 纹理/帧句柄的 ctx 约定(桩内约定,与真实句柄无冲突):
|
||||
/// - dst 纹理 ctx = 0xA1 → 输出帧 ctx = 0xB1;
|
||||
/// - src 纹理 ctx = 0xA2 → 输入帧 ctx = 0xB2。
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub mod stub {
|
||||
use super::*;
|
||||
use crate::handle::CHandle;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// 测试帧(数据 + 参数)。
|
||||
#[derive(Clone)]
|
||||
pub struct StubFrame {
|
||||
/// 视频参数。
|
||||
pub params: VideoParams,
|
||||
/// 像素缓冲(行优先)。
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl StubFrame {
|
||||
fn new(width: i32, height: i32, format: i32) -> Self {
|
||||
let len = width as usize * height as usize * 4 * bytes_per_pixel(format);
|
||||
Self {
|
||||
params: VideoParams {
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
..Default::default()
|
||||
},
|
||||
data: vec![0u8; len],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 桩 GL 纹理(渲染器上的 GPU 纹理的 CPU 镜像;GL 纹理同时
|
||||
/// 扮演 CPU 帧载体——与真实 olive Texture 包装 CPU 帧同构)。
|
||||
#[derive(Clone)]
|
||||
struct StubGlTexture {
|
||||
/// 原生 id(texture_id 的返回值)。
|
||||
id: i32,
|
||||
/// CPU 镜像帧(数据 + 参数)。
|
||||
frame: StubFrame,
|
||||
}
|
||||
|
||||
fn bytes_per_pixel(format: i32) -> usize {
|
||||
match format {
|
||||
PIXEL_FORMAT_F32 => 4,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// 按参数分配紧凑像素缓冲(宽×高×4 通道×每分量字节)。
|
||||
fn tight_len(params: &VideoParams) -> usize {
|
||||
params.width as usize * params.height as usize * 4 * bytes_per_pixel(params.format)
|
||||
}
|
||||
|
||||
struct StubState {
|
||||
dst: StubFrame,
|
||||
src: StubFrame,
|
||||
/// 标记为占位(dummy)的纹理 ctx(texture_is_dummy 用)。
|
||||
dummy: std::collections::HashSet<usize>,
|
||||
/// GL 渲染器是否可用(renderer_is_open_gl 的返回值)。
|
||||
gl_available: bool,
|
||||
/// GL 纹理注册表(ctx = GL_TEX_BASE + id)。
|
||||
gl_textures: Vec<StubGlTexture>,
|
||||
/// 下一个 GL 纹理 id(从 1 起)。
|
||||
next_gl_id: i32,
|
||||
}
|
||||
|
||||
static STATE: std::sync::LazyLock<Mutex<StubState>> = std::sync::LazyLock::new(|| {
|
||||
Mutex::new(StubState {
|
||||
dst: StubFrame::new(0, 0, 0),
|
||||
src: StubFrame::new(0, 0, 0),
|
||||
dummy: std::collections::HashSet::new(),
|
||||
gl_available: false,
|
||||
gl_textures: Vec::new(),
|
||||
next_gl_id: 1,
|
||||
})
|
||||
});
|
||||
|
||||
fn lock() -> std::sync::MutexGuard<'static, StubState> {
|
||||
STATE.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// 重置全部桩状态(测试隔离)。
|
||||
pub fn reset() {
|
||||
let mut s = lock();
|
||||
s.dst = StubFrame::new(0, 0, 0);
|
||||
s.src = StubFrame::new(0, 0, 0);
|
||||
s.dummy.clear();
|
||||
s.gl_available = false;
|
||||
s.gl_textures.clear();
|
||||
s.next_gl_id = 1;
|
||||
}
|
||||
|
||||
/// 把某纹理 ctx 标记为占位(dummy);`dummy=false` 取消标记。
|
||||
/// clip 桥把 dummy 输入视作空输入(NotFound)。
|
||||
pub fn set_dummy(ctx: usize, dummy: bool) {
|
||||
let mut s = lock();
|
||||
if dummy {
|
||||
s.dummy.insert(ctx);
|
||||
} else {
|
||||
s.dummy.remove(&ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// 配置输出帧(宽/高/格式;F32 = 全链路主路径)。
|
||||
pub fn setup_dst(width: i32, height: i32, format: i32) {
|
||||
lock().dst = StubFrame::new(width, height, format);
|
||||
}
|
||||
|
||||
/// 配置输入帧并填充像素。
|
||||
pub fn setup_src(width: i32, height: i32, format: i32, pixels: Vec<u8>) {
|
||||
let mut s = lock();
|
||||
s.src = StubFrame::new(width, height, format);
|
||||
s.src.data = pixels;
|
||||
}
|
||||
|
||||
/// 输出帧像素(断言用)。
|
||||
pub fn dst_pixels() -> Vec<u8> {
|
||||
lock().dst.data.clone()
|
||||
}
|
||||
|
||||
/// 输入帧像素(断言用)。
|
||||
pub fn src_pixels() -> Vec<u8> {
|
||||
lock().src.data.clone()
|
||||
}
|
||||
|
||||
/// 输出帧参数。
|
||||
pub fn dst_params() -> VideoParams {
|
||||
lock().dst.params
|
||||
}
|
||||
|
||||
/// GL 渲染器可用标记(renderer_is_open_gl 的桩返回值)。
|
||||
pub fn set_gl_available(available: bool) {
|
||||
lock().gl_available = available;
|
||||
}
|
||||
|
||||
/// 构造桩 GL 渲染器句柄(ctx = 0xD1)。
|
||||
pub fn make_gl_renderer() -> RendererHandle {
|
||||
magic_handle(GL_RENDERER)
|
||||
}
|
||||
|
||||
/// 已注册的 GL 纹理 id 列表(断言用)。
|
||||
pub fn gl_texture_ids() -> Vec<i32> {
|
||||
lock().gl_textures.iter().map(|t| t.id).collect()
|
||||
}
|
||||
|
||||
/// 某 GL 纹理 id 的像素(断言用;未知 id 返回空)。
|
||||
pub fn gl_texture_data(id: i32) -> Vec<u8> {
|
||||
lock()
|
||||
.gl_textures
|
||||
.iter()
|
||||
.find(|t| t.id == id)
|
||||
.map(|t| t.frame.data.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 某 GL 纹理 id 的参数(断言用)。
|
||||
pub fn gl_texture_params(id: i32) -> Option<VideoParams> {
|
||||
lock()
|
||||
.gl_textures
|
||||
.iter()
|
||||
.find(|t| t.id == id)
|
||||
.map(|t| t.frame.params)
|
||||
}
|
||||
|
||||
/// 直接构造一个 GL 纹理(GL 测试的目标纹理/输入纹理模拟;
|
||||
/// oakrender 侧创建纹理的 C ABI 等价物)。返回句柄(ctx =
|
||||
/// GL_TEX_BASE + id)。
|
||||
pub fn make_gl_texture(width: i32, height: i32, format: i32) -> TextureHandle {
|
||||
let mut s = lock();
|
||||
let id = s.next_gl_id;
|
||||
s.next_gl_id += 1;
|
||||
s.gl_textures.push(StubGlTexture {
|
||||
id,
|
||||
frame: StubFrame::new(width, height, format),
|
||||
});
|
||||
magic_handle(GL_TEX_BASE + id as usize)
|
||||
}
|
||||
|
||||
fn frame_of(state: &mut StubState, ctx: usize) -> &mut StubFrame {
|
||||
if ctx == SRC_FRAME {
|
||||
&mut state.src
|
||||
} else if let Some(id) = gl_id_of_ctx(ctx) {
|
||||
match state.gl_textures.iter_mut().find(|t| t.id == id) {
|
||||
Some(t) => &mut t.frame,
|
||||
None => &mut state.dst,
|
||||
}
|
||||
} else {
|
||||
&mut state.dst
|
||||
}
|
||||
}
|
||||
|
||||
fn magic_handle(ctx: usize) -> CHandle {
|
||||
CHandle {
|
||||
ctx: ctx as *mut c_void,
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
const DST_TEX: usize = 0xA1;
|
||||
const SRC_TEX: usize = 0xA2;
|
||||
const DST_FRAME: usize = 0xB1;
|
||||
const SRC_FRAME: usize = 0xB2;
|
||||
/// 桩 GL 渲染器 ctx。
|
||||
const GL_RENDERER: usize = 0xD1;
|
||||
/// 桩 GL 纹理 ctx 基址(ctx = GL_TEX_BASE + id)。
|
||||
const GL_TEX_BASE: usize = 0xC0;
|
||||
|
||||
/// GL 纹理 ctx → id。
|
||||
fn gl_id_of_ctx(ctx: usize) -> Option<i32> {
|
||||
if ctx > GL_TEX_BASE {
|
||||
Some((ctx - GL_TEX_BASE) as i32)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 行拷贝:`src`(行跨度 src_linesize 字节)→ `dst`(行跨度
|
||||
/// dst_linesize 字节),共 `rows` 行、每行 `row_bytes` 字节。
|
||||
fn copy_rows(
|
||||
src: &[u8],
|
||||
src_linesize: usize,
|
||||
dst: &mut [u8],
|
||||
dst_linesize: usize,
|
||||
row_bytes: usize,
|
||||
rows: usize,
|
||||
) {
|
||||
for y in 0..rows {
|
||||
let s = y * src_linesize;
|
||||
let d = y * dst_linesize;
|
||||
if row_bytes == src_linesize && src_linesize == dst_linesize {
|
||||
dst[d..d + row_bytes].copy_from_slice(&src[s..s + row_bytes]);
|
||||
} else {
|
||||
dst[d..d + row_bytes].copy_from_slice(&src[s..s + row_bytes.min(src.len().saturating_sub(s))]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_get_frame(texture: TextureHandle, out: *mut FrameHandle) -> i32 {
|
||||
if out.is_null() {
|
||||
return -1;
|
||||
}
|
||||
// GL 纹理:帧句柄即纹理自身 ctx(frame_of 按 ctx 反查镜像帧)。
|
||||
let frame_ctx = if texture.ctx as usize == SRC_TEX {
|
||||
SRC_FRAME
|
||||
} else if gl_id_of_ctx(texture.ctx as usize).is_some() {
|
||||
texture.ctx as usize
|
||||
} else {
|
||||
DST_FRAME
|
||||
};
|
||||
unsafe { *out = magic_handle(frame_ctx) };
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_is_dummy(texture: TextureHandle) -> i32 {
|
||||
lock().dummy.contains(&(texture.ctx as usize)) as i32
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_width(frame: FrameHandle) -> i32 {
|
||||
let mut s = lock();
|
||||
frame_of(&mut s, frame.ctx as usize).params.width
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_height(frame: FrameHandle) -> i32 {
|
||||
let mut s = lock();
|
||||
frame_of(&mut s, frame.ctx as usize).params.height
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_data(frame: FrameHandle) -> *mut c_void {
|
||||
let mut s = lock();
|
||||
let f = frame_of(&mut s, frame.ctx as usize);
|
||||
f.data.as_mut_ptr() as *mut c_void
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_get_params(frame: FrameHandle, out: *mut VideoParams) -> i32 {
|
||||
if out.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let mut s = lock();
|
||||
unsafe { *out = frame_of(&mut s, frame.ctx as usize).params };
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_allocate(frame: FrameHandle) -> i32 {
|
||||
let mut s = lock();
|
||||
let f = frame_of(&mut s, frame.ctx as usize);
|
||||
let len = f.params.width as usize
|
||||
* f.params.height as usize
|
||||
* 4
|
||||
* bytes_per_pixel(f.params.format);
|
||||
f.data.resize(len, 0);
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_free(frame: *mut FrameHandle) {
|
||||
if !frame.is_null() {
|
||||
unsafe { (*frame).ctx = std::ptr::null_mut() };
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) unsafe fn frame_linesize_bytes(frame: FrameHandle) -> i32 {
|
||||
let mut s = lock();
|
||||
let f = frame_of(&mut s, frame.ctx as usize);
|
||||
(f.params.width * 4 * bytes_per_pixel(f.params.format) as i32) as i32
|
||||
}
|
||||
|
||||
// ---- 渲染器族桩 ---------------------------------------------------------
|
||||
|
||||
pub(super) unsafe fn renderer_create_dynamic(backend: *const std::ffi::c_char) -> RendererHandle {
|
||||
if backend.is_null() {
|
||||
return RendererHandle::null();
|
||||
}
|
||||
let id = unsafe { std::ffi::CStr::from_ptr(backend) }.to_str().unwrap_or("");
|
||||
if id != "opengl" {
|
||||
return RendererHandle::null();
|
||||
}
|
||||
magic_handle(GL_RENDERER)
|
||||
}
|
||||
|
||||
pub(super) unsafe fn renderer_init(
|
||||
_renderer: RendererHandle,
|
||||
_gl_context: *mut std::ffi::c_void,
|
||||
) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn renderer_is_open_gl(renderer: RendererHandle) -> i32 {
|
||||
if renderer.ctx as usize != GL_RENDERER {
|
||||
return 0;
|
||||
}
|
||||
lock().gl_available as i32
|
||||
}
|
||||
|
||||
pub(super) unsafe fn renderer_destroy(renderer: *mut RendererHandle) {
|
||||
if !renderer.is_null() {
|
||||
unsafe { (*renderer).ctx = std::ptr::null_mut() };
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 纹理族桩(GL 纹理注册表)------------------------------------------
|
||||
|
||||
pub(super) unsafe fn texture_create(
|
||||
renderer: RendererHandle,
|
||||
params: *const VideoParams,
|
||||
pixels: *const std::ffi::c_void,
|
||||
linesize: i32,
|
||||
) -> TextureHandle {
|
||||
if renderer.ctx as usize != GL_RENDERER || params.is_null() {
|
||||
return TextureHandle::null();
|
||||
}
|
||||
let params = unsafe { *params };
|
||||
if params.width <= 0 || params.height <= 0 {
|
||||
return TextureHandle::null();
|
||||
}
|
||||
let mut s = lock();
|
||||
let id = s.next_gl_id;
|
||||
s.next_gl_id += 1;
|
||||
let mut data = vec![0u8; tight_len(¶ms)];
|
||||
if !pixels.is_null() {
|
||||
// linesize 字节/行(renderer.h 的 bytes 契约;0 → 紧凑行)。
|
||||
let bpp = bytes_per_pixel(params.format);
|
||||
let row_bytes = (params.width as usize) * 4 * bpp;
|
||||
let src_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
|
||||
let src = unsafe { std::slice::from_raw_parts(pixels as *const u8, src_linesize * params.height as usize) };
|
||||
copy_rows(src, src_linesize, &mut data, row_bytes, row_bytes.min(src_linesize), params.height as usize);
|
||||
}
|
||||
s.gl_textures.push(StubGlTexture {
|
||||
id,
|
||||
frame: StubFrame {
|
||||
params,
|
||||
data,
|
||||
},
|
||||
});
|
||||
magic_handle(GL_TEX_BASE + id as usize)
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_id(texture: TextureHandle) -> i32 {
|
||||
gl_id_of_ctx(texture.ctx as usize).unwrap_or(0)
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_get_params(texture: TextureHandle, out: *mut VideoParams) -> i32 {
|
||||
if out.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let mut s = lock();
|
||||
let Some(id) = gl_id_of_ctx(texture.ctx as usize) else {
|
||||
return -1;
|
||||
};
|
||||
let Some(t) = s.gl_textures.iter().find(|t| t.id == id) else {
|
||||
return -1;
|
||||
};
|
||||
unsafe { *out = t.frame.params };
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_download(
|
||||
texture: TextureHandle,
|
||||
pixels: *mut std::ffi::c_void,
|
||||
linesize: i32,
|
||||
) -> i32 {
|
||||
let mut s = lock();
|
||||
let Some(id) = gl_id_of_ctx(texture.ctx as usize) else {
|
||||
return -1;
|
||||
};
|
||||
let Some(t) = s.gl_textures.iter().find(|t| t.id == id) else {
|
||||
return -1;
|
||||
};
|
||||
if pixels.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let bpp = bytes_per_pixel(t.frame.params.format);
|
||||
let row_bytes = (t.frame.params.width as usize) * 4 * bpp;
|
||||
let dst_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
|
||||
let dst = unsafe { std::slice::from_raw_parts_mut(pixels as *mut u8, dst_linesize * t.frame.params.height as usize) };
|
||||
let src = t.frame.data.clone();
|
||||
copy_rows(&src, row_bytes, dst, dst_linesize, row_bytes, t.frame.params.height as usize);
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_upload(
|
||||
texture: TextureHandle,
|
||||
pixels: *const std::ffi::c_void,
|
||||
linesize: i32,
|
||||
) -> i32 {
|
||||
let mut s = lock();
|
||||
let Some(id) = gl_id_of_ctx(texture.ctx as usize) else {
|
||||
return -1;
|
||||
};
|
||||
let Some(t) = s.gl_textures.iter_mut().find(|t| t.id == id) else {
|
||||
return -1;
|
||||
};
|
||||
if pixels.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let bpp = bytes_per_pixel(t.frame.params.format);
|
||||
let row_bytes = (t.frame.params.width as usize) * 4 * bpp;
|
||||
let src_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
|
||||
let src = unsafe { std::slice::from_raw_parts(pixels as *const u8, src_linesize * t.frame.params.height as usize) };
|
||||
let mut data = vec![0u8; tight_len(&t.frame.params)];
|
||||
copy_rows(src, src_linesize, &mut data, row_bytes, row_bytes.min(src_linesize), t.frame.params.height as usize);
|
||||
t.frame.data = data;
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_retain(texture: TextureHandle) -> TextureHandle {
|
||||
texture
|
||||
}
|
||||
|
||||
pub(super) unsafe fn texture_free(texture: *mut TextureHandle) {
|
||||
if texture.is_null() {
|
||||
return;
|
||||
}
|
||||
let ctx = unsafe { (*texture).ctx as usize };
|
||||
if let Some(id) = gl_id_of_ctx(ctx) {
|
||||
let mut s = lock();
|
||||
s.gl_textures.retain(|t| t.id != id);
|
||||
}
|
||||
unsafe { (*texture).ctx = std::ptr::null_mut() };
|
||||
}
|
||||
|
||||
pub(super) unsafe fn renderer_download_from_texture(
|
||||
renderer: RendererHandle,
|
||||
texture_id: i32,
|
||||
params: *const VideoParams,
|
||||
dst: *mut std::ffi::c_void,
|
||||
linesize: i32,
|
||||
) -> i32 {
|
||||
if renderer.ctx as usize != GL_RENDERER || params.is_null() || dst.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let req = unsafe { *params };
|
||||
let s = lock();
|
||||
let Some(t) = s.gl_textures.iter().find(|t| t.id == texture_id) else {
|
||||
return -1;
|
||||
};
|
||||
let bpp = bytes_per_pixel(req.format);
|
||||
let row_bytes = (req.width as usize) * 4 * bpp;
|
||||
let dst_linesize = if linesize > 0 { linesize as usize } else { row_bytes };
|
||||
let out = unsafe { std::slice::from_raw_parts_mut(dst as *mut u8, dst_linesize * req.height as usize) };
|
||||
let src = t.frame.data.clone();
|
||||
copy_rows(&src, row_bytes, out, dst_linesize, row_bytes, req.height as usize);
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 默认模式单测(无桩;空/NULL 句柄经真实 crate 的可解释失败路径)----------------------
|
||||
|
||||
#[cfg(all(test, not(feature = "test-stubs")))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 空/NULL 句柄经真实 oakrender 全部容错不崩:返回
|
||||
/// `OAKRENDER_E_INVALID`(-70001)或空值。
|
||||
#[test]
|
||||
fn empty_handle_error_paths() {
|
||||
let mut frame = FrameHandle::null();
|
||||
let tex = TextureHandle::null();
|
||||
unsafe {
|
||||
assert_eq!(texture_get_frame(tex, &mut frame), -70001);
|
||||
// 空句柄不是占位纹理:真实 oakrender 返回 0。
|
||||
assert_eq!(texture_is_dummy(tex), 0);
|
||||
assert_eq!(frame_width(frame), 0);
|
||||
assert_eq!(frame_height(frame), 0);
|
||||
assert!(frame_data(frame).is_null());
|
||||
assert_eq!(frame_get_params(frame, &mut VideoParams::default()), -70001);
|
||||
assert_eq!(frame_allocate(frame), -70001);
|
||||
frame_free(&mut frame); // 空句柄 no-op
|
||||
frame_free(std::ptr::null_mut()); // NULL no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! oakundo C ABI 导入(参数回写打包用到的子集;include/undo/
|
||||
//! undocommand.h)。
|
||||
//!
|
||||
//! ## 语义(对照 C++ 的 oliveplugininstance.cpp `submit_undo_command`)
|
||||
//!
|
||||
//! 写回一律以"命令"为单位:数值/字符串 set 各产出一条命令
|
||||
//! (node 桥的 `*_undoable`),立即 `redo_now` 生效;编辑事务
|
||||
//! (paramEditBegin/End)内多条命令并入一条 multi
|
||||
//! ([`command_init_multi`] + [`command_multi_add_child`]),
|
||||
//! editEnd 时整体 `redo_now` 后释放。
|
||||
//!
|
||||
//! ## 双态实现(同 [`crate::bridge::render`])
|
||||
//!
|
||||
//! - 默认:直接 Rust 调用 oakundo 的 `ffi`(单库化,见
|
||||
//! `docs/zh/plans/riir/single-lib.md`);
|
||||
//! - `--features test-stubs`:库内状态桩([`stub`],纯 Rust、无
|
||||
//! `#[no_mangle]`,与真实 oakundo 的导出不冲突)——命令表 + undo/redo
|
||||
//! 语义在 cargo test 里全链路可跑。两种形态的调用面完全一致。
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// oakundo 命令句柄(值型)。
|
||||
pub type CommandHandle = crate::handle::CHandle;
|
||||
|
||||
// ---- 桥调用面 ------------------------------------------------------------
|
||||
|
||||
/// 创建 multi 命令(`oakundo_command_init_multi`,undocommand.h:85)。
|
||||
pub(crate) unsafe fn command_init_multi() -> CommandHandle {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::command_init_multi_impl() }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakundo::ffi::command::oakundo_command_init_multi() }
|
||||
}
|
||||
}
|
||||
|
||||
/// 直接 redo(不进栈的立即执行路径;`oakundo_command_redo_now`)。
|
||||
pub(crate) unsafe fn command_redo_now(command: CommandHandle) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::command_redo_now_impl(command) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 把子命令并入 multi(`oakundo_command_multi_add_child`)。
|
||||
pub(crate) unsafe fn command_multi_add_child(multi: CommandHandle, child: CommandHandle) -> i32 {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::command_multi_add_child_impl(multi, child) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放命令句柄(`oakundo_command_free`;NULL/空句柄 no-op)。
|
||||
pub(crate) unsafe fn command_free(command: *mut CommandHandle) {
|
||||
#[cfg(feature = "test-stubs")]
|
||||
{
|
||||
unsafe { stub::command_free_impl(command) }
|
||||
}
|
||||
#[cfg(not(feature = "test-stubs"))]
|
||||
{
|
||||
unsafe { oakundo::ffi::command::oakundo_command_free(command) }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 测试桩(--features test-stubs)--------------------------------------
|
||||
|
||||
/// oakundo 测试桩:命令表 + undo/redo 语义。
|
||||
///
|
||||
/// 命令句柄的 ctx 约定(桩内约定):`ctx = 命令 id`。记录在
|
||||
/// [`command_free`] 后仍保留(`freed` 标记),供测试检查命令捕获的
|
||||
/// prev/next 并驱动 undo/redo——真实 oakundo 的 free 会销毁命令,
|
||||
/// 这是桩的刻意简化(记录仅为测试保留)。
|
||||
#[cfg(feature = "test-stubs")]
|
||||
pub mod stub {
|
||||
use super::*;
|
||||
use crate::bridge::node;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
/// 一条命令的记录(redo 应用 next、undo 恢复 prev)。
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommandRecord {
|
||||
/// 命令 id(= 句柄 ctx)。
|
||||
pub id: usize,
|
||||
/// 目标节点身份。
|
||||
pub node: usize,
|
||||
/// 目标输入名。
|
||||
pub input: String,
|
||||
/// 数值回写的旧值。
|
||||
pub prev: node::Value,
|
||||
/// 数值回写的新值。
|
||||
pub next: node::Value,
|
||||
/// 字符串回写的旧值。
|
||||
pub prev_string: Option<String>,
|
||||
/// 字符串回写的新值。
|
||||
pub next_string: Option<String>,
|
||||
/// redo 是否已应用。
|
||||
pub applied: bool,
|
||||
/// 是否 multi 命令。
|
||||
pub is_multi: bool,
|
||||
/// multi 的子命令 id。
|
||||
pub children: Vec<usize>,
|
||||
/// free 是否已调用(记录保留给测试检查)。
|
||||
pub freed: bool,
|
||||
}
|
||||
|
||||
static COMMANDS: LazyLock<Mutex<HashMap<usize, CommandRecord>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
|
||||
|
||||
fn lock() -> std::sync::MutexGuard<'static, HashMap<usize, CommandRecord>> {
|
||||
COMMANDS.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
fn magic(id: usize) -> CommandHandle {
|
||||
CHandle {
|
||||
ctx: id as *mut std::ffi::c_void,
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: crate::handle::OAKPLUGIN_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_id() -> usize {
|
||||
NEXT_ID.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// 重置命令表(测试隔离)。
|
||||
pub fn reset() {
|
||||
lock().clear();
|
||||
}
|
||||
|
||||
/// 全部命令记录快照(含已 free 的;id 升序)。
|
||||
pub fn records() -> Vec<CommandRecord> {
|
||||
let mut v: Vec<CommandRecord> = lock().values().cloned().collect();
|
||||
v.sort_by_key(|r| r.id);
|
||||
v
|
||||
}
|
||||
|
||||
/// 最近一次创建的命令记录。
|
||||
pub fn last_command() -> Option<CommandRecord> {
|
||||
let m = lock();
|
||||
m.values().max_by_key(|r| r.id).cloned()
|
||||
}
|
||||
|
||||
/// 撤销一条命令(测试助手:验证命令捕获的 prev 正确)。multi 按
|
||||
/// 子命令逆序撤销;单命令仅在其已应用时恢复 prev。
|
||||
pub fn undo(id: usize) {
|
||||
let (is_multi, children) = lock()
|
||||
.get(&id)
|
||||
.map(|r| (r.is_multi, r.children.clone()))
|
||||
.unwrap_or((false, Vec::new()));
|
||||
if is_multi {
|
||||
for c in children.iter().rev() {
|
||||
undo(*c);
|
||||
}
|
||||
if let Some(r) = lock().get_mut(&id) {
|
||||
r.applied = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut m = lock();
|
||||
let Some(r) = m.get_mut(&id) else { return };
|
||||
if r.applied {
|
||||
node::stub::apply(r.node, &r.input, r.prev, r.prev_string.clone());
|
||||
r.applied = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 重做一条命令(测试助手)。redo 幂等(已应用则 no-op)。
|
||||
pub fn redo(id: usize) {
|
||||
let (is_multi, children) = lock()
|
||||
.get(&id)
|
||||
.map(|r| (r.is_multi, r.children.clone()))
|
||||
.unwrap_or((false, Vec::new()));
|
||||
if is_multi {
|
||||
for c in &children {
|
||||
redo(*c);
|
||||
}
|
||||
if let Some(r) = lock().get_mut(&id) {
|
||||
r.applied = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut m = lock();
|
||||
let Some(r) = m.get_mut(&id) else { return };
|
||||
if !r.applied {
|
||||
node::stub::apply(r.node, &r.input, r.next, r.next_string.clone());
|
||||
r.applied = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// 登记一条数值写回命令(node 桥调用)。
|
||||
pub(crate) fn create_numeric(
|
||||
node_id: usize,
|
||||
input: String,
|
||||
prev: node::Value,
|
||||
next: node::Value,
|
||||
) -> CommandHandle {
|
||||
let id = next_id();
|
||||
lock().insert(
|
||||
id,
|
||||
CommandRecord {
|
||||
id,
|
||||
node: node_id,
|
||||
input,
|
||||
prev,
|
||||
next,
|
||||
prev_string: None,
|
||||
next_string: None,
|
||||
applied: false,
|
||||
is_multi: false,
|
||||
children: Vec::new(),
|
||||
freed: false,
|
||||
},
|
||||
);
|
||||
magic(id)
|
||||
}
|
||||
|
||||
/// 登记一条字符串写回命令(node 桥调用)。
|
||||
pub(crate) fn create_string(
|
||||
node_id: usize,
|
||||
input: String,
|
||||
prev: String,
|
||||
next: String,
|
||||
) -> CommandHandle {
|
||||
let id = next_id();
|
||||
lock().insert(
|
||||
id,
|
||||
CommandRecord {
|
||||
id,
|
||||
node: node_id,
|
||||
input,
|
||||
prev: node::Value::string(),
|
||||
next: node::Value::string(),
|
||||
prev_string: Some(prev),
|
||||
next_string: Some(next),
|
||||
applied: false,
|
||||
is_multi: false,
|
||||
children: Vec::new(),
|
||||
freed: false,
|
||||
},
|
||||
);
|
||||
magic(id)
|
||||
}
|
||||
|
||||
pub(super) unsafe fn command_init_multi_impl() -> CommandHandle {
|
||||
let id = next_id();
|
||||
lock().insert(
|
||||
id,
|
||||
CommandRecord {
|
||||
id,
|
||||
node: 0,
|
||||
input: String::new(),
|
||||
prev: node::Value::default(),
|
||||
next: node::Value::default(),
|
||||
prev_string: None,
|
||||
next_string: None,
|
||||
applied: false,
|
||||
is_multi: true,
|
||||
children: Vec::new(),
|
||||
freed: false,
|
||||
},
|
||||
);
|
||||
magic(id)
|
||||
}
|
||||
|
||||
pub(super) unsafe fn command_redo_now_impl(command: CommandHandle) -> i32 {
|
||||
if command.is_null() {
|
||||
return -40001;
|
||||
}
|
||||
// 先取 multi 的子命令列表,再逐条递归(避免持锁递归)。
|
||||
let (is_multi, children) = lock()
|
||||
.get(&(command.ctx as usize))
|
||||
.map(|r| (r.is_multi, r.children.clone()))
|
||||
.unwrap_or((false, Vec::new()));
|
||||
if is_multi {
|
||||
for c in &children {
|
||||
let h = magic(*c);
|
||||
unsafe { command_redo_now_impl(h) };
|
||||
}
|
||||
if let Some(r) = lock().get_mut(&(command.ctx as usize)) {
|
||||
r.applied = true;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
let mut m = lock();
|
||||
let Some(r) = m.get_mut(&(command.ctx as usize)) else {
|
||||
return -40004;
|
||||
};
|
||||
if !r.applied {
|
||||
node::stub::apply(r.node, &r.input, r.next, r.next_string.clone());
|
||||
r.applied = true;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn command_multi_add_child_impl(multi: CommandHandle, child: CommandHandle) -> i32 {
|
||||
if multi.is_null() || child.is_null() {
|
||||
return -40001;
|
||||
}
|
||||
let mut m = lock();
|
||||
let Some(r) = m.get_mut(&(multi.ctx as usize)) else {
|
||||
return -40004;
|
||||
};
|
||||
if !r.is_multi {
|
||||
return -40002;
|
||||
}
|
||||
r.children.push(child.ctx as usize);
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) unsafe fn command_free_impl(command: *mut CommandHandle) {
|
||||
if command.is_null() {
|
||||
return;
|
||||
}
|
||||
let h = unsafe { &mut *command };
|
||||
if h.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Some(r) = lock().get_mut(&(h.ctx as usize)) {
|
||||
// 记录保留给测试检查(undo/redo 仍可按 id 驱动)。
|
||||
r.freed = true;
|
||||
}
|
||||
h.ctx = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 默认模式单测(无桩;空/NULL 句柄经真实 crate 的可解释失败路径)----------------------
|
||||
|
||||
#[cfg(all(test, not(feature = "test-stubs")))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 空/NULL 句柄经真实 oakundo 全部容错不崩:`init_multi` 产生
|
||||
/// 真实命令,空句柄操作返回 `OAKUNDO_E_INVALID`(-20001)。
|
||||
#[test]
|
||||
fn empty_handle_error_paths() {
|
||||
unsafe {
|
||||
let mut m = command_init_multi();
|
||||
assert!(!m.is_null(), "init_multi 应产生真实命令句柄");
|
||||
assert_eq!(command_redo_now(CommandHandle::null()), -20001);
|
||||
assert_eq!(
|
||||
command_multi_add_child(CommandHandle::null(), CommandHandle::null()),
|
||||
-20001
|
||||
);
|
||||
command_free(&mut m); // 空句柄 no-op
|
||||
command_free(std::ptr::null_mut()); // NULL no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,226 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! 引用计数句柄脚手架。
|
||||
//!
|
||||
//! 对应 C 侧布局(`include/plugin/instance.h`,与 oak 全项目约定一致):
|
||||
//!
|
||||
//! ```c
|
||||
//! typedef struct OakPluginInstance {
|
||||
//! void *ctx;
|
||||
//! void (*addref)(void *ctx);
|
||||
//! void (*release)(void *ctx);
|
||||
//! uint32_t abi_version;
|
||||
//! } OakPluginInstance;
|
||||
//! ```
|
||||
//!
|
||||
//! 句柄按值传;`ctx` 指向本 crate 堆上的 [`RefBox<T>`]。`addref`/
|
||||
//! `release` 函数指针永远指向本 crate 的代码(所有权不出 DLL)。
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
|
||||
use crate::error::OAKPLUGIN_E_FAILED;
|
||||
|
||||
/// 当前 ABI 版本,写进每个句柄的 `abi_version` 字段。
|
||||
pub const OAKPLUGIN_ABI_VERSION: u32 = 1;
|
||||
|
||||
/// 句柄背后的堆盒子。`owns == false` 的盒子(借用包装)在计数归零时
|
||||
/// 只释放盒子本身,不销毁内含对象。
|
||||
pub struct RefBox<T: ?Sized> {
|
||||
/// 引用计数(原子;release 可在任意线程发生)。
|
||||
pub refs: AtomicU32,
|
||||
/// 内含对象。
|
||||
pub value: T,
|
||||
}
|
||||
|
||||
/// C 句柄的 Rust 镜像。`#[repr(C)]`,与 C 头文件布局一致。
|
||||
///
|
||||
/// 生命周期:`*_init`/`*_create` 返回计数 1 的拥有型句柄;
|
||||
/// The shared ABI value-handle type (single-lib unification, see
|
||||
/// `docs/zh/plans/riir/single-lib.md`): one canonical
|
||||
/// `{ctx, addref, release, abi_version}` type in `oakcore-rs`, re-exported
|
||||
/// here so the crate's `ffi.rs` signatures and handle scaffolding stay
|
||||
/// source-compatible. `Send + Sync` come from the shared type.
|
||||
pub use oakcore_rs::handle::CHandle;
|
||||
|
||||
/// addref 的实现:原子 +1。拥有型与借用型共用——借用型只延长盒子
|
||||
/// 的寿命,不延长被借用对象。
|
||||
unsafe extern "C" fn refbox_addref<T: Any + Send>(ctx: *mut std::ffi::c_void) {
|
||||
unsafe {
|
||||
let rb = ctx as *const RefBox<T>;
|
||||
// 调用方保证句柄在借用期内有效(ctx 非空且未被释放)。
|
||||
(*rb).refs.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// release 的实现(拥有型):原子 -1,归零时回收盒子并销毁内含对象。
|
||||
unsafe extern "C" fn refbox_release_owned<T: Any + Send>(ctx: *mut std::ffi::c_void) {
|
||||
unsafe {
|
||||
let rb = ctx as *mut RefBox<T>;
|
||||
// AcqRel:归零这一侧要能看见最后一次引用前的全部写(含对象
|
||||
// 析构所需的内部状态)。
|
||||
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
|
||||
drop(Box::from_raw(rb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// release 的实现(借用型,[`make_borrowed`] 的产物):归零时只回收
|
||||
/// 盒子内存,把内含对象原样忘掉——其所有权仍在借用方手里。
|
||||
unsafe extern "C" fn refbox_release_borrowed<T: Any + Send>(ctx: *mut std::ffi::c_void) {
|
||||
unsafe {
|
||||
let rb = ctx as *mut RefBox<T>;
|
||||
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
|
||||
// 部分 move:把 value 移出临时 Box,Box 析构只释放分配;
|
||||
// value 用 forget 放弃析构(double-free 防线)。
|
||||
std::mem::forget((Box::from_raw(rb)).value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 为 `T` 制作拥有型句柄(计数 1)。分配失败返回空句柄并销毁对象。
|
||||
///
|
||||
/// 注:Rust 默认分配失败(OOM)直接 abort,不会走到"返回空句柄"
|
||||
/// 路径;此处语义保留给未来接入自定义分配器的场景。
|
||||
pub fn make_owned<T: Any + Send>(value: T) -> CHandle {
|
||||
let rb = Box::into_raw(Box::new(RefBox {
|
||||
refs: AtomicU32::new(1),
|
||||
value,
|
||||
}));
|
||||
CHandle {
|
||||
ctx: rb as *mut std::ffi::c_void,
|
||||
addref: Some(refbox_addref::<T>),
|
||||
release: Some(refbox_release_owned::<T>),
|
||||
abi_version: OAKPLUGIN_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// 为已有对象制作借用句柄(计数归零只释放盒子)。`ptr` 必须在本
|
||||
/// 句柄被释放前保持有效。
|
||||
///
|
||||
/// 语义:按位拷贝("借用拷贝",如纹理句柄的快照);被借用对象
|
||||
/// 的析构完全由调用方负责,盒子从不碰它。拷贝即快照——借出后
|
||||
/// 修改 `*ptr` 不会反映到句柄内。
|
||||
///
|
||||
/// # Safety
|
||||
/// 调用方保证 `ptr` 的生命周期覆盖所有派生句柄,且其值在借用期内
|
||||
/// 不被 move/析构。
|
||||
pub unsafe fn make_borrowed<T: Any + Send>(ptr: *mut T) -> CHandle {
|
||||
if ptr.is_null() {
|
||||
return CHandle::null();
|
||||
}
|
||||
let rb = Box::into_raw(Box::new(RefBox {
|
||||
refs: AtomicU32::new(1),
|
||||
value: unsafe { std::ptr::read(ptr) },
|
||||
}));
|
||||
CHandle {
|
||||
ctx: rb as *mut std::ffi::c_void,
|
||||
addref: Some(refbox_addref::<T>),
|
||||
release: Some(refbox_release_borrowed::<T>),
|
||||
abi_version: OAKPLUGIN_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// 取回盒子内对象的不可变引用;空句柄返回 `None`。
|
||||
///
|
||||
/// # Safety
|
||||
/// 调用方必须保证 `T` 与创建句柄时的类型一致。
|
||||
pub unsafe fn get<T: Any>(h: &CHandle) -> Option<&T> {
|
||||
if h.is_null() {
|
||||
return None;
|
||||
}
|
||||
unsafe { Some(&(*(h.ctx as *const RefBox<T>)).value) }
|
||||
}
|
||||
|
||||
/// FFI 兜底:捕获 panic,把 `Result<i32>` 映射为对外错误码
|
||||
/// ([`crate::error`])。所有返回 i32 的导出函数必须经它。
|
||||
///
|
||||
/// panic 路径返回 `OAKPLUGIN_E_FAILED`;panic 详情暂不落日志
|
||||
/// (message 桥接入后补 TODO)。
|
||||
pub fn guard<F>(f: F) -> i32
|
||||
where
|
||||
F: FnOnce() -> crate::error::Result<()>,
|
||||
{
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(Ok(())) => crate::error::OAKPLUGIN_OK,
|
||||
Ok(Err(e)) => e.code(),
|
||||
Err(_) => OAKPLUGIN_E_FAILED,
|
||||
}
|
||||
}
|
||||
|
||||
/// 指针/句柄返回值版本的 [`guard`]:panic 或 Err 时返回空句柄
|
||||
/// (指针类返回 NULL)。
|
||||
pub fn guard_handle<F>(f: F) -> CHandle
|
||||
where
|
||||
F: FnOnce() -> crate::error::Result<CHandle>,
|
||||
{
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(Ok(h)) => h,
|
||||
Ok(Err(_)) | Err(_) => CHandle::null(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 无返回值版本:panic 被吞并记录日志(经 bridge 的日志回调)。
|
||||
pub fn guard_void<F>(f: F)
|
||||
where
|
||||
F: FnOnce(),
|
||||
{
|
||||
let _ = catch_unwind(AssertUnwindSafe(f));
|
||||
}
|
||||
|
||||
/// 句柄身份注册表:`usize` 身份 ↔ 弱引用。供 param↔node 等需要
|
||||
/// "按身份找回对象"的桥使用(替代 M9 C++ 版的
|
||||
/// `oaknode_node_identity()` 注册表)。
|
||||
pub struct Registry<T: Any + Send> {
|
||||
map: Mutex<HashMap<usize, Weak<RefBox<T>>>>,
|
||||
}
|
||||
|
||||
impl<T: Any + Send> Registry<T> {
|
||||
/// 空注册表。
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
map: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 登记对象,返回其身份(地址语义,进程内唯一)。
|
||||
pub fn register(&self, obj: &Arc<RefBox<T>>) -> usize {
|
||||
// Arc 分配地址即身份:同一 RefBox 恒稳定,进程内唯一。
|
||||
let id = Arc::as_ptr(obj) as *const () as usize;
|
||||
lock(&self.map).insert(id, Arc::downgrade(obj));
|
||||
id
|
||||
}
|
||||
|
||||
/// 按身份取对象;对象已销毁或身份未知返回 `None`。
|
||||
pub fn lookup(&self, id: usize) -> Option<Arc<RefBox<T>>> {
|
||||
lock(&self.map).get(&id).and_then(|w| w.upgrade())
|
||||
}
|
||||
|
||||
/// 摘除身份(对象销毁路径调用)。未知身份是 no-op。
|
||||
pub fn unregister(&self, id: usize) {
|
||||
lock(&self.map).remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// 取锁。毒锁(本 crate 代码持锁时 panic)时接管内部状态继续——
|
||||
/// 一次 panic 不级联成后续所有 FFI 调用失败。
|
||||
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
m.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,454 @@
|
||||
// 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 默认构建(真实 oakrender 链接):NULL 输出纹理在触达
|
||||
/// oakrender 之前被插件层校验拒绝(E_INVALID),不崩溃。真实纹理
|
||||
/// 路径需要 GL/GPU,属集成测试范畴。
|
||||
#[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 = CHandle::null();
|
||||
assert_eq!(unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) }, -90001);
|
||||
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,249 @@
|
||||
// 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;
|
||||
}
|
||||
// NULL 输出纹理在触达 oakrender 前被插件层校验拒绝(E_INVALID)。
|
||||
let dst = CHandle::null();
|
||||
let r = unsafe {
|
||||
oakplugin_instance_render_job(h, dst, 0.0, 0, 0, std::ptr::null(), CHandle::null(), std::ptr::null(), 0, std::ptr::null(), 0, CHandle::null())
|
||||
};
|
||||
assert_eq!(r, -90001, "NULL 输出纹理 → E_INVALID(不崩溃)");
|
||||
unsafe { oakplugin_instance_free(&mut h) };
|
||||
});
|
||||
}
|
||||
|
||||
/// 驱动错误路径:非 F32 输出 → 明确失败;取消标记 → 明确失败。
|
||||
#[cfg(feature = "test-stubs")]
|
||||
#[test]
|
||||
fn render_job_error_paths() {
|
||||
use oakplugin::bridge::render::stub;
|
||||
use oakplugin::ffi::{oakplugin_instance_cancel, oakplugin_instance_render_job};
|
||||
common::with_host(|| {
|
||||
stub::reset();
|
||||
let mut h = scan_and_create(TEST_PLUGIN_ID);
|
||||
if h.is_null() {
|
||||
return;
|
||||
}
|
||||
// 非 F32 输出(U8)→ read_dst 拒绝。
|
||||
stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_U8);
|
||||
let dst = fake_texture(0xA1);
|
||||
let r = unsafe {
|
||||
oakplugin_instance_render_job(h, dst, 0.0, 0, 0, std::ptr::null(), CHandle::null(), std::ptr::null(), 0, std::ptr::null(), 0, CHandle::null())
|
||||
};
|
||||
assert_eq!(r, -90003, "非 F32 输出 → E_FAILED");
|
||||
|
||||
// 取消标记 → 驱动入口短路。
|
||||
stub::setup_dst(4, 4, oakplugin::bridge::render::PIXEL_FORMAT_F32);
|
||||
assert_eq!(unsafe { oakplugin_instance_cancel(h) }, OK);
|
||||
let r = unsafe {
|
||||
oakplugin_instance_render_job(h, dst, 0.0, 0, 0, std::ptr::null(), CHandle::null(), std::ptr::null(), 0, std::ptr::null(), 0, CHandle::null())
|
||||
};
|
||||
assert_eq!(r, -90003, "已取消 → E_FAILED");
|
||||
unsafe { oakplugin_instance_free(&mut h) };
|
||||
});
|
||||
}
|
||||
@@ -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