feat(oakplugin): GL render bridge, color picker, push-button action, worker progress, Interact host
- gl_bridge: macOS CGL offscreen context (process-wide singleton, serialized GlGuard), real GL output textures/FBOs, glReadPixels readback with vertical flip and format conversion; use_opengl now really engages for OpenGLRenderSupported plugins (verified with real GL rendering: C smoke 11/11, unit tests, GL e2e). - OfxColor: color params get a swatch button plus a real picker popup (RGBA sliders, live preview, hex input, undoable commit) replacing the four spinboxes. - Push buttons route kOfxActionInstanceChanged (UserEdited) per the OFX contract; test plugin asserts the callback. - Worker-side plugin progress flows to the main-process progress dialog over the NDJSON control channel, with cancel propagation. - OFX Interact host: NewInteract/Describe lifecycle, Draw/Pen/Key/Idle action surface with proper in-args, DrawSuite v1 host implementation sharing the gl_bridge context; interact test plugin verifies the event stream and real GL drawing.
This commit is contained in:
@@ -43,6 +43,7 @@
|
||||
//! shm slots and publish `frame_ready` / `frame_failed` (protocol v2).
|
||||
|
||||
use std::io::{self, BufRead, Write};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -55,9 +56,10 @@ use oakrender::ticket::{AudioTicketParams, MontageClip, VideoTicketParams};
|
||||
|
||||
use crate::ipc::{
|
||||
error_message, write_message, AudioTicketSpec, BatchTicketSpec, FrameSlotPool, FrameSlotMeta,
|
||||
HandshakeMsg, LoadGraphMsg, RenderAudioBatchMsg, RenderBatchMsg, RenderFrameMsg,
|
||||
SharedMemoryRegion, ShmMode, TYPE_CANCEL, TYPE_HANDSHAKE, TYPE_LOAD_GRAPH, TYPE_RENDER_AUDIO_BATCH,
|
||||
TYPE_RENDER_BATCH, TYPE_RENDER_FRAME, TYPE_SHUTDOWN, SLOT_FORMAT_AUDIO_F32, SLOT_FORMAT_BGRA8,
|
||||
HandshakeMsg, LoadGraphMsg, PluginProgressMsg, RenderAudioBatchMsg, RenderBatchMsg,
|
||||
RenderFrameMsg, SharedMemoryRegion, ShmMode, TYPE_CANCEL, TYPE_HANDSHAKE, TYPE_LOAD_GRAPH,
|
||||
TYPE_PLUGIN_CANCEL, TYPE_RENDER_AUDIO_BATCH, TYPE_RENDER_BATCH, TYPE_RENDER_FRAME,
|
||||
TYPE_SHUTDOWN, SLOT_FORMAT_AUDIO_F32, SLOT_FORMAT_BGRA8,
|
||||
};
|
||||
use crate::{log_error, PROTOCOL_VERSION};
|
||||
|
||||
@@ -77,6 +79,101 @@ struct LoadedGraph {
|
||||
project_copy: u64,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Worker-side plugin-progress forwarding
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// OFX plugin rendering happens in this process (crash isolation), so the
|
||||
// main process's inline progress reporter is not in effect here. The
|
||||
// worker installs its own progress reporter factory (see
|
||||
// [`install_worker_progress_factory`]) whose reporters push `plugin_progress`
|
||||
// NDJSON events into [`WORKER_PROGRESS_EVENTS`]; the main loop drains the
|
||||
// buffer after each control message ([`flush_worker_progress`]). Plugin
|
||||
// renders are synchronous on the loop thread, so the buffer only ever
|
||||
// mutates there — a plain `Mutex` suffices.
|
||||
//
|
||||
// Cancel: the main process broadcasts `plugin_cancel` (the progress
|
||||
// dialog's Cancel button); the worker sets [`WORKER_PLUGIN_CANCEL`] and
|
||||
// every live reporter answers false (the plugin aborts at its next
|
||||
// progressUpdate). Mirrors the main-process reporter factory: a fresh
|
||||
// render (progressStart) resets the sticky flag. Because the worker
|
||||
// processes control messages between batches, an in-flight frame
|
||||
// completes before the cancel is observed (batch granularity); the main
|
||||
// process stops the render loop separately (export cancel atom / preview
|
||||
// window invalidation).
|
||||
|
||||
/// The worker's sticky plugin-cancel flag (set by the `plugin_cancel`
|
||||
/// control message, read by every live progress reporter).
|
||||
static WORKER_PLUGIN_CANCEL: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Buffered `plugin_progress` events awaiting the next flush.
|
||||
static WORKER_PROGRESS_EVENTS: Mutex<Vec<Value>> = Mutex::new(Vec::new());
|
||||
|
||||
/// Queue one `plugin_progress` NDJSON event for the main loop to flush.
|
||||
fn push_worker_progress(fraction: f64, label: &str, message: &str) {
|
||||
let event = PluginProgressMsg {
|
||||
label: label.to_string(),
|
||||
message: message.to_string(),
|
||||
fraction,
|
||||
}
|
||||
.to_json();
|
||||
WORKER_PROGRESS_EVENTS
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.push(event);
|
||||
}
|
||||
|
||||
/// Write every buffered progress event to `out` (called by the main loop
|
||||
/// after each control message; `out` is the NDJSON stdout writer).
|
||||
fn flush_worker_progress(out: &mut impl Write) {
|
||||
let events = std::mem::take(
|
||||
&mut *WORKER_PROGRESS_EVENTS
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()),
|
||||
);
|
||||
for event in events {
|
||||
if write_message(out, &event).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = out.flush();
|
||||
}
|
||||
|
||||
/// The worker-side `UiProgressReporter`: forwards (label, message,
|
||||
/// fraction) to the main process and honours the sticky cancel flag.
|
||||
struct WorkerProgressReporter {
|
||||
label: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl oakplugin::progress::UiProgressReporter for WorkerProgressReporter {
|
||||
fn update(&mut self, progress: f64) -> bool {
|
||||
push_worker_progress(progress, &self.label, &self.message);
|
||||
!WORKER_PLUGIN_CANCEL.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn end(&mut self) {
|
||||
// progressEnd: forward completion (fraction 1.0) so the app closes
|
||||
// the progress dialog without waiting for a 1.0 update.
|
||||
push_worker_progress(1.0, &self.label, &self.message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Install the worker-side progress reporter factory. Called from
|
||||
/// [`WorkerSession::initialize_runtime`]; mirrors the main-process factory
|
||||
/// (a fresh progressStart resets the sticky cancel flag).
|
||||
fn install_worker_progress_factory() {
|
||||
oakplugin::progress::set_reporter_factory(Some(Arc::new(|label, message| {
|
||||
// A fresh render begins: reset the sticky cancel flag.
|
||||
WORKER_PLUGIN_CANCEL.store(false, Ordering::Relaxed);
|
||||
push_worker_progress(0.0, label, message);
|
||||
Box::new(WorkerProgressReporter {
|
||||
label: label.to_string(),
|
||||
message: message.to_string(),
|
||||
})
|
||||
})));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Renderer (backend selection)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -248,6 +345,11 @@ impl WorkerSession {
|
||||
"runtime: registered {} OFX plugin node type(s)",
|
||||
registered.len()
|
||||
));
|
||||
// Worker-side plugin progress forwarding (see the module docs): the
|
||||
// plugin progress suite runs in this process, so progress events
|
||||
// must cross the IPC boundary to reach the app's progress dialog.
|
||||
log_error("runtime: installing worker plugin-progress reporter factory");
|
||||
install_worker_progress_factory();
|
||||
log_error(
|
||||
"runtime: config / frame manager / disk manager / project \
|
||||
serializer have no Rust backing in the worker binary; skipped",
|
||||
@@ -292,6 +394,13 @@ impl WorkerSession {
|
||||
// cancel: the worker does synchronous single-frame work
|
||||
// (nothing in flight), so a cancel produces no response.
|
||||
TYPE_CANCEL => None,
|
||||
// plugin_cancel: the user cancelled the plugin render; sticky
|
||||
// until the next progressStart resets it (main-process
|
||||
// semantics).
|
||||
TYPE_PLUGIN_CANCEL => {
|
||||
WORKER_PLUGIN_CANCEL.store(true, Ordering::Relaxed);
|
||||
None
|
||||
}
|
||||
TYPE_SHUTDOWN => {
|
||||
self.shutdown_requested = true;
|
||||
None
|
||||
@@ -1123,6 +1232,8 @@ pub fn worker_main(backend: &str) -> i32 {
|
||||
exit_code = 1;
|
||||
break;
|
||||
}
|
||||
// Plugin progress events buffered during the batch go out now.
|
||||
flush_worker_progress(&mut out);
|
||||
continue;
|
||||
}
|
||||
// M15 S3: render_audio_batch streams the same way (audio range
|
||||
@@ -1133,6 +1244,7 @@ pub fn worker_main(backend: &str) -> i32 {
|
||||
exit_code = 1;
|
||||
break;
|
||||
}
|
||||
flush_worker_progress(&mut out);
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
@@ -1149,6 +1261,8 @@ pub fn worker_main(backend: &str) -> i32 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Progress events buffered while serving the control message.
|
||||
flush_worker_progress(&mut out);
|
||||
}
|
||||
exit_code
|
||||
}
|
||||
|
||||
+100
-16
@@ -19,6 +19,8 @@ src/
|
||||
property.rs memory.rs image_effect.rs param.rs
|
||||
message.rs progress.rs timeline.rs multithread.rs
|
||||
gl_render.rs OfxImageEffectOpenGLRenderSuiteV1(M11 §4 新增)
|
||||
draw.rs OfxDrawSuiteV1(OFX 1.5,interact 绘制;真实 GL 渲染)
|
||||
interact.rs OfxInteractSuiteV1 + Interact 宿主对象(interact 宿主)
|
||||
host.rs Host 单例:bundle 扫描、插件缓存、action 分发
|
||||
descriptor.rs EffectDescriptor/ClipDescriptor(describe 的产物)
|
||||
instance.rs Instance:action 调用面(render/协商/RoD/RoI/isIdentity/
|
||||
@@ -92,22 +94,40 @@ src/
|
||||
OpenGLTextureTarget/PixelDepth/Components/PreMultiplication/
|
||||
RenderScale/PixelAspectRatio/Bounds/ROD/RowBytes/Field/
|
||||
UniqueIdentifier);存活表持有 Box<PropertySet>(句柄地址稳定)。
|
||||
Output clip 的渲染目标绑定由调用方契约保证(等价 C++
|
||||
`attach_output_texture`);clipFreeTexture 对 Output 不删纹理。
|
||||
**OpenGLTextureIndex 是真实 GL 纹理名**(GL 模式;0 = CPU 回退)。
|
||||
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 输出回读,渲染结果留在附着纹理上。
|
||||
渲染结果留在 FBO 附着的输出 GL 纹理上,render 返回后宿主
|
||||
**glReadPixels 回读**装帧(方案 B,见下)。
|
||||
- **GL 上下文规则**(ofxGPURender.h "OpenGL Current Context"):宿主
|
||||
只在 Render/Begin/EndSequenceRender/Attach/Detach 期间要求上下文
|
||||
current——本实现的约定是调用方(oakrender PluginJob 路径)在
|
||||
进入 render_job 前把渲染器上下文置为 current 并附着输出纹理
|
||||
(文档见 include/plugin/instance.h 的新增声明)。
|
||||
current——本实现的约定是 render 驱动一次 `gl_bridge::acquire` 整个
|
||||
GL render action(上下文 current 到 guard drop),suite 回调期间
|
||||
恒 current。
|
||||
- 格式协商:插件描述符声明 kOfxImageEffectPropOpenGLRenderSupported
|
||||
("false"/"true"/"needed")与 kOfxOpenGLPropPixelDepth(可选位深
|
||||
列表);宿主 `pick_gl_pixel_depth` 按管线 F32 约束选型(声明列表
|
||||
不含 Float → GL 模式不可行,回退 CPU)。use_opengl 决策在
|
||||
render_driver。
|
||||
render_driver(插件 GL 声明 + 深度协商 + 桥可用)。
|
||||
|
||||
### GL 互操作桥(方案 B 落地,`gl_bridge.rs`)
|
||||
|
||||
- **macOS 真实实现**(Core OpenGL / CGL,`OpenGL.framework` 直链,
|
||||
无新 crate):进程级共享离屏上下文(3.2 core profile、offline
|
||||
renderer 允许),`acquire()` 全局串行 + 置当前线程 current。为每次
|
||||
GL 渲染建输出 GL 纹理(尺寸 = 目标帧;F32 → RGBA32F、U8 → RGBA8)
|
||||
+ FBO 挂载;插件直接画进 FBO;render 返回后 `glReadPixels` 回读
|
||||
(垂直翻转、U8 归一化 F32),与 CPU 路径输出格式一致。
|
||||
- 输入 clip 在 GL 模式经桥上传为真实 GL 纹理(clipFreeTexture 删除)。
|
||||
- GL 失败回退 CPU(对齐现有失败语义)。Linux(EGL)/Windows(WGL)
|
||||
预留 cfg stub。
|
||||
- 验证:`gl_bridge.rs` 单元测试(clear 已知颜色 → 回读逐像素断言,
|
||||
`OAK_GPU_TESTS` 门)+ `tests/gl_render_test.rs` 端到端(GL 测试插件
|
||||
真实走 GL 路径、输出已知颜色)。
|
||||
|
||||
### ofxColour(OFX 1.4)
|
||||
|
||||
@@ -150,6 +170,63 @@ src/
|
||||
(输入 clip 纹理)两个 POD 随附。GL 契约(上下文 current + 输出
|
||||
附着)见头文件文档注释。
|
||||
|
||||
## OFX Interact 宿主(interact + Draw suite,M11 §5)
|
||||
|
||||
插件自定义 UI 的宿主侧:Interact 实例在主进程创建(UI 事件宿主),与
|
||||
worker 进程里的渲染实例并存(OFX 允许同一插件多实例)。
|
||||
|
||||
- **`suites/interact.rs`**:
|
||||
- `Instance::new_interact`(任务契约):取插件声明的 overlay interact
|
||||
入口(kOfxImageEffectPluginPropOverlayInteractV2 优先、V1 次之;未
|
||||
声明则插件 main entry)→ 建 `Interact`(属性表 + tagged INTERACT
|
||||
handle)→ 发 `kOfxActionNewInteract`。插件返回 OK → Some;返回
|
||||
ReplyDefault 且未声明 overlay 入口(无 interact)→ None;错误 → None。
|
||||
- 生命周期:`describe`(kOfxActionDescribe == kOfxActionDescribeInteract)
|
||||
→ `create_instance`(kOfxActionCreateInstance)→ 事件 → `destroy`
|
||||
(kOfxActionDestroyInstance);**与效果实例同生命周期**——实例销毁
|
||||
(`Instance::notify_destroy`)时先连带销毁 interact。
|
||||
- 调用面(宿主→插件,返回值如实透传):`draw(viewport, pixel_scale,
|
||||
time, background)`、`pen_motion(pen_pos, pen_down)`、`pen_down`、
|
||||
`pen_up`、`key_down(key_sym, key_string)`、`key_up`、`idle`(任务契约
|
||||
的 kOfxInteractActionIdle,属宿主扩展)、`gain_focus`/`lose_focus`。
|
||||
pen 坐标为视口像素,inArgs 另带 canonical 位置(viewport/像素比)、
|
||||
viewport 位置、压力(两态笔映射 0/1);key 带 kOfxPropKeySym
|
||||
(ofxKeySyms.h 关键码)+ kOfxPropKeyString。返回 kOfxStatOK = 插件
|
||||
已处理(宿主不再把事件给其他对象),ReplyDefault = 未处理。
|
||||
- `OfxInteractSuiteV1`(ofxInteract.h:534):interactSwapBuffers /
|
||||
interactRedraw 把请求记入 `swap_requested`/`redraw_requested`
|
||||
(app 侧轮询面);interactGetPropertySet 返回 interact 属性集句柄。
|
||||
- interact 属性集走现有 property suite:PixelScale / ViewportSize /
|
||||
BackgroundImage / SuggestedColour / SlaveToParam / BackgroundColour /
|
||||
BitDepth / HasAlpha(ofxInteract.h 的 PropertiesInteract;ViewportSize
|
||||
取 OFX 1.3 名 "OfxInteractPropViewport"、BackgroundImage 与 Idle 为
|
||||
任务契约扩展)。
|
||||
- **`suites/draw.rs`**(`OfxDrawSuiteV1`,OFX 1.5,vendored ofxDrawSuite.h):
|
||||
getColour / setColour / setLineWidth / setLineStipple / draw / drawText。
|
||||
状态(colour/lineWidth/stipple)保存在 `DrawContext`(每次 draw action
|
||||
创建、经存活表注册,draw 返回摘除——draw 外调用 → kOfxStatFailed)。
|
||||
GL 绘制是**真实渲染**:gl_bridge 的 CGL 上下文是 3.2 core(无固定管
|
||||
线),draw 用最小着色器 + VAO/VBO 按正交投影画线/矩形/多边形/椭圆,
|
||||
非不透明色按 "over" 合成。drawText 无字体光栅化器 → 如实返回
|
||||
kOfxStatErrUnsupported。非 macOS 无 GL 桥 → kOfxStatFailed。
|
||||
- **GL 上下文模型**:宿主在调 draw 前经 `gl_bridge::acquire` 保持
|
||||
current 整个 action(与 WG1 渲染路径同模型);`acquire` 支持同线程
|
||||
可重入(测试"一次 acquire 覆盖 FBO 装配 → draw → 回读")。
|
||||
- vendored 头新增:`ofxDrawSuite.h`、`ofxKeySyms.h`(kOfxPropKeySym/
|
||||
kOfxPropKeyString + kOfxKey_*)、`ofxPixels.h`(OfxRGBAColourF)——
|
||||
官方 openfx(BSD-3-Clause),与既有 vendored 头同源。
|
||||
- 验证:`tests/interact_test.rs`(生命周期 + 事件参数逐条断言插件侧
|
||||
marker 记录;Escape key_down 的 ReplyDefault 透传;实例销毁连带
|
||||
destroy)+ GL draw 端到端(`OAK_GPU_TESTS` 门:插件 glClear 暗背景 +
|
||||
Draw suite setColour(0.9,0.1,0.2,1) + draw(Rectangle) → 回读断言矩形
|
||||
颜色与背景)。
|
||||
|
||||
app 接线(WG3b)公共 API:`Instance::new_interact` / `Instance::interact` /
|
||||
`Instance::describe_interact`、`Interact::{describe, create_instance, draw,
|
||||
pen_motion, pen_down, pen_up, key_down, key_up, idle, gain_focus, lose_focus,
|
||||
destroy, props, swap_requested, redraw_requested}`、`host::KEY_*` 关键码、
|
||||
`fetch_suite("OfxInteractSuite"/"OfxDrawSuite", 1)`。
|
||||
|
||||
## 与 M11 §3.5 验收的对照(第 1+2 期现状)
|
||||
|
||||
| 验收项 | 状态 |
|
||||
@@ -243,12 +320,15 @@ crates/,不动 src/(app)与 gpui/。
|
||||
`oakrender::eval::set_plugin_executor`(依赖反转),duplicator
|
||||
装进 `oaknode::nodes::plugin::set_plugin_duplicator`。
|
||||
- `set_project_extent(w,h)`:normalised 坐标默认值换算基准。
|
||||
- **`gl_bridge.rs`(新增,spike 文档)**:`texture_id` 桩的 GL 互
|
||||
操作评估——方案 A(wgpu-hal GL 互操作)在 macOS 不可行(Metal
|
||||
后端无 GL 命名空间);方案 B(独立离屏 GL 上下文 + 回读 +
|
||||
wgpu upload)技术可行但暂缓(无真实 GL 插件可验证 + 需新 GL 依
|
||||
赖 + 每帧同步回读 stall)。`texture_id` 保持恒 0,GL 插件经 CPU
|
||||
render action 正确出帧。
|
||||
- **`gl_bridge.rs`(方案 B 落地)**:`texture_id` 桩的 GL 互操作评估
|
||||
结论——方案 A(wgpu-hal GL 互操作)在 macOS 不可行(Metal 后端无
|
||||
GL 命名空间);**方案 B(独立离屏 GL 上下文 + 回读)已落地**:
|
||||
macOS CGL 离屏上下文(进程级共享、`acquire()` 串行 + current)、
|
||||
输出 GL 纹理/FBO 挂载、glReadPixels 回读装帧(垂直翻转、格式转
|
||||
换)、输入 clip 真实 GL 纹理上传。use_opengl 决策与 GL suite 的
|
||||
OpenGLTextureIndex 接通真实 GL 名。Linux(EGL)/Windows(WGL)
|
||||
预留 cfg stub。验证:单元测试(clear → 回读断言)+ 端到端
|
||||
(GL 测试插件真实渲染)经 `OAK_GPU_TESTS` 门。
|
||||
- **`progress.rs`**:新增 `UiProgressReporter` trait +
|
||||
`ReporterFactory` + `set_reporter_factory`(app 注入点)。render
|
||||
未装 C 回调时装静默报告器,progressStart 携 (label,message) 经
|
||||
@@ -297,9 +377,10 @@ cargo tarpaulin --out stdout --features test-stubs # 覆盖率门槛
|
||||
(`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 编译为共享库,
|
||||
- 最小测试插件(cbits/oak_test_plugin.c,四个入口:
|
||||
org.oak.test-plugin / org.oak.test-plugin.gl /
|
||||
org.oak.test-plugin.identity / org.oak.test-plugin.interact)由
|
||||
build.rs 编译为共享库,
|
||||
`common::test_plugin_dir` 运行时装配成 bundle;不可用时相关用例
|
||||
skip;
|
||||
- 宿主单例无锁:触碰宿主面的用例经 `common::with_host` 串行化。
|
||||
@@ -336,6 +417,9 @@ TDD:测试声明与实现声明同步冻结(tests/,函数体 `todo!()`)
|
||||
(偏好采纳、交叉引用解析、输出写回;ACEScg 工作空间)。
|
||||
- `gl_render_test.rs` — GL suite 往返/错误路径/像素深度协商矩阵 +
|
||||
GL render 路径端到端(无 GPU 优雅跳过策略见上)。
|
||||
- `interact_test.rs` — Interact 宿主:生命周期 + 事件调用面(插件侧
|
||||
marker 逐条断言)、Escape 状态透传、实例销毁连带、draw 真实 GL 渲染
|
||||
断言(`OAK_GPU_TESTS` 门)。
|
||||
- `render_driver_test.rs` — render_job CPU 路径(序列括号、多输入、
|
||||
参数覆盖、isIdentity 透传像素断言、无桩降级)。
|
||||
- `bridge_test.rs` — node/render/undo 三桥(`--features test-stubs`
|
||||
|
||||
@@ -46,15 +46,22 @@ fn build_test_plugin() {
|
||||
} else {
|
||||
("-shared", "so")
|
||||
};
|
||||
let mut args = vec![
|
||||
"-fPIC".to_string(),
|
||||
"-Iofx".to_string(),
|
||||
"cbits/oak_test_plugin.c".to_string(),
|
||||
link_flag.to_string(),
|
||||
"-o".to_string(),
|
||||
format!("{out}/oak_test_plugin.{ext}"),
|
||||
];
|
||||
// GL 端到端测试:插件 GL 变体在 macOS 上真实绘制(glClear),需要
|
||||
// OpenGL.framework。
|
||||
if cfg!(target_os = "macos") {
|
||||
args.push("-framework".into());
|
||||
args.push("OpenGL".into());
|
||||
}
|
||||
let status = Command::new(&cc)
|
||||
.args([
|
||||
"-fPIC",
|
||||
"-Iofx",
|
||||
"cbits/oak_test_plugin.c",
|
||||
link_flag,
|
||||
"-o",
|
||||
&format!("{out}/oak_test_plugin.{ext}"),
|
||||
])
|
||||
.args(&args)
|
||||
.status()
|
||||
.expect("编译测试插件失败");
|
||||
assert!(status.success(), "测试插件编译失败");
|
||||
|
||||
@@ -32,17 +32,28 @@
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#include "ofxCore.h"
|
||||
#include "ofxColour.h"
|
||||
#include "ofxDrawSuite.h"
|
||||
#include "ofxGPURender.h"
|
||||
#include "ofxImageEffect.h"
|
||||
#include "ofxInteract.h"
|
||||
#include "ofxKeySyms.h"
|
||||
#include "ofxMessage.h"
|
||||
#include "ofxParam.h"
|
||||
#include "ofxProgress.h"
|
||||
#include "ofxProperty.h"
|
||||
|
||||
/* GL 端到端测试的真实绘制需要 GL 命令(仅 macOS 有真实 CGL 桥;
|
||||
* Linux/Windows 的 GL 桥是 stub,绘制代码随 __APPLE__ 排除)。 */
|
||||
#ifdef __APPLE__
|
||||
#include <OpenGL/gl.h>
|
||||
#endif
|
||||
|
||||
/* kOfxImageEffectGLFormatRGBA 在 vendored ofxOpenGLRender.h 是 stub
|
||||
* 未收录(OpenFX 1.4 规范名),按规范定义。kOfxImageEffectPropIsIdentity
|
||||
* 同理(vendored ofxImageEffect.h 只文档化了该属性,未给宏)。 */
|
||||
@@ -56,9 +67,48 @@ static const OfxParameterSuiteV1 *g_paramSuite = NULL;
|
||||
static const OfxMessageSuiteV1 *g_messageSuite = NULL;
|
||||
static const OfxProgressSuiteV1 *g_progressSuite = NULL;
|
||||
static const OfxImageEffectOpenGLRenderSuiteV1 *g_glSuite = NULL;
|
||||
static const OfxDrawSuiteV1 *g_drawSuite = NULL;
|
||||
static const OfxInteractSuiteV1 *g_interactSuite = NULL;
|
||||
|
||||
/* 宿主扩展(任务契约;vendored ofxInteract.h 未收录的 action 名与属性
|
||||
* 名)。kOfxInteractPropViewportSize 取 OFX 1.3 的规范名
|
||||
* "OfxInteractPropViewport"(vendored 1.5 头已删);BackgroundImage 与
|
||||
* NewInteract/Idle 是本宿主扩展。 */
|
||||
#ifndef kOfxActionNewInteract
|
||||
#define kOfxActionNewInteract "OfxActionNewInteract"
|
||||
#endif
|
||||
#ifndef kOfxInteractActionIdle
|
||||
#define kOfxInteractActionIdle "OfxInteractActionIdle"
|
||||
#endif
|
||||
#ifndef kOfxInteractPropViewportSize
|
||||
#define kOfxInteractPropViewportSize "OfxInteractPropViewport"
|
||||
#endif
|
||||
#ifndef kOfxInteractPropBackgroundImage
|
||||
#define kOfxInteractPropBackgroundImage "OfxInteractPropBackgroundImage"
|
||||
#endif
|
||||
|
||||
/* push button 的 instanceChanged 调用计数(宿主按下按钮后应路由
|
||||
* kOfxActionInstanceChanged/UserEdited 到这里)。 */
|
||||
static int g_button_instance_changed = 0;
|
||||
|
||||
/* ---------- suite 便捷封装 ---------- */
|
||||
|
||||
/* 记录一次 push button 的 instanceChanged:计数自增,并在
|
||||
* OAK_TEST_PLUGIN_INSTANCECHANGED_MARKER 指向的文件追加一行(宿主
|
||||
* 测试断言用;未设环境变量时静默)。 */
|
||||
static void record_button_instance_changed(void)
|
||||
{
|
||||
g_button_instance_changed++;
|
||||
const char *marker = getenv("OAK_TEST_PLUGIN_INSTANCECHANGED_MARKER");
|
||||
if (!marker)
|
||||
return;
|
||||
FILE *f = fopen(marker, "a");
|
||||
if (!f)
|
||||
return;
|
||||
fprintf(f, "button instanceChanged count=%d\n", g_button_instance_changed);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static OfxStatus propSetString(OfxPropertySetHandle h, const char *name, int index, const char *v)
|
||||
{
|
||||
return g_propSuite->propSetString(h, name, index, v);
|
||||
@@ -69,6 +119,11 @@ static OfxStatus propSetStringN(OfxPropertySetHandle h, const char *name, int co
|
||||
return g_propSuite->propSetStringN(h, name, count, v);
|
||||
}
|
||||
|
||||
static OfxStatus propSetPointer(OfxPropertySetHandle h, const char *name, int index, void *v)
|
||||
{
|
||||
return g_propSuite->propSetPointer(h, name, index, v);
|
||||
}
|
||||
|
||||
static OfxStatus propSetDoubleN(OfxPropertySetHandle h, const char *name, int count, const double *v)
|
||||
{
|
||||
return g_propSuite->propSetDoubleN(h, name, count, v);
|
||||
@@ -117,6 +172,8 @@ static void setHost(OfxHost *host)
|
||||
g_progressSuite = (const OfxProgressSuiteV1 *)host->fetchSuite(host->host, kOfxProgressSuite, 1);
|
||||
g_glSuite = (const OfxImageEffectOpenGLRenderSuiteV1 *)host->fetchSuite(
|
||||
host->host, kOfxOpenGLRenderSuite, 1);
|
||||
g_drawSuite = (const OfxDrawSuiteV1 *)host->fetchSuite(host->host, kOfxDrawSuite, 1);
|
||||
g_interactSuite = (const OfxInteractSuiteV1 *)host->fetchSuite(host->host, kOfxInteractSuite, 1);
|
||||
}
|
||||
|
||||
/* ---------- action 实现 ---------- */
|
||||
@@ -179,6 +236,13 @@ static OfxStatus actionDescribe(const void *handle, int is_gl)
|
||||
return st;
|
||||
propSetString(paramProps, kOfxPropLabel, 0, "Label");
|
||||
|
||||
/* push button:无值,按下经 instanceChanged (UserEdited) 通知
|
||||
* 插件(ofxCore.h kOfxActionInstanceChanged)。 */
|
||||
st = g_paramSuite->paramDefine((OfxParamSetHandle)handle, kOfxParamTypePushButton, "button", ¶mProps);
|
||||
if (st != kOfxStatOK)
|
||||
return st;
|
||||
propSetString(paramProps, kOfxPropLabel, 0, "Button");
|
||||
|
||||
/* clip:Source + Output。 */
|
||||
OfxPropertySetHandle clipProps = NULL;
|
||||
st = g_imageEffectSuite->clipDefine((OfxImageEffectHandle)handle, "Source", &clipProps);
|
||||
@@ -341,6 +405,17 @@ static OfxStatus actionGLDetached(OfxImageEffectHandle handle)
|
||||
|
||||
/* GL render:经 OpenGL suite 取 Source 与 Output 纹理并上报索引
|
||||
* (宿主侧测试经 message 捕获断言)。GL 未使能时回退 CPU render。 */
|
||||
static void draw_gl_test_pattern(void)
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
/* 把当前绑定帧缓冲(宿主为输出帧挂的 FBO)清成已知颜色。 */
|
||||
glClearColor(0.1f, 0.2f, 0.3f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
#else
|
||||
(void)0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static OfxStatus actionRenderGL(OfxImageEffectHandle inst, OfxPropertySetHandle inArgs)
|
||||
{
|
||||
int gl_enabled = 0;
|
||||
@@ -391,6 +466,11 @@ static OfxStatus actionRenderGL(OfxImageEffectHandle inst, OfxPropertySetHandle
|
||||
if (st != kOfxStatOK)
|
||||
return st;
|
||||
|
||||
/* 真实绘制:把宿主已绑定的输出 FBO 清成已知颜色
|
||||
* (0.1, 0.2, 0.3, 1.0)——宿主 glReadPixels 回读后应得到该颜色,
|
||||
* 这是"插件真实走 GL 路径且输出正确"的端到端证据。 */
|
||||
draw_gl_test_pattern();
|
||||
|
||||
return kOfxStatOK;
|
||||
}
|
||||
|
||||
@@ -432,6 +512,17 @@ static OfxStatus mainEntry(const char *action, const void *handle, OfxPropertySe
|
||||
if (strcmp(action, kOfxImageEffectActionRender) == 0) {
|
||||
return actionRender((OfxImageEffectHandle)handle, inArgs);
|
||||
}
|
||||
if (strcmp(action, kOfxActionInstanceChanged) == 0) {
|
||||
/* 宿主按下 push button:kOfxPropName == "button"、
|
||||
* kOfxPropChangeReason == kOfxChangeUserEdited。只对 button
|
||||
* 计数(其余参数变更静默)。 */
|
||||
char *name = NULL;
|
||||
g_propSuite->propGetString(inArgs, kOfxPropName, 0, &name);
|
||||
if (name && strcmp(name, "button") == 0) {
|
||||
record_button_instance_changed();
|
||||
}
|
||||
return kOfxStatOK;
|
||||
}
|
||||
return kOfxStatReplyDefault;
|
||||
}
|
||||
|
||||
@@ -496,6 +587,184 @@ static OfxStatus mainEntryID(const char *action, const void *handle,
|
||||
return mainEntry(action, handle, inArgs, outArgs);
|
||||
}
|
||||
|
||||
/* ---------- interact 变体(M11 §5):自定义 UI 宿主验证 ---------- */
|
||||
|
||||
/* interact 调用记录:追加到 OAK_TEST_PLUGIN_INTERACT_MARKER 指向的文件
|
||||
* (宿主测试断言用;未设环境变量时静默)。 */
|
||||
static void interact_record(const char *fmt, ...)
|
||||
{
|
||||
const char *marker = getenv("OAK_TEST_PLUGIN_INTERACT_MARKER");
|
||||
if (!marker)
|
||||
return;
|
||||
FILE *f = fopen(marker, "a");
|
||||
if (!f)
|
||||
return;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vfprintf(f, fmt, ap);
|
||||
va_end(ap);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* 记录 pen 事件的真实参数(inArgs 的 canonical/视口/压力属性)。 */
|
||||
static void record_pen(const char *action, OfxPropertySetHandle inArgs)
|
||||
{
|
||||
int vp[2] = { 0, 0 };
|
||||
double canon[2] = { 0.0, 0.0 };
|
||||
double pressure = 0.0;
|
||||
g_propSuite->propGetIntN(inArgs, kOfxInteractPropPenViewportPosition, 2, vp);
|
||||
g_propSuite->propGetDoubleN(inArgs, kOfxInteractPropPenPosition, 2, canon);
|
||||
g_propSuite->propGetDouble(inArgs, kOfxInteractPropPenPressure, 0, &pressure);
|
||||
interact_record("%s vp=%d,%d canon=%g,%g pressure=%g\n", action, vp[0], vp[1], canon[0],
|
||||
canon[1], pressure);
|
||||
}
|
||||
|
||||
/* 记录 key 事件的真实参数(keySym/keyString)。 */
|
||||
static void record_key(const char *action, OfxPropertySetHandle inArgs)
|
||||
{
|
||||
int sym = 0;
|
||||
char *s = NULL;
|
||||
g_propSuite->propGetInt(inArgs, kOfxPropKeySym, 0, &sym);
|
||||
g_propSuite->propGetString(inArgs, kOfxPropKeyString, 0, &s);
|
||||
interact_record("%s sym=%d str=%s\n", action, sym, s ? s : "");
|
||||
}
|
||||
|
||||
/* kOfxInteractActionDraw:读 inArgs(viewport/pixelScale/time/
|
||||
* backgroundImage/drawContext),经 Draw suite setColour + 原生 GL 画
|
||||
* 已知色块(矩形 10..30 canonical)。macOS 上真实绘制;非 macOS 跳过
|
||||
* GL。返回 OK。 */
|
||||
static OfxStatus actionInteractDraw(OfxPropertySetHandle inArgs)
|
||||
{
|
||||
double vp[2] = { 0.0, 0.0 };
|
||||
double scale[2] = { 1.0, 1.0 };
|
||||
double t = 0.0;
|
||||
void *bg = NULL;
|
||||
void *drawCtx = NULL;
|
||||
g_propSuite->propGetDoubleN(inArgs, kOfxInteractPropViewportSize, 2, vp);
|
||||
g_propSuite->propGetDoubleN(inArgs, kOfxInteractPropPixelScale, 2, scale);
|
||||
g_propSuite->propGetDouble(inArgs, kOfxPropTime, 0, &t);
|
||||
g_propSuite->propGetPointer(inArgs, kOfxInteractPropBackgroundImage, 0, &bg);
|
||||
g_propSuite->propGetPointer(inArgs, kOfxInteractPropDrawContext, 0, &drawCtx);
|
||||
|
||||
/* Draw suite 状态真实读写:setColour 写、getColour 读(宿主色板)。 */
|
||||
double setcol[4] = { 0.0, 0.0, 0.0, 0.0 };
|
||||
double getcol[4] = { 0.0, 0.0, 0.0, 0.0 };
|
||||
int setcol_st = kOfxStatFailed, getcol_st = kOfxStatFailed;
|
||||
if (g_drawSuite && drawCtx) {
|
||||
OfxRGBAColourF col = { 0.9f, 0.1f, 0.2f, 1.0f };
|
||||
setcol_st = g_drawSuite->setColour(drawCtx, &col);
|
||||
setcol[0] = col.r;
|
||||
setcol[1] = col.g;
|
||||
setcol[2] = col.b;
|
||||
setcol[3] = col.a;
|
||||
OfxRGBAColourF bgc = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||
getcol_st = g_drawSuite->getColour(drawCtx, kOfxStandardColourOverlayBackground, &bgc);
|
||||
getcol[0] = bgc.r;
|
||||
getcol[1] = bgc.g;
|
||||
getcol[2] = bgc.b;
|
||||
getcol[3] = bgc.a;
|
||||
}
|
||||
|
||||
int draw_st = kOfxStatFailed;
|
||||
#ifdef __APPLE__
|
||||
/* 清成暗背景 + Draw suite 画实心矩形(canonical 坐标;宿主经正交
|
||||
* 投影映射到视口)。 */
|
||||
glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
if (g_drawSuite && drawCtx) {
|
||||
OfxPointD pts[2] = { { 10.0, 10.0 }, { 30.0, 30.0 } };
|
||||
draw_st = g_drawSuite->draw(drawCtx, kOfxDrawPrimitiveRectangle, pts, 2);
|
||||
}
|
||||
#else
|
||||
(void)0;
|
||||
#endif
|
||||
|
||||
interact_record("draw vp=%gx%g scale=%gx%g t=%g bg=%p setcol_st=%d setcol=%g,%g,%g,%g "
|
||||
"getcol_st=%d getcol=%g,%g,%g,%g draw_st=%d\n",
|
||||
vp[0], vp[1], scale[0], scale[1], t, bg, setcol_st, setcol[0], setcol[1],
|
||||
setcol[2], setcol[3], getcol_st, getcol[0], getcol[1], getcol[2], getcol[3],
|
||||
draw_st);
|
||||
return kOfxStatOK;
|
||||
}
|
||||
|
||||
/* interact 入口(经 kOfxImageEffectPluginPropOverlayInteractV2 声明):
|
||||
* 只处理 interact 的 action;效果侧 action 一律 ReplyDefault(不会被
|
||||
* 宿主以效果 handle 调到这里)。 */
|
||||
static OfxStatus mainEntryInteract(const char *action, const void *handle,
|
||||
OfxPropertySetHandle inArgs, OfxPropertySetHandle outArgs)
|
||||
{
|
||||
(void)handle;
|
||||
(void)outArgs;
|
||||
if (strcmp(action, kOfxActionNewInteract) == 0) {
|
||||
interact_record("new_interact\n");
|
||||
return kOfxStatOK;
|
||||
}
|
||||
if (strcmp(action, kOfxActionDescribe) == 0) {
|
||||
interact_record("describe\n");
|
||||
return kOfxStatOK;
|
||||
}
|
||||
if (strcmp(action, kOfxActionCreateInstance) == 0) {
|
||||
interact_record("create\n");
|
||||
return kOfxStatOK;
|
||||
}
|
||||
if (strcmp(action, kOfxActionDestroyInstance) == 0) {
|
||||
interact_record("destroy\n");
|
||||
return kOfxStatOK;
|
||||
}
|
||||
if (strcmp(action, kOfxInteractActionDraw) == 0) {
|
||||
return actionInteractDraw(inArgs);
|
||||
}
|
||||
if (strcmp(action, kOfxInteractActionPenMotion) == 0) {
|
||||
record_pen("pen_motion", inArgs);
|
||||
return kOfxStatOK;
|
||||
}
|
||||
if (strcmp(action, kOfxInteractActionPenDown) == 0) {
|
||||
record_pen("pen_down", inArgs);
|
||||
return kOfxStatOK;
|
||||
}
|
||||
if (strcmp(action, kOfxInteractActionPenUp) == 0) {
|
||||
record_pen("pen_up", inArgs);
|
||||
return kOfxStatOK;
|
||||
}
|
||||
if (strcmp(action, kOfxInteractActionKeyDown) == 0) {
|
||||
int sym = 0;
|
||||
g_propSuite->propGetInt(inArgs, kOfxPropKeySym, 0, &sym);
|
||||
record_key("key_down", inArgs);
|
||||
/* Escape → ReplyDefault(宿主应透传该状态:插件未处理,事件
|
||||
* 可交视图中的其他对象)。 */
|
||||
if (sym == kOfxKey_Escape)
|
||||
return kOfxStatReplyDefault;
|
||||
return kOfxStatOK;
|
||||
}
|
||||
if (strcmp(action, kOfxInteractActionKeyUp) == 0) {
|
||||
record_key("key_up", inArgs);
|
||||
return kOfxStatOK;
|
||||
}
|
||||
if (strcmp(action, kOfxInteractActionIdle) == 0) {
|
||||
interact_record("idle\n");
|
||||
return kOfxStatOK;
|
||||
}
|
||||
/* GainFocus/LoseFocus 等:默认(ReplyDefault = 未处理,宿主可继续)。 */
|
||||
return kOfxStatReplyDefault;
|
||||
}
|
||||
|
||||
/* interact 变体的效果侧 main entry:describe 建参数/clip 并声明 overlay
|
||||
* interact V2 入口(指向 mainEntryInteract);其余效果 action 委托 base
|
||||
* mainEntry。 */
|
||||
static OfxStatus mainEntryInteractEffect(const char *action, const void *handle,
|
||||
OfxPropertySetHandle inArgs, OfxPropertySetHandle outArgs)
|
||||
{
|
||||
if (strcmp(action, kOfxActionDescribe) == 0 ||
|
||||
strcmp(action, kOfxImageEffectActionDescribeInContext) == 0) {
|
||||
OfxStatus st = actionDescribe(handle, 0);
|
||||
if (st != kOfxStatOK)
|
||||
return st;
|
||||
return propSetPointer(handle, kOfxImageEffectPluginPropOverlayInteractV2, 0,
|
||||
(void *)mainEntryInteract);
|
||||
}
|
||||
return mainEntry(action, handle, inArgs, outArgs);
|
||||
}
|
||||
|
||||
/* ---------- 导出 ---------- */
|
||||
|
||||
static const OfxPlugin test_plugin = {
|
||||
@@ -528,9 +797,19 @@ static const OfxPlugin test_plugin_id = {
|
||||
/* mainEntry */ mainEntryID,
|
||||
};
|
||||
|
||||
static const OfxPlugin test_plugin_interact = {
|
||||
/* pluginApi */ kOfxImageEffectPluginApi,
|
||||
/* apiVersion */ kOfxImageEffectPluginApiVersion,
|
||||
/* pluginIdentifier */ "org.oak.test-plugin.interact",
|
||||
/* pluginVersionMajor */ 1,
|
||||
/* pluginVersionMinor */ 0,
|
||||
/* setHost */ setHost,
|
||||
/* mainEntry */ mainEntryInteractEffect,
|
||||
};
|
||||
|
||||
OfxExport int OfxGetNumberOfPlugins(void)
|
||||
{
|
||||
return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
OfxExport OfxPlugin *OfxGetPlugin(int nth)
|
||||
@@ -541,5 +820,7 @@ OfxExport OfxPlugin *OfxGetPlugin(int nth)
|
||||
return (OfxPlugin *)&test_plugin_gl;
|
||||
if (nth == 2)
|
||||
return (OfxPlugin *)&test_plugin_id;
|
||||
if (nth == 3)
|
||||
return (OfxPlugin *)&test_plugin_interact;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
#ifndef _ofxDraw_h_
|
||||
#define _ofxDraw_h_
|
||||
|
||||
#include "ofxCore.h"
|
||||
#include "ofxPixels.h"
|
||||
|
||||
// Copyright OpenFX and contributors to the OpenFX project.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** @file ofxDrawSuite.h
|
||||
API for host- and GPU API-independent drawing.
|
||||
@version Added in OpenFX 1.5
|
||||
*/
|
||||
|
||||
|
||||
/** @brief the string that names the DrawSuite, passed to OfxHost::fetchSuite */
|
||||
#define kOfxDrawSuite "OfxDrawSuite"
|
||||
|
||||
/** @brief Blind declaration of an OFX drawing context
|
||||
*/
|
||||
typedef struct OfxDrawContext *OfxDrawContextHandle;
|
||||
|
||||
/** @brief The Draw Context handle
|
||||
|
||||
@propdef
|
||||
type: pointer
|
||||
dimension: 1
|
||||
*/
|
||||
#define kOfxInteractPropDrawContext "OfxInteractPropDrawContext"
|
||||
|
||||
/** @brief Defines valid values for OfxDrawSuiteV1::getColour */
|
||||
typedef enum OfxStandardColour
|
||||
{
|
||||
kOfxStandardColourOverlayBackground,
|
||||
kOfxStandardColourOverlayActive,
|
||||
kOfxStandardColourOverlaySelected,
|
||||
kOfxStandardColourOverlayDeselected,
|
||||
kOfxStandardColourOverlayMarqueeFG,
|
||||
kOfxStandardColourOverlayMarqueeBG,
|
||||
kOfxStandardColourOverlayText
|
||||
} OfxStandardColour;
|
||||
|
||||
/** @brief Defines valid values for OfxDrawSuiteV1::setLineStipple */
|
||||
typedef enum OfxDrawLineStipplePattern
|
||||
{
|
||||
kOfxDrawLineStipplePatternSolid, // -----
|
||||
kOfxDrawLineStipplePatternDot, // .....
|
||||
kOfxDrawLineStipplePatternDash, // - - -
|
||||
kOfxDrawLineStipplePatternAltDash, // - - -
|
||||
kOfxDrawLineStipplePatternDotDash // .-.-.-
|
||||
} OfxDrawLineStipplePattern;
|
||||
|
||||
/** @brief Defines valid values for OfxDrawSuiteV1::draw */
|
||||
|
||||
typedef enum OfxDrawPrimitive
|
||||
{
|
||||
kOfxDrawPrimitiveLines,
|
||||
kOfxDrawPrimitiveLineStrip,
|
||||
kOfxDrawPrimitiveLineLoop,
|
||||
kOfxDrawPrimitiveRectangle,
|
||||
kOfxDrawPrimitivePolygon,
|
||||
kOfxDrawPrimitiveEllipse
|
||||
} OfxDrawPrimitive;
|
||||
|
||||
/** @brief Defines text alignment values for OfxDrawSuiteV1::drawText */
|
||||
typedef enum OfxDrawTextAlignment
|
||||
{
|
||||
kOfxDrawTextAlignmentLeft = 0x0001,
|
||||
kOfxDrawTextAlignmentRight = 0x0002,
|
||||
kOfxDrawTextAlignmentTop = 0x0004,
|
||||
kOfxDrawTextAlignmentBottom = 0x0008,
|
||||
kOfxDrawTextAlignmentBaseline = 0x0010,
|
||||
kOfxDrawTextAlignmentCenterH = (kOfxDrawTextAlignmentLeft | kOfxDrawTextAlignmentRight),
|
||||
kOfxDrawTextAlignmentCenterV = (kOfxDrawTextAlignmentTop | kOfxDrawTextAlignmentBaseline)
|
||||
} OfxDrawTextAlignment;
|
||||
|
||||
/** @brief OFX suite that allows an effect to draw to a host-defined display context.
|
||||
To use this, the plugin must use kOfxImageEffectPluginPropOverlayInteractV2.
|
||||
*/
|
||||
typedef struct OfxDrawSuiteV1 {
|
||||
/** @brief Retrieves the host's desired draw colour for
|
||||
|
||||
\arg \c context draw context
|
||||
\arg \c std_colour desired colour type
|
||||
\arg \c colour returned RGBA colour
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK - the colour was returned
|
||||
- ::kOfxStatErrValue - std_colour was invalid
|
||||
- ::kOfxStatFailed - failure, e.g. if function is called outside kOfxInteractActionDraw
|
||||
*/
|
||||
OfxStatus (*getColour)(OfxDrawContextHandle context, OfxStandardColour std_colour, OfxRGBAColourF *colour);
|
||||
|
||||
/** @brief Sets the colour for future drawing operations (lines, filled shapes and text)
|
||||
|
||||
\arg \c context draw context
|
||||
\arg \c colour RGBA colour
|
||||
|
||||
The host should use "over" compositing when using a non-opaque colour.
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK - the colour was changed
|
||||
- ::kOfxStatFailed - failure, e.g. if function is called outside kOfxInteractActionDraw
|
||||
*/
|
||||
OfxStatus (*setColour)(OfxDrawContextHandle context, const OfxRGBAColourF *colour);
|
||||
|
||||
/** @brief Sets the line width for future line drawing operations
|
||||
|
||||
\arg \c context draw context
|
||||
\arg \c width line width
|
||||
|
||||
Use width 0 for a single pixel line or non-zero for a smooth line of the desired width
|
||||
|
||||
The host should adjust for screen density.
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK - the width was changed
|
||||
- ::kOfxStatFailed - failure, e.g. if function is called outside kOfxInteractActionDraw
|
||||
*/
|
||||
OfxStatus (*setLineWidth)(OfxDrawContextHandle context, float width);
|
||||
|
||||
/** @brief Sets the stipple pattern for future line drawing operations
|
||||
|
||||
\arg \c context draw context
|
||||
\arg \c pattern desired stipple pattern
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK - the pattern was changed
|
||||
- ::kOfxStatErrValue - pattern was not valid
|
||||
- ::kOfxStatFailed - failure, e.g. if function is called outside kOfxInteractActionDraw
|
||||
*/
|
||||
OfxStatus (*setLineStipple)(OfxDrawContextHandle context, OfxDrawLineStipplePattern pattern);
|
||||
|
||||
/** @brief Draws a primitive of the desired type
|
||||
|
||||
\arg \c context draw context
|
||||
\arg \c primitive desired primitive
|
||||
\arg \c points array of points in the primitive
|
||||
\arg \c point_count number of points in the array
|
||||
|
||||
kOfxDrawPrimitiveLines - like GL_LINES, n points draws n/2 separated lines
|
||||
kOfxDrawPrimitiveLineStrip - like GL_LINE_STRIP, n points draws n-1 connected lines
|
||||
kOfxDrawPrimitiveLineLoop - like GL_LINE_LOOP, n points draws n connected lines
|
||||
kOfxDrawPrimitiveRectangle - draws an axis-aligned filled rectangle defined by 2 opposite corner points
|
||||
kOfxDrawPrimitivePolygon - like GL_POLYGON, draws a filled n-sided polygon
|
||||
kOfxDrawPrimitiveEllipse - draws a axis-aligned elliptical line (not filled) within the rectangle defined by 2 opposite corner points
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK - the draw was completed
|
||||
- ::kOfxStatErrValue - invalid primitive, or point_count not valid for primitive
|
||||
- ::kOfxStatFailed - failure, e.g. if function is called outside kOfxInteractActionDraw
|
||||
*/
|
||||
OfxStatus (*draw)(OfxDrawContextHandle context, OfxDrawPrimitive primitive, const OfxPointD *points, int point_count);
|
||||
|
||||
|
||||
/** @brief Draws text at the specified position
|
||||
|
||||
\arg \c context draw context
|
||||
\arg \c text text to draw (UTF-8 encoded)
|
||||
\arg \c pos position at which to align the text
|
||||
\arg \c alignment text alignment flags (see kOfxDrawTextAlignment*)
|
||||
|
||||
The text font face and size are determined by the host.
|
||||
|
||||
@returns
|
||||
- ::kOfxStatOK - the text was drawn
|
||||
- ::kOfxStatErrValue - text or pos were not defined
|
||||
- ::kOfxStatFailed - failure, e.g. if function is called outside kOfxInteractActionDraw
|
||||
*/
|
||||
OfxStatus (*drawText)(OfxDrawContextHandle context, const char *text, const OfxPointD *pos, int alignment);
|
||||
|
||||
} OfxDrawSuiteV1;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,529 @@
|
||||
#ifndef _ofxKeySyms_h_
|
||||
#define _ofxKeySyms_h_
|
||||
|
||||
// Copyright OpenFX and contributors to the OpenFX project.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
|
||||
/**
|
||||
\addtogroup PropertiesGeneral
|
||||
*/
|
||||
/*@{*/
|
||||
|
||||
/** @brief Property used to indicate which a key on the keyboard or a button on a button device has been pressed
|
||||
|
||||
This property represents a raw key press, it does not represent the 'character value' of the key.
|
||||
|
||||
This property is associated with a ::kOfxPropKeyString property, which encodes the UTF8
|
||||
value for the keypress/button press. Some keys (for example arrow keys) have no UTF8 equivalent.
|
||||
|
||||
Some keys, especially on non-english language systems, may have a UTF8 value, but \em not a keysym values, in these
|
||||
cases, the keysym will have a value of kOfxKey_Unknown, but the ::kOfxPropKeyString property will still be set with
|
||||
the UTF8 value.
|
||||
|
||||
- Valid Values - one of any specified by #defines in the file ofxKeySyms.h.
|
||||
@propdef
|
||||
type: int
|
||||
dimension: 1
|
||||
cname: kOfxPropKeySym
|
||||
*/
|
||||
#define kOfxPropKeySym "kOfxPropKeySym"
|
||||
|
||||
/** @brief This property encodes a single keypresses that generates a unicode code point. The value is stored as a UTF8 string.
|
||||
|
||||
This property represents the UTF8 encode value of a single key press by a user in an OFX interact.
|
||||
|
||||
This property is associated with a ::kOfxPropKeySym which represents an integer value for the key press. Some keys (for example arrow keys) have no UTF8 equivalent,
|
||||
in which case this is set to the empty string "", and the associate ::kOfxPropKeySym is set to the equivalent raw key press.
|
||||
|
||||
Some keys, especially on non-english language systems, may have a UTF8 value, but \em not a keysym values, in these
|
||||
cases, the keysym will have a value of kOfxKey_Unknown, but the ::kOfxPropKeyString property will still be set with
|
||||
the UTF8 value.
|
||||
|
||||
- Valid Values - a UTF8 string representing a single character, or the empty string.
|
||||
@propdef
|
||||
type: string
|
||||
dimension: 1
|
||||
cname: kOfxPropKeyString
|
||||
*/
|
||||
#define kOfxPropKeyString "kOfxPropKeyString"
|
||||
|
||||
/*
|
||||
The keysyms below have been lifted wholesale out of the X-Windows key symbol header file. Only
|
||||
the names have been changed to protect the innocent.
|
||||
*/
|
||||
|
||||
/*@}*/
|
||||
|
||||
/*
|
||||
Copyright (c) 1987, 1994 X Consortium
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of the X Consortium shall
|
||||
not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization
|
||||
from the X Consortium.
|
||||
|
||||
|
||||
Copyright 1987 by Digital Equipment Corporation, Maynard, Massachusetts
|
||||
|
||||
All Rights Reserved
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted,
|
||||
provided that the above copyright notice appear in all copies and that
|
||||
both that copyright notice and this permission notice appear in
|
||||
supporting documentation, and that the name of Digital not be
|
||||
used in advertising or publicity pertaining to distribution of the
|
||||
software without specific, written prior permission.
|
||||
|
||||
DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
|
||||
ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL
|
||||
DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
|
||||
ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
|
||||
SOFTWARE.
|
||||
|
||||
**/
|
||||
|
||||
/**
|
||||
\defgroup KeySyms OFX Key symbols
|
||||
|
||||
These keysymbols are used as values by the ::kOfxPropKeySym property to indicate the value
|
||||
of a key that has been pressed. A corresponding ::kOfxPropKeyString property is also set to
|
||||
contain the unicode value of the key (if it has one).
|
||||
|
||||
The special keysym ::kOfxKey_Unknown is used to set the ::kOfxPropKeySym property in cases
|
||||
where the key has a UTF8 value which is not supported by the symbols below.
|
||||
|
||||
*/
|
||||
/*@{*/
|
||||
|
||||
#define kOfxKey_Unknown 0x0
|
||||
|
||||
/*
|
||||
* TTY Functions, cleverly chosen to map to ascii, for convenience of
|
||||
* programming, but could have been arbitrary (at the cost of lookup
|
||||
* tables in client code.
|
||||
*/
|
||||
#define kOfxKey_BackSpace 0xFF08 /* back space, back char */
|
||||
#define kOfxKey_Tab 0xFF09
|
||||
#define kOfxKey_Linefeed 0xFF0A /* Linefeed, LF */
|
||||
#define kOfxKey_Clear 0xFF0B
|
||||
#define kOfxKey_Return 0xFF0D /* Return, enter */
|
||||
#define kOfxKey_Pause 0xFF13 /* Pause, hold */
|
||||
#define kOfxKey_Scroll_Lock 0xFF14
|
||||
#define kOfxKey_Sys_Req 0xFF15
|
||||
#define kOfxKey_Escape 0xFF1B
|
||||
#define kOfxKey_Delete 0xFFFF /* Delete, rubout */
|
||||
|
||||
|
||||
|
||||
/* International & multi-key character composition */
|
||||
|
||||
#define kOfxKey_Multi_key 0xFF20 /* Multi-key character compose */
|
||||
#define kOfxKey_SingleCandidate 0xFF3C
|
||||
#define kOfxKey_MultipleCandidate 0xFF3D
|
||||
#define kOfxKey_PreviousCandidate 0xFF3E
|
||||
|
||||
/* Japanese keyboard support */
|
||||
|
||||
#define kOfxKey_Kanji 0xFF21 /* Kanji, Kanji convert */
|
||||
#define kOfxKey_Muhenkan 0xFF22 /* Cancel Conversion */
|
||||
#define kOfxKey_Henkan_Mode 0xFF23 /* Start/Stop Conversion */
|
||||
#define kOfxKey_Henkan 0xFF23 /* Alias for Henkan_Mode */
|
||||
#define kOfxKey_Romaji 0xFF24 /* to Romaji */
|
||||
#define kOfxKey_Hiragana 0xFF25 /* to Hiragana */
|
||||
#define kOfxKey_Katakana 0xFF26 /* to Katakana */
|
||||
#define kOfxKey_Hiragana_Katakana 0xFF27 /* Hiragana/Katakana toggle */
|
||||
#define kOfxKey_Zenkaku 0xFF28 /* to Zenkaku */
|
||||
#define kOfxKey_Hankaku 0xFF29 /* to Hankaku */
|
||||
#define kOfxKey_Zenkaku_Hankaku 0xFF2A /* Zenkaku/Hankaku toggle */
|
||||
#define kOfxKey_Touroku 0xFF2B /* Add to Dictionary */
|
||||
#define kOfxKey_Massyo 0xFF2C /* Delete from Dictionary */
|
||||
#define kOfxKey_Kana_Lock 0xFF2D /* Kana Lock */
|
||||
#define kOfxKey_Kana_Shift 0xFF2E /* Kana Shift */
|
||||
#define kOfxKey_Eisu_Shift 0xFF2F /* Alphanumeric Shift */
|
||||
#define kOfxKey_Eisu_toggle 0xFF30 /* Alphanumeric toggle */
|
||||
#define kOfxKey_Zen_Koho 0xFF3D /* Multiple/All Candidate(s) */
|
||||
#define kOfxKey_Mae_Koho 0xFF3E /* Previous Candidate */
|
||||
|
||||
/* Cursor control & motion */
|
||||
|
||||
#define kOfxKey_Home 0xFF50
|
||||
#define kOfxKey_Left 0xFF51 /* Move left, left arrow */
|
||||
#define kOfxKey_Up 0xFF52 /* Move up, up arrow */
|
||||
#define kOfxKey_Right 0xFF53 /* Move right, right arrow */
|
||||
#define kOfxKey_Down 0xFF54 /* Move down, down arrow */
|
||||
#define kOfxKey_Prior 0xFF55 /* Prior, previous */
|
||||
#define kOfxKey_Page_Up 0xFF55
|
||||
#define kOfxKey_Next 0xFF56 /* Next */
|
||||
#define kOfxKey_Page_Down 0xFF56
|
||||
#define kOfxKey_End 0xFF57 /* EOL */
|
||||
#define kOfxKey_Begin 0xFF58 /* BOL */
|
||||
|
||||
|
||||
/* Misc Functions */
|
||||
|
||||
#define kOfxKey_Select 0xFF60 /* Select, mark */
|
||||
#define kOfxKey_Print 0xFF61
|
||||
#define kOfxKey_Execute 0xFF62 /* Execute, run, do */
|
||||
#define kOfxKey_Insert 0xFF63 /* Insert, insert here */
|
||||
#define kOfxKey_Undo 0xFF65 /* Undo, oops */
|
||||
#define kOfxKey_Redo 0xFF66 /* redo, again */
|
||||
#define kOfxKey_Menu 0xFF67
|
||||
#define kOfxKey_Find 0xFF68 /* Find, search */
|
||||
#define kOfxKey_Cancel 0xFF69 /* Cancel, stop, abort, exit */
|
||||
#define kOfxKey_Help 0xFF6A /* Help */
|
||||
#define kOfxKey_Break 0xFF6B
|
||||
#define kOfxKey_Mode_switch 0xFF7E /* Character set switch */
|
||||
#define kOfxKey_script_switch 0xFF7E /* Alias for mode_switch */
|
||||
#define kOfxKey_Num_Lock 0xFF7F
|
||||
|
||||
/* Keypad Functions, keypad numbers cleverly chosen to map to ascii */
|
||||
|
||||
#define kOfxKey_KP_Space 0xFF80 /* space */
|
||||
#define kOfxKey_KP_Tab 0xFF89
|
||||
#define kOfxKey_KP_Enter 0xFF8D /* enter */
|
||||
#define kOfxKey_KP_F1 0xFF91 /* PF1, KP_A, ... */
|
||||
#define kOfxKey_KP_F2 0xFF92
|
||||
#define kOfxKey_KP_F3 0xFF93
|
||||
#define kOfxKey_KP_F4 0xFF94
|
||||
#define kOfxKey_KP_Home 0xFF95
|
||||
#define kOfxKey_KP_Left 0xFF96
|
||||
#define kOfxKey_KP_Up 0xFF97
|
||||
#define kOfxKey_KP_Right 0xFF98
|
||||
#define kOfxKey_KP_Down 0xFF99
|
||||
#define kOfxKey_KP_Prior 0xFF9A
|
||||
#define kOfxKey_KP_Page_Up 0xFF9A
|
||||
#define kOfxKey_KP_Next 0xFF9B
|
||||
#define kOfxKey_KP_Page_Down 0xFF9B
|
||||
#define kOfxKey_KP_End 0xFF9C
|
||||
#define kOfxKey_KP_Begin 0xFF9D
|
||||
#define kOfxKey_KP_Insert 0xFF9E
|
||||
#define kOfxKey_KP_Delete 0xFF9F
|
||||
#define kOfxKey_KP_Equal 0xFFBD /* equals */
|
||||
#define kOfxKey_KP_Multiply 0xFFAA
|
||||
#define kOfxKey_KP_Add 0xFFAB
|
||||
#define kOfxKey_KP_Separator 0xFFAC /* separator, often comma */
|
||||
#define kOfxKey_KP_Subtract 0xFFAD
|
||||
#define kOfxKey_KP_Decimal 0xFFAE
|
||||
#define kOfxKey_KP_Divide 0xFFAF
|
||||
|
||||
#define kOfxKey_KP_0 0xFFB0
|
||||
#define kOfxKey_KP_1 0xFFB1
|
||||
#define kOfxKey_KP_2 0xFFB2
|
||||
#define kOfxKey_KP_3 0xFFB3
|
||||
#define kOfxKey_KP_4 0xFFB4
|
||||
#define kOfxKey_KP_5 0xFFB5
|
||||
#define kOfxKey_KP_6 0xFFB6
|
||||
#define kOfxKey_KP_7 0xFFB7
|
||||
#define kOfxKey_KP_8 0xFFB8
|
||||
#define kOfxKey_KP_9 0xFFB9
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Auxiliary Functions; note the duplicate definitions for left and right
|
||||
* function keys; Sun keyboards and a few other manufactures have such
|
||||
* function key groups on the left and/or right sides of the keyboard.
|
||||
* We've not found a keyboard with more than 35 function keys total.
|
||||
*/
|
||||
|
||||
#define kOfxKey_F1 0xFFBE
|
||||
#define kOfxKey_F2 0xFFBF
|
||||
#define kOfxKey_F3 0xFFC0
|
||||
#define kOfxKey_F4 0xFFC1
|
||||
#define kOfxKey_F5 0xFFC2
|
||||
#define kOfxKey_F6 0xFFC3
|
||||
#define kOfxKey_F7 0xFFC4
|
||||
#define kOfxKey_F8 0xFFC5
|
||||
#define kOfxKey_F9 0xFFC6
|
||||
#define kOfxKey_F10 0xFFC7
|
||||
#define kOfxKey_F11 0xFFC8
|
||||
#define kOfxKey_L1 0xFFC8
|
||||
#define kOfxKey_F12 0xFFC9
|
||||
#define kOfxKey_L2 0xFFC9
|
||||
#define kOfxKey_F13 0xFFCA
|
||||
#define kOfxKey_L3 0xFFCA
|
||||
#define kOfxKey_F14 0xFFCB
|
||||
#define kOfxKey_L4 0xFFCB
|
||||
#define kOfxKey_F15 0xFFCC
|
||||
#define kOfxKey_L5 0xFFCC
|
||||
#define kOfxKey_F16 0xFFCD
|
||||
#define kOfxKey_L6 0xFFCD
|
||||
#define kOfxKey_F17 0xFFCE
|
||||
#define kOfxKey_L7 0xFFCE
|
||||
#define kOfxKey_F18 0xFFCF
|
||||
#define kOfxKey_L8 0xFFCF
|
||||
#define kOfxKey_F19 0xFFD0
|
||||
#define kOfxKey_L9 0xFFD0
|
||||
#define kOfxKey_F20 0xFFD1
|
||||
#define kOfxKey_L10 0xFFD1
|
||||
#define kOfxKey_F21 0xFFD2
|
||||
#define kOfxKey_R1 0xFFD2
|
||||
#define kOfxKey_F22 0xFFD3
|
||||
#define kOfxKey_R2 0xFFD3
|
||||
#define kOfxKey_F23 0xFFD4
|
||||
#define kOfxKey_R3 0xFFD4
|
||||
#define kOfxKey_F24 0xFFD5
|
||||
#define kOfxKey_R4 0xFFD5
|
||||
#define kOfxKey_F25 0xFFD6
|
||||
#define kOfxKey_R5 0xFFD6
|
||||
#define kOfxKey_F26 0xFFD7
|
||||
#define kOfxKey_R6 0xFFD7
|
||||
#define kOfxKey_F27 0xFFD8
|
||||
#define kOfxKey_R7 0xFFD8
|
||||
#define kOfxKey_F28 0xFFD9
|
||||
#define kOfxKey_R8 0xFFD9
|
||||
#define kOfxKey_F29 0xFFDA
|
||||
#define kOfxKey_R9 0xFFDA
|
||||
#define kOfxKey_F30 0xFFDB
|
||||
#define kOfxKey_R10 0xFFDB
|
||||
#define kOfxKey_F31 0xFFDC
|
||||
#define kOfxKey_R11 0xFFDC
|
||||
#define kOfxKey_F32 0xFFDD
|
||||
#define kOfxKey_R12 0xFFDD
|
||||
#define kOfxKey_F33 0xFFDE
|
||||
#define kOfxKey_R13 0xFFDE
|
||||
#define kOfxKey_F34 0xFFDF
|
||||
#define kOfxKey_R14 0xFFDF
|
||||
#define kOfxKey_F35 0xFFE0
|
||||
#define kOfxKey_R15 0xFFE0
|
||||
|
||||
/* Modifiers */
|
||||
|
||||
#define kOfxKey_Shift_L 0xFFE1 /* Left shift */
|
||||
#define kOfxKey_Shift_R 0xFFE2 /* Right shift */
|
||||
#define kOfxKey_Control_L 0xFFE3 /* Left control */
|
||||
#define kOfxKey_Control_R 0xFFE4 /* Right control */
|
||||
#define kOfxKey_Caps_Lock 0xFFE5 /* Caps lock */
|
||||
#define kOfxKey_Shift_Lock 0xFFE6 /* Shift lock */
|
||||
|
||||
#define kOfxKey_Meta_L 0xFFE7 /* Left meta */
|
||||
#define kOfxKey_Meta_R 0xFFE8 /* Right meta */
|
||||
#define kOfxKey_Alt_L 0xFFE9 /* Left alt */
|
||||
#define kOfxKey_Alt_R 0xFFEA /* Right alt */
|
||||
#define kOfxKey_Super_L 0xFFEB /* Left super */
|
||||
#define kOfxKey_Super_R 0xFFEC /* Right super */
|
||||
#define kOfxKey_Hyper_L 0xFFED /* Left hyper */
|
||||
#define kOfxKey_Hyper_R 0xFFEE /* Right hyper */
|
||||
|
||||
#define kOfxKey_space 0x020
|
||||
#define kOfxKey_exclam 0x021
|
||||
#define kOfxKey_quotedbl 0x022
|
||||
#define kOfxKey_numbersign 0x023
|
||||
#define kOfxKey_dollar 0x024
|
||||
#define kOfxKey_percent 0x025
|
||||
#define kOfxKey_ampersand 0x026
|
||||
#define kOfxKey_apostrophe 0x027
|
||||
#define kOfxKey_quoteright 0x027 /* deprecated */
|
||||
#define kOfxKey_parenleft 0x028
|
||||
#define kOfxKey_parenright 0x029
|
||||
#define kOfxKey_asterisk 0x02a
|
||||
#define kOfxKey_plus 0x02b
|
||||
#define kOfxKey_comma 0x02c
|
||||
#define kOfxKey_minus 0x02d
|
||||
#define kOfxKey_period 0x02e
|
||||
#define kOfxKey_slash 0x02f
|
||||
#define kOfxKey_0 0x030
|
||||
#define kOfxKey_1 0x031
|
||||
#define kOfxKey_2 0x032
|
||||
#define kOfxKey_3 0x033
|
||||
#define kOfxKey_4 0x034
|
||||
#define kOfxKey_5 0x035
|
||||
#define kOfxKey_6 0x036
|
||||
#define kOfxKey_7 0x037
|
||||
#define kOfxKey_8 0x038
|
||||
#define kOfxKey_9 0x039
|
||||
#define kOfxKey_colon 0x03a
|
||||
#define kOfxKey_semicolon 0x03b
|
||||
#define kOfxKey_less 0x03c
|
||||
#define kOfxKey_equal 0x03d
|
||||
#define kOfxKey_greater 0x03e
|
||||
#define kOfxKey_question 0x03f
|
||||
#define kOfxKey_at 0x040
|
||||
#define kOfxKey_A 0x041
|
||||
#define kOfxKey_B 0x042
|
||||
#define kOfxKey_C 0x043
|
||||
#define kOfxKey_D 0x044
|
||||
#define kOfxKey_E 0x045
|
||||
#define kOfxKey_F 0x046
|
||||
#define kOfxKey_G 0x047
|
||||
#define kOfxKey_H 0x048
|
||||
#define kOfxKey_I 0x049
|
||||
#define kOfxKey_J 0x04a
|
||||
#define kOfxKey_K 0x04b
|
||||
#define kOfxKey_L 0x04c
|
||||
#define kOfxKey_M 0x04d
|
||||
#define kOfxKey_N 0x04e
|
||||
#define kOfxKey_O 0x04f
|
||||
#define kOfxKey_P 0x050
|
||||
#define kOfxKey_Q 0x051
|
||||
#define kOfxKey_R 0x052
|
||||
#define kOfxKey_S 0x053
|
||||
#define kOfxKey_T 0x054
|
||||
#define kOfxKey_U 0x055
|
||||
#define kOfxKey_V 0x056
|
||||
#define kOfxKey_W 0x057
|
||||
#define kOfxKey_X 0x058
|
||||
#define kOfxKey_Y 0x059
|
||||
#define kOfxKey_Z 0x05a
|
||||
#define kOfxKey_bracketleft 0x05b
|
||||
#define kOfxKey_backslash 0x05c
|
||||
#define kOfxKey_bracketright 0x05d
|
||||
#define kOfxKey_asciicircum 0x05e
|
||||
#define kOfxKey_underscore 0x05f
|
||||
#define kOfxKey_grave 0x060
|
||||
#define kOfxKey_quoteleft 0x060 /* deprecated */
|
||||
#define kOfxKey_a 0x061
|
||||
#define kOfxKey_b 0x062
|
||||
#define kOfxKey_c 0x063
|
||||
#define kOfxKey_d 0x064
|
||||
#define kOfxKey_e 0x065
|
||||
#define kOfxKey_f 0x066
|
||||
#define kOfxKey_g 0x067
|
||||
#define kOfxKey_h 0x068
|
||||
#define kOfxKey_i 0x069
|
||||
#define kOfxKey_j 0x06a
|
||||
#define kOfxKey_k 0x06b
|
||||
#define kOfxKey_l 0x06c
|
||||
#define kOfxKey_m 0x06d
|
||||
#define kOfxKey_n 0x06e
|
||||
#define kOfxKey_o 0x06f
|
||||
#define kOfxKey_p 0x070
|
||||
#define kOfxKey_q 0x071
|
||||
#define kOfxKey_r 0x072
|
||||
#define kOfxKey_s 0x073
|
||||
#define kOfxKey_t 0x074
|
||||
#define kOfxKey_u 0x075
|
||||
#define kOfxKey_v 0x076
|
||||
#define kOfxKey_w 0x077
|
||||
#define kOfxKey_x 0x078
|
||||
#define kOfxKey_y 0x079
|
||||
#define kOfxKey_z 0x07a
|
||||
#define kOfxKey_braceleft 0x07b
|
||||
#define kOfxKey_bar 0x07c
|
||||
#define kOfxKey_braceright 0x07d
|
||||
#define kOfxKey_asciitilde 0x07e
|
||||
|
||||
#define kOfxKey_nobreakspace 0x0a0
|
||||
#define kOfxKey_exclamdown 0x0a1
|
||||
#define kOfxKey_cent 0x0a2
|
||||
#define kOfxKey_sterling 0x0a3
|
||||
#define kOfxKey_currency 0x0a4
|
||||
#define kOfxKey_yen 0x0a5
|
||||
#define kOfxKey_brokenbar 0x0a6
|
||||
#define kOfxKey_section 0x0a7
|
||||
#define kOfxKey_diaeresis 0x0a8
|
||||
#define kOfxKey_copyright 0x0a9
|
||||
#define kOfxKey_ordfeminine 0x0aa
|
||||
#define kOfxKey_guillemotleft 0x0ab /* left angle quotation mark */
|
||||
#define kOfxKey_notsign 0x0ac
|
||||
#define kOfxKey_hyphen 0x0ad
|
||||
#define kOfxKey_registered 0x0ae
|
||||
#define kOfxKey_macron 0x0af
|
||||
#define kOfxKey_degree 0x0b0
|
||||
#define kOfxKey_plusminus 0x0b1
|
||||
#define kOfxKey_twosuperior 0x0b2
|
||||
#define kOfxKey_threesuperior 0x0b3
|
||||
#define kOfxKey_acute 0x0b4
|
||||
#define kOfxKey_mu 0x0b5
|
||||
#define kOfxKey_paragraph 0x0b6
|
||||
#define kOfxKey_periodcentered 0x0b7
|
||||
#define kOfxKey_cedilla 0x0b8
|
||||
#define kOfxKey_onesuperior 0x0b9
|
||||
#define kOfxKey_masculine 0x0ba
|
||||
#define kOfxKey_guillemotright 0x0bb /* right angle quotation mark */
|
||||
#define kOfxKey_onequarter 0x0bc
|
||||
#define kOfxKey_onehalf 0x0bd
|
||||
#define kOfxKey_threequarters 0x0be
|
||||
#define kOfxKey_questiondown 0x0bf
|
||||
#define kOfxKey_Agrave 0x0c0
|
||||
#define kOfxKey_Aacute 0x0c1
|
||||
#define kOfxKey_Acircumflex 0x0c2
|
||||
#define kOfxKey_Atilde 0x0c3
|
||||
#define kOfxKey_Adiaeresis 0x0c4
|
||||
#define kOfxKey_Aring 0x0c5
|
||||
#define kOfxKey_AE 0x0c6
|
||||
#define kOfxKey_Ccedilla 0x0c7
|
||||
#define kOfxKey_Egrave 0x0c8
|
||||
#define kOfxKey_Eacute 0x0c9
|
||||
#define kOfxKey_Ecircumflex 0x0ca
|
||||
#define kOfxKey_Ediaeresis 0x0cb
|
||||
#define kOfxKey_Igrave 0x0cc
|
||||
#define kOfxKey_Iacute 0x0cd
|
||||
#define kOfxKey_Icircumflex 0x0ce
|
||||
#define kOfxKey_Idiaeresis 0x0cf
|
||||
#define kOfxKey_ETH 0x0d0
|
||||
#define kOfxKey_Eth 0x0d0 /* deprecated */
|
||||
#define kOfxKey_Ntilde 0x0d1
|
||||
#define kOfxKey_Ograve 0x0d2
|
||||
#define kOfxKey_Oacute 0x0d3
|
||||
#define kOfxKey_Ocircumflex 0x0d4
|
||||
#define kOfxKey_Otilde 0x0d5
|
||||
#define kOfxKey_Odiaeresis 0x0d6
|
||||
#define kOfxKey_multiply 0x0d7
|
||||
#define kOfxKey_Ooblique 0x0d8
|
||||
#define kOfxKey_Ugrave 0x0d9
|
||||
#define kOfxKey_Uacute 0x0da
|
||||
#define kOfxKey_Ucircumflex 0x0db
|
||||
#define kOfxKey_Udiaeresis 0x0dc
|
||||
#define kOfxKey_Yacute 0x0dd
|
||||
#define kOfxKey_THORN 0x0de
|
||||
#define kOfxKey_ssharp 0x0df
|
||||
#define kOfxKey_agrave 0x0e0
|
||||
#define kOfxKey_aacute 0x0e1
|
||||
#define kOfxKey_acircumflex 0x0e2
|
||||
#define kOfxKey_atilde 0x0e3
|
||||
#define kOfxKey_adiaeresis 0x0e4
|
||||
#define kOfxKey_aring 0x0e5
|
||||
#define kOfxKey_ae 0x0e6
|
||||
#define kOfxKey_ccedilla 0x0e7
|
||||
#define kOfxKey_egrave 0x0e8
|
||||
#define kOfxKey_eacute 0x0e9
|
||||
#define kOfxKey_ecircumflex 0x0ea
|
||||
#define kOfxKey_ediaeresis 0x0eb
|
||||
#define kOfxKey_igrave 0x0ec
|
||||
#define kOfxKey_iacute 0x0ed
|
||||
#define kOfxKey_icircumflex 0x0ee
|
||||
#define kOfxKey_idiaeresis 0x0ef
|
||||
#define kOfxKey_eth 0x0f0
|
||||
#define kOfxKey_ntilde 0x0f1
|
||||
#define kOfxKey_ograve 0x0f2
|
||||
#define kOfxKey_oacute 0x0f3
|
||||
#define kOfxKey_ocircumflex 0x0f4
|
||||
#define kOfxKey_otilde 0x0f5
|
||||
#define kOfxKey_odiaeresis 0x0f6
|
||||
#define kOfxKey_division 0x0f7
|
||||
#define kOfxKey_oslash 0x0f8
|
||||
#define kOfxKey_ugrave 0x0f9
|
||||
#define kOfxKey_uacute 0x0fa
|
||||
#define kOfxKey_ucircumflex 0x0fb
|
||||
#define kOfxKey_udiaeresis 0x0fc
|
||||
#define kOfxKey_yacute 0x0fd
|
||||
#define kOfxKey_thorn 0x0fe
|
||||
#define kOfxKey_ydiaeresis 0x0ff
|
||||
|
||||
/*@}*/
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,62 @@
|
||||
#ifndef _ofxPixels_h_
|
||||
#define _ofxPixels_h_
|
||||
|
||||
// Copyright OpenFX and contributors to the OpenFX project.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** @file ofxPixels.h
|
||||
Contains pixel struct definitions
|
||||
*/
|
||||
|
||||
/** @brief Defines an 8 bit per component RGBA pixel */
|
||||
typedef struct OfxRGBAColourB {
|
||||
unsigned char r, g, b, a;
|
||||
}OfxRGBAColourB;
|
||||
|
||||
/** @brief Defines a 16 bit per component RGBA pixel */
|
||||
typedef struct OfxRGBAColourS {
|
||||
unsigned short r, g, b, a;
|
||||
}OfxRGBAColourS;
|
||||
|
||||
/** @brief Defines a floating point component RGBA pixel */
|
||||
typedef struct OfxRGBAColourF {
|
||||
float r, g, b, a;
|
||||
}OfxRGBAColourF;
|
||||
|
||||
|
||||
/** @brief Defines a double precision floating point component RGBA pixel */
|
||||
typedef struct OfxRGBAColourD {
|
||||
double r, g, b, a;
|
||||
}OfxRGBAColourD;
|
||||
|
||||
|
||||
/** @brief Defines an 8 bit per component RGB pixel */
|
||||
typedef struct OfxRGBColourB {
|
||||
unsigned char r, g, b;
|
||||
}OfxRGBColourB;
|
||||
|
||||
/** @brief Defines a 16 bit per component RGB pixel */
|
||||
typedef struct OfxRGBColourS {
|
||||
unsigned short r, g, b;
|
||||
}OfxRGBColourS;
|
||||
|
||||
/** @brief Defines a floating point component RGB pixel */
|
||||
typedef struct OfxRGBColourF {
|
||||
float r, g, b;
|
||||
}OfxRGBColourF;
|
||||
|
||||
/** @brief Defines a double precision floating point component RGB pixel */
|
||||
typedef struct OfxRGBColourD {
|
||||
double r, g, b;
|
||||
}OfxRGBColourD;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -181,9 +181,155 @@ pub(crate) const ACTION_GL_CONTEXT_ATTACHED: &str = "OfxActionOpenGLContextAttac
|
||||
pub(crate) const ACTION_GL_CONTEXT_DETACHED: &str = "kOfxActionOpenGLContextDetached";
|
||||
/// kOfxImageEffectActionGetOutputColourspace(ofxColour.h:283)。
|
||||
pub(crate) const ACTION_GET_OUTPUT_COLOURSPACE: &str = "OfxImageEffectActionGetOutputColourspace";
|
||||
/// kOfxActionInstanceChanged(ofxCore.h:449):宿主侧参数/时间变更
|
||||
/// 通知。按下 push button 后宿主须以 kOfxChangeUserEdited 的原因调用
|
||||
/// 它(ofxCore.h:405-435 的 inArgs 契约)。
|
||||
pub(crate) const ACTION_INSTANCE_CHANGED: &str = "OfxActionInstanceChanged";
|
||||
// ---- Interact(OFX 自定义交互;ofxInteract.h / ofxDrawSuite.h / ofxKeySyms.h)----
|
||||
|
||||
/// kOfxActionNewInteract:宿主创建 interact 时发给 interact 入口的
|
||||
/// 首个 action。vendored ofxInteract.h 未定义该宏(官方头文件无此
|
||||
/// action;任务契约按 "向插件 main entry 发 kOfxActionNewInteract"
|
||||
/// 命名)——取值 "OfxActionNewInteract",与 OFX action 命名惯例一致。
|
||||
/// 插件不实现该 action 时返回 kOfxStatReplyDefault(宿主视为"无
|
||||
/// interact"或"不参与 NewInteract 协议",见 [`crate::instance::Instance::new_interact`])。
|
||||
pub(crate) const ACTION_NEW_INTERACT: &str = "OfxActionNewInteract";
|
||||
/// kOfxInteractActionIdle:宿主空闲泵(任务契约;vendored ofxInteract.h
|
||||
/// 未收录——OFX 官方规范无 Idle action,属本宿主扩展,取值
|
||||
/// "OfxInteractActionIdle")。插件实现与否自愿;未处理返回
|
||||
/// kOfxStatReplyDefault。
|
||||
pub(crate) const ACTION_INTERACT_IDLE: &str = "OfxInteractActionIdle";
|
||||
/// kOfxInteractActionDraw(ofxInteract.h:265)。
|
||||
pub(crate) const ACTION_INTERACT_DRAW: &str = "OfxInteractActionDraw";
|
||||
/// kOfxInteractActionPenMotion(ofxInteract.h:302)。
|
||||
pub(crate) const ACTION_INTERACT_PEN_MOTION: &str = "OfxInteractActionPenMotion";
|
||||
/// kOfxInteractActionPenDown(ofxInteract.h:340)。
|
||||
pub(crate) const ACTION_INTERACT_PEN_DOWN: &str = "OfxInteractActionPenDown";
|
||||
/// kOfxInteractActionPenUp(ofxInteract.h:376)。
|
||||
pub(crate) const ACTION_INTERACT_PEN_UP: &str = "OfxInteractActionPenUp";
|
||||
/// kOfxInteractActionKeyDown(ofxInteract.h:410)。
|
||||
pub(crate) const ACTION_INTERACT_KEY_DOWN: &str = "OfxInteractActionKeyDown";
|
||||
/// kOfxInteractActionKeyUp(ofxInteract.h:443)。
|
||||
pub(crate) const ACTION_INTERACT_KEY_UP: &str = "OfxInteractActionKeyUp";
|
||||
/// kOfxInteractActionGainFocus(ofxInteract.h:501)。
|
||||
pub(crate) const ACTION_INTERACT_GAIN_FOCUS: &str = "OfxInteractActionGainFocus";
|
||||
/// kOfxInteractActionLoseFocus(ofxInteract.h:526)。
|
||||
pub(crate) const ACTION_INTERACT_LOSE_FOCUS: &str = "OfxInteractActionLoseFocus";
|
||||
|
||||
/// kOfxInteractPropPixelScale(ofxInteract.h:58):canonical→屏幕像素
|
||||
/// 换算比例(Double×2)。
|
||||
pub(crate) const PROP_INTERACT_PIXEL_SCALE: &str = "OfxInteractPropPixelScale";
|
||||
/// kOfxInteractPropViewportSize(OFX 1.3 命名 "OfxInteractPropViewport";
|
||||
/// vendored 1.5 头文件已删,任务契约要求 draw inArgs 携带视口尺寸)。
|
||||
pub(crate) const PROP_INTERACT_VIEWPORT_SIZE: &str = "OfxInteractPropViewport";
|
||||
/// kOfxInteractPropBackgroundImage:任务契约的 draw inArgs 背景图像
|
||||
/// 句柄(Pointer;无背景时为空——本宿主 Phase 1 无合成背景,恒空)。
|
||||
/// 非官方 OFX 属性(官方只有 BackgroundColour),属本宿主扩展。
|
||||
pub(crate) const PROP_INTERACT_BACKGROUND_IMAGE: &str = "OfxInteractPropBackgroundImage";
|
||||
/// kOfxInteractPropBackgroundColour(ofxInteract.h:71):宿主视口背景色
|
||||
/// (Double×3)。
|
||||
pub(crate) const PROP_INTERACT_BACKGROUND_COLOUR: &str = "OfxInteractPropBackgroundColour";
|
||||
/// kOfxInteractPropSuggestedColour(ofxInteract.h:86):宿主建议的 overlay
|
||||
/// 颜色(Double×3;宿主不支持颜色选择时返回 ReplyDefault)。
|
||||
pub(crate) const PROP_INTERACT_SUGGESTED_COLOUR: &str = "OfxInteractPropSuggestedColour";
|
||||
/// kOfxInteractPropSlaveToParam(ofxInteract.h:50):值变化触发 interact
|
||||
/// 重绘的参数名(String×N)。
|
||||
pub(crate) const PROP_INTERACT_SLAVE_TO_PARAM: &str = "OfxInteractPropSlaveToParam";
|
||||
/// kOfxInteractPropPenPosition(ofxInteract.h:95):笔的 canonical 位置
|
||||
/// (Double×2,只读 inArgs)。
|
||||
pub(crate) const PROP_INTERACT_PEN_POSITION: &str = "OfxInteractPropPenPosition";
|
||||
/// kOfxInteractPropPenViewportPosition(ofxInteract.h:104):笔的视口像素
|
||||
/// 位置(Int×2,只读 inArgs)。
|
||||
pub(crate) const PROP_INTERACT_PEN_VIEWPORT_POSITION: &str = "OfxInteractPropPenViewportPosition";
|
||||
/// kOfxInteractPropPenPressure(ofxInteract.h:114):笔压(Double×1,
|
||||
/// 0..1;两态笔映射 0/1)。
|
||||
pub(crate) const PROP_INTERACT_PEN_PRESSURE: &str = "OfxInteractPropPenPressure";
|
||||
/// kOfxInteractPropBitDepth(ofxInteract.h:122):interact 帧缓冲位深
|
||||
/// (Int×1,只读)。
|
||||
pub(crate) const PROP_INTERACT_BIT_DEPTH: &str = "OfxInteractPropBitDepth";
|
||||
/// kOfxInteractPropHasAlpha(ofxInteract.h:132):interact 帧缓冲是否含
|
||||
/// alpha(Int×1,只读)。
|
||||
pub(crate) const PROP_INTERACT_HAS_ALPHA: &str = "OfxInteractPropHasAlpha";
|
||||
/// kOfxInteractPropDrawContext(ofxDrawSuite.h:34):Draw suite 上下文句柄
|
||||
/// (Pointer;draw inArgs 携带,插件取来传给 Draw suite 函数)。
|
||||
pub(crate) const PROP_INTERACT_DRAW_CONTEXT: &str = "OfxInteractPropDrawContext";
|
||||
/// kOfxPropKeySym(ofxKeySyms.h:30):键盘事件的关键码(Int×1)。
|
||||
pub(crate) const PROP_KEY_SYM: &str = "kOfxPropKeySym";
|
||||
/// kOfxPropKeyString(ofxKeySyms.h:49):键盘事件的 UTF-8 字符(String×1)。
|
||||
pub(crate) const PROP_KEY_STRING: &str = "kOfxPropKeyString";
|
||||
/// kOfxImageEffectPluginPropOverlayInteractV2(ofxImageEffect.h:825):
|
||||
/// 插件声明的 overlay interact 入口(Pointer→OfxPluginEntryPoint;V2
|
||||
/// 要求 Draw suite 绘制)。
|
||||
pub(crate) const PROP_OVERLAY_INTERACT_V2: &str = "OfxImageEffectPluginPropOverlayInteractV2";
|
||||
/// kOfxImageEffectPluginPropOverlayInteractV1(ofxImageEffect.h:812)。
|
||||
pub(crate) const PROP_OVERLAY_INTERACT_V1: &str = "OfxImageEffectPluginPropOverlayInteractV1";
|
||||
/// kOfxImageEffectPropSupportsOverlays(ofxImageEffect.h:801):宿主是否
|
||||
/// 允许插件在输出图像上绘制 overlay(能力宣告)。
|
||||
pub(crate) const PROP_SUPPORTS_OVERLAYS: &str = "OfxImageEffectPropSupportsOverlays";
|
||||
|
||||
// ---- OFX 关键码(ofxKeySyms.h;X11 keysym 值,测试/宿主常用子集)----
|
||||
//
|
||||
// 公共:app 侧(WG3b)经 [`crate::suites::interact::Interact::key_down`]/
|
||||
// `key_up` 传关键码。
|
||||
|
||||
/// kOfxKey_Unknown(ofxKeySyms.h:121)。
|
||||
pub const KEY_UNKNOWN: i32 = 0x0;
|
||||
/// kOfxKey_BackSpace(ofxKeySyms.h:128)。
|
||||
pub const KEY_BACKSPACE: i32 = 0xFF08;
|
||||
/// kOfxKey_Tab(ofxKeySyms.h:129)。
|
||||
pub const KEY_TAB: i32 = 0xFF09;
|
||||
/// kOfxKey_Return(ofxKeySyms.h:132)。
|
||||
pub const KEY_RETURN: i32 = 0xFF0D;
|
||||
/// kOfxKey_Escape(ofxKeySyms.h:136)。
|
||||
pub const KEY_ESCAPE: i32 = 0xFF1B;
|
||||
/// kOfxKey_Delete(ofxKeySyms.h:137)。
|
||||
pub const KEY_DELETE: i32 = 0xFFFF;
|
||||
/// kOfxKey_Home(ofxKeySyms.h:172)。
|
||||
pub const KEY_HOME: i32 = 0xFF50;
|
||||
/// kOfxKey_Left(ofxKeySyms.h:173)。
|
||||
pub const KEY_LEFT: i32 = 0xFF51;
|
||||
/// kOfxKey_Up(ofxKeySyms.h:174)。
|
||||
pub const KEY_UP: i32 = 0xFF52;
|
||||
/// kOfxKey_Right(ofxKeySyms.h:175)。
|
||||
pub const KEY_RIGHT: i32 = 0xFF53;
|
||||
/// kOfxKey_Down(ofxKeySyms.h:176)。
|
||||
pub const KEY_DOWN: i32 = 0xFF54;
|
||||
/// kOfxKey_Page_Up(ofxKeySyms.h:178)。
|
||||
pub const KEY_PAGE_UP: i32 = 0xFF55;
|
||||
/// kOfxKey_Page_Down(ofxKeySyms.h:180)。
|
||||
pub const KEY_PAGE_DOWN: i32 = 0xFF56;
|
||||
/// kOfxKey_End(ofxKeySyms.h:181)。
|
||||
pub const KEY_END: i32 = 0xFF57;
|
||||
/// kOfxKey_F1(ofxKeySyms.h:252)。
|
||||
pub const KEY_F1: i32 = 0xFFBE;
|
||||
/// kOfxKey_Shift_L(ofxKeySyms.h:315)。
|
||||
pub const KEY_SHIFT_L: i32 = 0xFFE1;
|
||||
/// kOfxKey_Control_L(ofxKeySyms.h:317)。
|
||||
pub const KEY_CONTROL_L: i32 = 0xFFE3;
|
||||
/// kOfxKey_Alt_L(ofxKeySyms.h:323)。
|
||||
pub const KEY_ALT_L: i32 = 0xFFE9;
|
||||
/// kOfxKey_space(ofxKeySyms.h:331)。
|
||||
pub const KEY_SPACE: i32 = 0x020;
|
||||
/// kOfxKey_a(ofxKeySyms.h:398)。
|
||||
pub const KEY_A: i32 = 0x061;
|
||||
/// kOfxKey_z(ofxKeySyms.h:423)。
|
||||
pub const KEY_Z: i32 = 0x07a;
|
||||
|
||||
/// kOfxPropChangeReason(ofxCore.h:763):instanceChanged 的 inArgs 里
|
||||
/// 说明变更来源(UserEdited / PluginEdited / Time)。
|
||||
pub(crate) const PROP_CHANGE_REASON: &str = "OfxPropChangeReason";
|
||||
/// kOfxChangeUserEdited(ofxCore.h:792)。
|
||||
pub(crate) const CHANGE_USER_EDITED: &str = "OfxChangeUserEdited";
|
||||
/// kOfxChangePluginEdited(ofxCore.h:795)。
|
||||
pub(crate) const CHANGE_PLUGIN_EDITED: &str = "OfxChangePluginEdited";
|
||||
/// kOfxChangeTime(ofxCore.h:798)。
|
||||
pub(crate) const CHANGE_TIME: &str = "OfxChangeTime";
|
||||
|
||||
/// kOfxPropTime(ofxCore.h:613)。
|
||||
pub(crate) const PROP_TIME: &str = "OfxPropTime";
|
||||
/// kOfxPropEffectInstance(ofxCore.h:776):interact/渲染 inArgs 里指向
|
||||
/// 效果实例句柄的指针属性。
|
||||
pub(crate) const PROP_EFFECT_INSTANCE: &str = "OfxPropEffectInstance";
|
||||
/// kOfxImageEffectPropRenderScale。
|
||||
pub(crate) const PROP_RENDER_SCALE: &str = "OfxImageEffectPropRenderScale";
|
||||
/// kOfxImageEffectPropRenderWindow。
|
||||
@@ -341,6 +487,30 @@ pub struct Plugin {
|
||||
unsafe impl Send for Plugin {}
|
||||
unsafe impl Sync for Plugin {}
|
||||
|
||||
/// 插件入口函数类型(`OfxPluginEntryPoint`,ofxCore.h:84)。
|
||||
pub(crate) type EntryPoint = unsafe extern "C" fn(
|
||||
action: *const c_char,
|
||||
handle: *const c_void,
|
||||
in_args: *mut c_void,
|
||||
out_args: *mut c_void,
|
||||
) -> i32;
|
||||
|
||||
/// 插件声明的 overlay interact 入口(ofxImageEffect.h:825/812):
|
||||
/// V2 优先、V1 次之;两者都未声明返回 None。属性值是
|
||||
/// `Pointer→OfxPluginEntryPoint`(宿主预定义,插件 describe 期写入)。
|
||||
pub(crate) fn overlay_interact_entry(props: &PropertySet) -> Option<EntryPoint> {
|
||||
for name in [PROP_OVERLAY_INTERACT_V2, PROP_OVERLAY_INTERACT_V1] {
|
||||
if let Some(Value::Pointer(p)) = props.get(name, 0) {
|
||||
if !p.is_null() {
|
||||
// 属性值是函数指针(void* 存放);转换与 dlsym_fn 同款
|
||||
// (调用方保证类型正确)。
|
||||
return Some(unsafe { std::mem::transmute_copy(&p) });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
/// 调插件的 action。`handle` 视 action 而定(describe 时为
|
||||
/// descriptor,render 时为 instance)。`in_args`/`out_args` 按
|
||||
@@ -364,6 +534,27 @@ impl Plugin {
|
||||
let action = cs(action);
|
||||
unsafe { (self.entry)(action.as_ptr(), handle as *const c_void, in_ptr, out_ptr) }
|
||||
}
|
||||
|
||||
/// 按指定入口函数调用(interact 的 overlay 入口与 main entry 可
|
||||
/// 不同——插件经 kOfxImageEffectPluginPropOverlayInteractV2 声明)。
|
||||
/// 语义同 [`Plugin::call_action`]。
|
||||
///
|
||||
/// # Safety
|
||||
/// `entry` 必须是插件导出的合法 OfxPluginEntryPoint;`handle`/参数
|
||||
/// 集与 action 匹配。
|
||||
pub(crate) unsafe fn call_entry(
|
||||
&self,
|
||||
entry: EntryPoint,
|
||||
action: &str,
|
||||
handle: *mut c_void,
|
||||
in_args: &PropertySet,
|
||||
out_args: &PropertySet,
|
||||
) -> i32 {
|
||||
let in_ptr = in_args as *const PropertySet as *mut c_void;
|
||||
let out_ptr = out_args as *const PropertySet as *mut c_void;
|
||||
let action = cs(action);
|
||||
unsafe { (entry)(action.as_ptr(), handle as *const c_void, in_ptr, out_ptr) }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- PluginCache ---------------------------------------------------------
|
||||
@@ -730,6 +921,11 @@ fn init_descriptor_props(props: &PropertySet, bundle: &Path) {
|
||||
"OfxImageEffectPluginPropOverlayInteractV1",
|
||||
Value::Pointer(std::ptr::null_mut()),
|
||||
);
|
||||
// overlay interact V2(ofxImageEffect.h:825):插件 describe 期声明
|
||||
// 自定义交互入口(Pointer→OfxPluginEntryPoint;V2 要求 Draw suite
|
||||
// 绘制);宿主预定义空指针默认(propSet 不创建属性,宿主预定义
|
||||
// 属性宇宙,与 V1 同款)。
|
||||
props.set_one(PROP_OVERLAY_INTERACT_V2, Value::Pointer(std::ptr::null_mut()));
|
||||
props.set_one("OfxImageEffectPropSupportsMultiResolution", Value::Int(1));
|
||||
props.set_one(PROP_SUPPORTS_TILES, Value::Int(1));
|
||||
props.set_one("OfxImageEffectPropTemporalClipAccess", Value::Int(0));
|
||||
@@ -886,6 +1082,7 @@ impl Host {
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
interact: std::sync::Mutex::new(None),
|
||||
};
|
||||
init_instance_props(&instance.props, &instance);
|
||||
|
||||
@@ -989,6 +1186,9 @@ fn init_host_props(props: &PropertySet) {
|
||||
// GL 能力宣告(M11 §4;ofxGPURender.h "OpenGL House Keeping":
|
||||
// 宿主在描述符置 "true")。
|
||||
props.set_one(PROP_GL_RENDER_SUPPORTED, Value::String(cs("true")));
|
||||
// overlay 能力宣告(ofxImageEffect.h:801):宿主允许插件在输出
|
||||
// 图像上绘制 overlay(interact draw 的前置)。
|
||||
props.set_one(PROP_SUPPORTS_OVERLAYS, Value::Int(1));
|
||||
// ofxColour 能力宣告(M11 §4):OCIO 模式 + native 配置列表。
|
||||
props.set_one(PROP_COLOUR_STYLE, Value::String(cs(COLOUR_STYLE_OCIO)));
|
||||
props.define(
|
||||
|
||||
@@ -122,6 +122,9 @@ pub struct Instance {
|
||||
/// pluginrenderer.cpp:1436-1444 的 instance_lock——渲染路径非
|
||||
/// 线程安全,并发 render 必须互斥)。
|
||||
pub render_lock: std::sync::Mutex<()>,
|
||||
/// 关联的 interact(`new_interact` 创建;与实例同生命周期,
|
||||
/// notify_destroy 时连带销毁)。
|
||||
pub interact: std::sync::Mutex<Option<std::sync::Arc<crate::suites::interact::Interact>>>,
|
||||
}
|
||||
|
||||
/// 实例销毁路径:先通知 destroyInstance action,再摘除 param 回写登记。
|
||||
@@ -641,12 +644,16 @@ impl Instance {
|
||||
/// 要求上下文 current——本实现的约定是 oakrender 的 PluginJob
|
||||
/// 路径在进入前做好),且 `output_texture` 已附着为渲染器输出
|
||||
/// 目标(等价 C++ `PluginRenderer::attach_output_texture`)。
|
||||
/// `output_gl_texture` 为宿主为输出帧建的真实 GL 纹理名(GL 模式
|
||||
/// 下 clipLoadTexture(Output) 的 OpenGLTextureIndex 以它为准);
|
||||
/// None = CPU 回退语义。
|
||||
///
|
||||
/// action 序列:kOfxActionOpenGLContextAttached → render(in args
|
||||
/// 带 kOfxImageEffectPropOpenGLEnabled=1)→
|
||||
/// kOfxActionOpenGLContextDetached(ofxGPURender.h:345-371;
|
||||
/// attach/detach 必须配对)。渲染结果留在 GL 输出纹理上(插件
|
||||
/// 直接画进附着目标),宿主不做 CPU 回读;GL 模式下
|
||||
/// 直接画进附着目标),宿主经 glReadPixels 回读(render 驱动
|
||||
/// 负责,见 [`crate::gl_bridge`]);GL 模式下
|
||||
/// clipGetImage(Output) 不可用(插件按规范走 OpenGL suite——
|
||||
/// ofxGPURender.h "the effect SHOULD access all its images through
|
||||
/// the OpenGL suite")。render 返回前对未释放的输入 GL 纹理做
|
||||
@@ -658,6 +665,7 @@ impl Instance {
|
||||
window: OfxRectD,
|
||||
renderer: crate::render::Renderer,
|
||||
output_texture: crate::render::Texture,
|
||||
output_gl_texture: Option<i32>,
|
||||
) -> crate::error::Result<()> {
|
||||
use crate::host::ACTION_RENDER;
|
||||
|
||||
@@ -682,6 +690,7 @@ impl Instance {
|
||||
renderer,
|
||||
output_texture,
|
||||
gl_pixel_depth,
|
||||
output_gl_texture,
|
||||
}));
|
||||
// GL 模式无 CPU 输出图像(current_output 保持 None)。
|
||||
|
||||
@@ -963,6 +972,143 @@ impl Instance {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// kOfxActionInstanceChanged 的宿主侧分发(ofxCore.h:405-435 的
|
||||
/// inArgs 契约):
|
||||
///
|
||||
/// - kOfxPropType = kOfxTypeParameter(参数值变更)
|
||||
/// - kOfxPropName = 变更的参数名
|
||||
/// - kOfxPropChangeReason = kOfxChange*(UserEdited / PluginEdited /
|
||||
/// Time)
|
||||
/// - kOfxPropTime = 变更发生时的效果时间(Image Effect 插件专属)
|
||||
/// - kOfxImageEffectPropRenderScale = 当前渲染比例
|
||||
///
|
||||
/// 宿主按下 push button 后以 UserEdited 调用它(push button 无值,
|
||||
/// 插件的反应全在 instanceChanged 里;真实插件如 CImg 依赖此动作
|
||||
/// 刷新内部状态)。返回非 OK/ReplyDefault 状态码时为 Err。
|
||||
pub fn instance_changed(
|
||||
&self,
|
||||
param_name: &str,
|
||||
reason: crate::param::ChangeReason,
|
||||
time: f64,
|
||||
scale: RenderScale,
|
||||
) -> crate::error::Result<()> {
|
||||
use crate::host::{
|
||||
ACTION_INSTANCE_CHANGED, CHANGE_PLUGIN_EDITED, CHANGE_TIME, CHANGE_USER_EDITED,
|
||||
PROP_CHANGE_REASON, PROP_RENDER_SCALE, PROP_TIME,
|
||||
};
|
||||
use crate::property::Value;
|
||||
|
||||
let in_args = PropertySet::new();
|
||||
in_args.set_one(
|
||||
crate::param::PROP_TYPE,
|
||||
Value::String(CString::new(crate::param::TYPE_PARAMETER).unwrap()),
|
||||
);
|
||||
in_args.set_one(
|
||||
crate::param::PROP_NAME,
|
||||
Value::String(CString::new(param_name).unwrap()),
|
||||
);
|
||||
let reason_str = match reason {
|
||||
crate::param::ChangeReason::UserEdited => CHANGE_USER_EDITED,
|
||||
crate::param::ChangeReason::PluginEdited => CHANGE_PLUGIN_EDITED,
|
||||
crate::param::ChangeReason::TimeChanged => CHANGE_TIME,
|
||||
};
|
||||
in_args.set_one(
|
||||
PROP_CHANGE_REASON,
|
||||
Value::String(CString::new(reason_str).unwrap()),
|
||||
);
|
||||
in_args.set_one(PROP_TIME, Value::Double(time));
|
||||
in_args.define(
|
||||
PROP_RENDER_SCALE,
|
||||
vec![Value::Double(scale.x), Value::Double(scale.y)],
|
||||
);
|
||||
|
||||
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_INSTANCE_CHANGED, inst_handle, &in_args, &out)
|
||||
};
|
||||
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
|
||||
return Err(crate::error::Error::Failed(format!(
|
||||
"instanceChanged 失败:{stat}"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 创建关联的 interact(主进程 UI 事件宿主;任务契约的
|
||||
/// `new_interact(instance)`)。与 worker 进程里的渲染实例并存——
|
||||
/// OFX 允许同一插件多实例,interact 是独立对象(独立 handle),
|
||||
/// 经 [`crate::suites::interact::Interact::handle`] 与效果实例区分。
|
||||
///
|
||||
/// 流程:插件声明的 overlay interact 入口(V2 优先、V1 次之;
|
||||
/// ofxImageEffect.h:825/812)作为 interact 的入口——未声明则用插件
|
||||
/// main entry(任务契约)→ 建 [`Interact`](属性表 + tagged handle)
|
||||
/// → 向入口发 `kOfxActionNewInteract`。
|
||||
///
|
||||
/// 返回值:
|
||||
/// - `Some(interact)`:创建成功(NewInteract 返回 OK;或插件未实现
|
||||
/// NewInteract 但声明了 overlay interact——真实 overlay 插件不认
|
||||
/// NewInteract,走 describe/create 序列)。
|
||||
/// - `None`:插件无 interact(NewInteract 返回 kOfxStatReplyDefault
|
||||
/// 且未声明 overlay 入口)或返回错误状态。
|
||||
///
|
||||
/// 创建后调用方继续 [`Interact::describe`] → [`Interact::create_instance`]
|
||||
/// 完成实例化;销毁随实例自动连带([`Instance::notify_destroy`])。
|
||||
pub fn new_interact(&self) -> Option<std::sync::Arc<crate::suites::interact::Interact>> {
|
||||
use crate::host::overlay_interact_entry;
|
||||
use crate::suites::interact::Interact;
|
||||
|
||||
let overlay = overlay_interact_entry(&self.plugin.descriptor.props);
|
||||
let entry = overlay.unwrap_or(self.plugin.entry);
|
||||
let has_overlay = overlay.is_some();
|
||||
|
||||
let effect_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let interact = Interact::new(self.plugin.clone(), entry, effect_handle);
|
||||
|
||||
let empty = PropertySet::new();
|
||||
let st = interact.call(crate::host::ACTION_NEW_INTERACT, &empty, &empty);
|
||||
let accepted = match st {
|
||||
crate::suites::status::OK => true,
|
||||
// 插件未实现 NewInteract 但声明了 overlay interact → 仍创建
|
||||
//(走 describe/create 的官方序列)。
|
||||
crate::suites::status::REPLY_DEFAULT if has_overlay => true,
|
||||
_ => false,
|
||||
};
|
||||
if !accepted {
|
||||
return None;
|
||||
}
|
||||
*self
|
||||
.interact
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(interact.clone());
|
||||
Some(interact)
|
||||
}
|
||||
|
||||
/// 已创建的 interact(None = 未创建)。
|
||||
pub fn interact(&self) -> Option<std::sync::Arc<crate::suites::interact::Interact>> {
|
||||
self.interact
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// describe_interact(任务契约):对已创建的 interact 发
|
||||
/// `kOfxActionDescribe`(kOfxActionDescribeInteract)。未创建 →
|
||||
/// kOfxStatErrBadHandle。
|
||||
pub fn describe_interact(&self) -> i32 {
|
||||
match self.interact() {
|
||||
Some(i) => i.describe(),
|
||||
None => crate::suites::status::ERR_BAD_HANDLE,
|
||||
}
|
||||
}
|
||||
|
||||
/// 销毁(destroyInstance action)。析构由 `Arc<RefBox<Instance>>`
|
||||
/// 归零驱动;此处只做 action 通知,幂等([`Instance::drop`] 的
|
||||
/// `destroyed` 门保证只发一次)。
|
||||
@@ -973,6 +1119,17 @@ impl Instance {
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let empty = PropertySet::new();
|
||||
// interact 与实例同生命周期:实例销毁前先销毁 interact
|
||||
//(ofxInteract.h kOfxActionDestroyInstanceInteract 的 \pre 要求
|
||||
// 实例成员尚未销毁)。
|
||||
if let Some(i) = self
|
||||
.interact
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.take()
|
||||
{
|
||||
i.destroy();
|
||||
}
|
||||
// 通知失败只记日志(销毁路径不可回滚)。
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
|
||||
@@ -33,9 +33,11 @@
|
||||
//! 删除:oaknode/oakrender/oakundo 均以 path 依赖直接链接。桥以
|
||||
//! 直接 Rust 类型重建([`node`] 的身份注册表与
|
||||
//! `set_input_*_undoable`;[`render`] 的 `Texture`/`Frame`/`Renderer`
|
||||
//! 值类型),仅 GPU 相关且 wgpu 模型无等价物的调用面保留标注桩
|
||||
//! ([`render::texture_id`]——GL 命名空间不存在,见 `// STUB`
|
||||
//! 标记)。
|
||||
//! 值类型)。GL 纹理名(旧 `oakrender_texture_id` 的 GL 命名空间)
|
||||
//! 由 [`gl_bridge`](方案 B:离屏 CGL 上下文 + 回读)提供;
|
||||
//! [`render::texture_id`] 对 oakrender 纹理保持恒 0(wgpu/Metal 无
|
||||
//! GL 命名空间),use_opengl 决策与 GL suite 的 OpenGLTextureIndex
|
||||
//! 改从 [`gl_bridge`] 取真实名。
|
||||
//!
|
||||
//! ## 句柄纪律(全 crate 最高优先级约定)
|
||||
//!
|
||||
|
||||
@@ -110,15 +110,14 @@ pub fn registered_instance_count() -> usize {
|
||||
/// inspector's button widget).
|
||||
///
|
||||
/// OFX push buttons carry no value: the host's press signal is a single
|
||||
/// set on the parameter (the plugin reacts in its own instanceChanged
|
||||
/// action). Locates the instance and parameter, type-checks, then
|
||||
/// `set_ofx`. Returns false when the instance/parameter is unknown or
|
||||
/// the parameter is not a push button.
|
||||
///
|
||||
/// TODO(instanceChanged): per the OFX contract the press should be
|
||||
/// routed to the plugin as kOfxActionInstanceChanged (UserEdited);
|
||||
/// oakplugin has no dispatcher for that action yet — for now the value
|
||||
/// is only marked, and the plugin-side reaction is future work.
|
||||
/// set on the parameter, then the press is routed to the plugin as
|
||||
/// `kOfxActionInstanceChanged` (UserEdited) so the plugin reacts in its
|
||||
/// own instanceChanged action (the OFX contract, ofxCore.h:405-435;
|
||||
/// real plugins such as CImg depend on that action). Locates the
|
||||
/// instance and parameter, type-checks, marks the value, then dispatches
|
||||
/// the action. Returns false when the instance/parameter is unknown or
|
||||
/// the parameter is not a push button; an instanceChanged failure is
|
||||
/// logged and still reported as a successful press (the value was set).
|
||||
pub fn push_button_clicked(instance: u64, param_name: &str) -> bool {
|
||||
let Some(inst) = instance_from_id(instance) else {
|
||||
return false;
|
||||
@@ -130,6 +129,19 @@ pub fn push_button_clicked(instance: u64, param_name: &str) -> bool {
|
||||
return false;
|
||||
}
|
||||
p.set_ofx(ParamValue::PushButton);
|
||||
// The current render context supplies the time / render scale when the
|
||||
// press happens during a render; outside one, time 0 and scale 1:1.
|
||||
let (time, scale) = crate::suites::render_ctx()
|
||||
.map(|ctx| (ctx.time, ctx.scale))
|
||||
.unwrap_or((0.0, crate::instance::RenderScale { x: 1.0, y: 1.0 }));
|
||||
if let Err(e) = inst.value.instance_changed(
|
||||
param_name,
|
||||
crate::param::ChangeReason::UserEdited,
|
||||
time,
|
||||
scale,
|
||||
) {
|
||||
eprintln!("push_button_clicked: instanceChanged failed for \"{param_name}\": {e}");
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -853,6 +865,10 @@ pub fn install_render_executor() {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Records what the mock plugin entry saw, so the push-button test can
|
||||
/// assert the kOfxActionInstanceChanged routing and its inArgs.
|
||||
static PUSH_ENTRY_CALLS: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
#[test]
|
||||
fn input_type_table_covers_all_ofx_kinds() {
|
||||
assert_eq!(
|
||||
@@ -1000,11 +1016,31 @@ mod tests {
|
||||
use crate::param::{ParamDef, ParamInstance, ParamSetInstance};
|
||||
|
||||
unsafe extern "C" fn dummy_entry(
|
||||
_: *const c_char,
|
||||
action: *const c_char,
|
||||
_: *const c_void,
|
||||
_: *mut c_void,
|
||||
in_args: *mut c_void,
|
||||
_: *mut c_void,
|
||||
) -> i32 {
|
||||
if !action.is_null() {
|
||||
let action = unsafe {
|
||||
std::ffi::CStr::from_ptr(action).to_string_lossy().into_owned()
|
||||
};
|
||||
if action == crate::host::ACTION_INSTANCE_CHANGED {
|
||||
let mut calls = PUSH_ENTRY_CALLS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
calls.push(action);
|
||||
// The instanceChanged inArgs contract (ofxCore.h:405-435):
|
||||
// kOfxPropChangeReason = kOfxChangeUserEdited.
|
||||
let props = unsafe { &*(in_args as *const crate::property::PropertySet) };
|
||||
let reason = props.get(crate::host::PROP_CHANGE_REASON, 0);
|
||||
let reason = match reason {
|
||||
Some(crate::property::Value::String(s)) => {
|
||||
s.to_string_lossy().into_owned()
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
calls.push(reason);
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
let plugin = Arc::new(Plugin {
|
||||
@@ -1039,6 +1075,7 @@ mod tests {
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
interact: std::sync::Mutex::new(None),
|
||||
};
|
||||
register_instance(Arc::new(RefBox {
|
||||
refs: AtomicU32::new(1),
|
||||
@@ -1047,15 +1084,21 @@ mod tests {
|
||||
}
|
||||
|
||||
/// push_button_clicked:实例/参数查无与类型不匹配 → false;命中 →
|
||||
/// true 并触发一次 set。
|
||||
/// true、置 PushButton 值并把按下路由为 kOfxActionInstanceChanged
|
||||
/// (UserEdited,inArgs 带 change reason / name / type / time /
|
||||
/// render scale)。
|
||||
#[test]
|
||||
fn push_button_clicked_requires_a_push_button_param() {
|
||||
fn push_button_clicked_routes_instance_changed() {
|
||||
*PUSH_ENTRY_CALLS.lock().unwrap_or_else(|e| e.into_inner()) = Vec::new();
|
||||
let id = instance_with_push_button();
|
||||
assert!(push_button_clicked(id, "button"));
|
||||
// 类型不匹配 / 查无参数 / 查无实例。
|
||||
// 类型不匹配 / 查无参数 / 查无实例:不触发 entry。
|
||||
assert!(!push_button_clicked(id, "gain"));
|
||||
assert!(!push_button_clicked(id, "nope"));
|
||||
assert!(!push_button_clicked(u64::MAX, "button"));
|
||||
// 只 "button" 那次按下进了插件 entry,且 reason 是 UserEdited。
|
||||
let calls = PUSH_ENTRY_CALLS.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
assert_eq!(calls, vec!["OfxActionInstanceChanged", "OfxChangeUserEdited"]);
|
||||
unregister_instance(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ use std::sync::{Arc, Mutex, OnceLock};
|
||||
pub trait UiProgressReporter: Send {
|
||||
/// 上报进度(0.0..=1.0);false = 取消。
|
||||
fn update(&mut self, progress: f64) -> bool;
|
||||
/// progressEnd 通知(默认 no-op;app 可据此关闭进度 UI / 把完成
|
||||
/// 事件转发过 IPC——worker 侧的进度回传依赖它)。
|
||||
fn end(&mut self) {}
|
||||
}
|
||||
|
||||
/// 报告器工厂:progressStart 携 (label, message) 调用,现造一个
|
||||
@@ -160,4 +163,12 @@ impl ProgressReporter {
|
||||
pub(crate) fn is_cancelled(&self) -> bool {
|
||||
self.cancelled.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// progressEnd 钩子:转发给 UI 报告器(无报告器时 no-op)。
|
||||
pub(crate) fn end_ui(&self) {
|
||||
let mut slot = self.ui.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(ui) = slot.as_mut() {
|
||||
ui.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,11 @@
|
||||
//! - 像素格式常量直接别名 [`oakcore_rs::PixelFormat`]。
|
||||
//!
|
||||
//! 保留桩(GPU 相关、wgpu 模型无直接 Rust 等价物):
|
||||
//! [`texture_id`]——wgpu 后端没有 OpenGL 纹理名(旧 C ABI 的
|
||||
//! `oakrender_texture_id` 语义是 GL 命名空间),恒 0;GL suite 的
|
||||
//! `OpenGLTextureIndex` 属性与 render 驱动的 use_opengl 决策据此
|
||||
//! 回退 CPU 路径。
|
||||
//! [`texture_id`]——oakrender 纹理(wgpu/CPU)没有 OpenGL 纹理名,
|
||||
//! 恒 0。**GL 模式的真实 GL 纹理名来自 [`crate::gl_bridge`]**(方案 B
|
||||
//! 已落地):render 驱动为输出帧建 GL 纹理 + FBO,GL suite 的
|
||||
//! `OpenGLTextureIndex` 属性据此返回真实名;use_opengl 决策改用
|
||||
//! [`crate::gl_bridge::gl_available`] 作"目标纹理有有效 GL 名"的门。
|
||||
|
||||
/// `oakrender_video_params` POD — single-lib unification: aliases the
|
||||
/// oakrender crate's struct (identical layout;
|
||||
@@ -124,13 +125,15 @@ pub fn texture_create(
|
||||
Ok(Texture::wrap_frame(frame))
|
||||
}
|
||||
|
||||
/// STUB: oakrender C ABI deleted (single-lib),且 wgpu 后端没有
|
||||
/// OpenGL 纹理名——旧 `oakrender_texture_id` 的 GL 命名空间语义无
|
||||
/// Rust 等价物。恒 0(GL suite 的 `OpenGLTextureIndex` 属性与
|
||||
/// render 驱动的 use_opengl 决策据此回退 CPU 路径;GPU 上传若落地
|
||||
/// 走 `oakrender::backend::GpuContextLike::upload` 的 wgpu token,
|
||||
/// 不暴露 GL id)。真实化的评估与方案见 [`crate::gl_bridge`]
|
||||
/// (阶段 6a spike:方案 A 不可行,方案 B 暂缓)。
|
||||
/// oakrender 纹理在 GL 命名空间中的纹理名。oakrender 纹理(wgpu
|
||||
/// Metal / CPU 帧)没有 OpenGL 纹理名,恒 0。
|
||||
///
|
||||
/// **GL 模式的真实纹理名不由本函数提供**:宿主自建的离屏 GL 纹理
|
||||
/// (输出帧 + FBO 挂载、输入 clip 上传)由 [`crate::gl_bridge`] 产生,
|
||||
/// 经 GL suite 的 `OpenGLTextureIndex` 属性直接写出;use_opengl 决策
|
||||
/// 用 [`crate::gl_bridge::gl_available`] 当"目标纹理有有效 GL 名"的
|
||||
/// 门(桥能为目标帧建出真实 GL 纹理 ⟺ 可用)。评估历史见
|
||||
/// [`crate::gl_bridge`] 模块文档。
|
||||
pub fn texture_id(_texture: &Texture) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
@@ -47,8 +47,10 @@
|
||||
//! 10. 参数覆盖(pluginrenderer.cpp:132-290 apply_param_overrides);
|
||||
//! 11. render action:CPU 路径经 [`crate::instance::Instance::render`]
|
||||
//! (输出装配:图像 → 目标纹理帧,行跨度感知);GL 路径经
|
||||
//! [`crate::instance::Instance::render_gl`](插件直接画进已附着
|
||||
//! 的输出纹理,无 CPU 回读)。
|
||||
//! [`crate::instance::Instance::render_gl`] + [`crate::gl_bridge`]
|
||||
//! (方案 B:宿主建离屏上下文,插件画进 FBO 附着的输出 GL 纹理,
|
||||
//! render 返回后 glReadPixels 回读装配——输出格式与 CPU 路径一致;
|
||||
//! GL 失败回退 CPU)。
|
||||
//!
|
||||
//! ## begin/end 序列括号
|
||||
//!
|
||||
@@ -112,6 +114,10 @@ pub fn begin_sequence(
|
||||
gl: Option<Renderer>,
|
||||
) -> crate::error::Result<()> {
|
||||
if gl.is_some() {
|
||||
// GL 模式:ofxGPURender.h "OpenGL Current Context" 要求
|
||||
// BeginSequenceRender 期间上下文 current(插件可能在此分配 GL
|
||||
// 资源)。acquire 覆盖整个 action;返回即清 current。
|
||||
let _guard = crate::gl_bridge::acquire()?;
|
||||
inst.begin_sequence_render_gl(range)
|
||||
} else {
|
||||
inst.begin_sequence_render(range)
|
||||
@@ -125,6 +131,7 @@ pub fn end_sequence(
|
||||
gl: Option<Renderer>,
|
||||
) -> crate::error::Result<()> {
|
||||
if gl.is_some() {
|
||||
let _guard = crate::gl_bridge::acquire()?;
|
||||
inst.end_sequence_render_gl(range)
|
||||
} else {
|
||||
inst.end_sequence_render(range)
|
||||
@@ -153,18 +160,18 @@ pub fn render_frame(
|
||||
}
|
||||
|
||||
// 2. use_opengl(pluginrenderer.cpp:1446-1457):插件声明 GL 支持
|
||||
// 且渲染器是 OpenGL 且目标纹理有 GL id 且像素深度协商可行
|
||||
// (管线 F32 满足插件 kOfxOpenGLPropPixelDepth 声明)。
|
||||
// `texture_id` 为桩恒 0(wgpu 无 GL 命名空间)→ 本决策恒回退
|
||||
// CPU 路径,GL 分支保留给 GL 后端落地。
|
||||
// 且渲染器是 OpenGL 且像素深度协商可行(管线 F32 满足插件
|
||||
// kOfxOpenGLPropPixelDepth 声明)且目标纹理有有效 GL 名(= 桥能为
|
||||
// 目标帧建出真实 GL 纹理 ⟺ 离屏上下文可用,[`crate::gl_bridge`])。
|
||||
// 任一不满足 → 回退 CPU 路径(GL 分支在下方真正渲染)。
|
||||
let use_opengl = match job.renderer.as_ref() {
|
||||
Some(r) if render::renderer_is_open_gl(r) => {
|
||||
let plugin_gl = plugin_supports_opengl(inst);
|
||||
let depth_ok =
|
||||
crate::suites::gl_render::pick_gl_pixel_depth(&inst.plugin.descriptor.props)
|
||||
.is_some();
|
||||
let dst_id = render::texture_id(&job.dst);
|
||||
plugin_gl && depth_ok && dst_id != 0
|
||||
let gl_name_ok = crate::gl_bridge::gl_available();
|
||||
plugin_gl && depth_ok && gl_name_ok
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
@@ -276,20 +283,103 @@ pub fn render_frame(
|
||||
// 输出装配(pluginrenderer.cpp:1762-1834 的 CPU 路径)。
|
||||
write_output_frame(&mut dst, &output)?;
|
||||
} else {
|
||||
// GL 路径:插件直接画进已附着的输出纹理(
|
||||
// pluginrenderer.cpp:1784-1834 的 GL 分支);无 CPU 回读。
|
||||
inst.render_gl(
|
||||
job.time,
|
||||
// GL 路径(方案 B,见 [`crate::gl_bridge`]):宿主自建离屏
|
||||
// 上下文,为输出帧建 GL 纹理 + FBO(插件直接画进附着的输出
|
||||
// 纹理,等价 C++ attach_output_texture),render 返回后
|
||||
// glReadPixels 回读装帧(pluginrenderer.cpp:1784-1834 的 GL
|
||||
// 分支 + 本实现的回读装配)。GL 失败回退 CPU(对齐现有失败
|
||||
// 语义;最终失败仍上抛)。
|
||||
match render_gl_frame(
|
||||
inst,
|
||||
job,
|
||||
RenderScale { x: 1.0, y: 1.0 },
|
||||
render_window,
|
||||
job.renderer.clone().unwrap(),
|
||||
dst.clone(),
|
||||
)?;
|
||||
w,
|
||||
h,
|
||||
&dst_params,
|
||||
) {
|
||||
Ok(image) => write_output_frame(&mut dst, &image)?,
|
||||
Err(gl_err) => {
|
||||
eprintln!("[PLUGIN] GL 渲染失败,回退 CPU:{gl_err}");
|
||||
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(),
|
||||
)?;
|
||||
write_output_frame(&mut dst, &output)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((dst, zip_rois(inst, &rois)))
|
||||
}
|
||||
|
||||
/// GL 渲染一帧(render_frame 的 GL 分支主体;返回回读装配完成的
|
||||
/// F32 RGBA 图像)。
|
||||
///
|
||||
/// 流程:acquire 离屏上下文(本线程 current,全局串行)→ 建输出 GL
|
||||
/// 纹理(尺寸 = 目标帧,格式按 `dst_params`)→ FBO 挂载并绑定 →
|
||||
/// 视口 → [`Instance::render_gl`](插件画进附着纹理;真实纹理名经
|
||||
/// GlCtx 注入,clipLoadTexture(Output) 返回它)→ glReadPixels 回读
|
||||
/// 装配(垂直翻转 + 格式转换)→ 清理 FBO/纹理。任何一步失败返回
|
||||
/// Err,调用方回退 CPU。
|
||||
fn render_gl_frame(
|
||||
inst: &Instance,
|
||||
job: &RenderJob,
|
||||
scale: RenderScale,
|
||||
window: OfxRectD,
|
||||
w: f64,
|
||||
h: f64,
|
||||
dst_params: &render::VideoParams,
|
||||
) -> crate::error::Result<std::sync::Arc<Image>> {
|
||||
use crate::error::Error;
|
||||
|
||||
// 离屏上下文(进程级共享;本线程 current 直到 guard drop)。
|
||||
let _guard = crate::gl_bridge::acquire()?;
|
||||
let (wpx, hpx) = (w as i32, h as i32);
|
||||
// 输出 GL 纹理 + FBO 挂载(GL_RGBA32F/GL_RGBA8 按目标帧格式)。
|
||||
let gl_tex = crate::gl_bridge::create_output_texture(wpx, hpx, dst_params)?;
|
||||
let fbo = crate::gl_bridge::create_fbo(gl_tex, wpx, hpx)?;
|
||||
crate::gl_bridge::bind_fbo(fbo);
|
||||
crate::gl_bridge::set_viewport(wpx, hpx);
|
||||
|
||||
// render action(OpenGLEnabled=1;输出纹理真实名经 GlCtx 注入)。
|
||||
let render_res = inst.render_gl(
|
||||
job.time,
|
||||
scale,
|
||||
window,
|
||||
job.renderer.clone().unwrap(),
|
||||
job.dst.clone(),
|
||||
Some(gl_tex),
|
||||
);
|
||||
|
||||
// 回读前防御性重绑 FBO + 视口(规范下插件不解除输出绑定,但个别
|
||||
// 插件可能改绑/改视口——重绑保证 glReadPixels 读的是输出纹理)。
|
||||
crate::gl_bridge::bind_fbo(fbo);
|
||||
crate::gl_bridge::set_viewport(wpx, hpx);
|
||||
|
||||
// 回读(FBO 仍绑定、输出 GL 纹理仍存活)。
|
||||
let readback = crate::gl_bridge::read_pixels_to_image(wpx, hpx, dst_params);
|
||||
|
||||
// 清理(guard drop 时清 current 并放锁)。
|
||||
crate::gl_bridge::delete_fbo(fbo);
|
||||
crate::gl_bridge::delete_gl_texture(gl_tex);
|
||||
|
||||
render_res?;
|
||||
readback.map_err(|e| Error::Failed(format!("GL 回读失败:{e}")))
|
||||
}
|
||||
|
||||
/// 把输入 clip 名与 RoI 列表配对(与 `clips` 顺序一致)。
|
||||
fn zip_rois(inst: &Instance, rois: &[OfxRectD]) -> Vec<(String, OfxRectD)> {
|
||||
inst.clips
|
||||
|
||||
@@ -0,0 +1,795 @@
|
||||
// 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/>.
|
||||
|
||||
//! OfxDrawSuiteV1(OFX 1.5,vendored ofxDrawSuite.h):宿主绘制套件。
|
||||
//!
|
||||
//! 插件在 `kOfxInteractActionDraw` 内经 `kOfxInteractPropDrawContext`
|
||||
//! 取到绘制上下文(本模块的 [`DrawContext`]),再调 suite 函数画图:
|
||||
//! getColour / setColour / setLineWidth / setLineStipple / draw /
|
||||
//! drawText。
|
||||
//!
|
||||
//! ## 上下文模型
|
||||
//!
|
||||
//! - [`DrawContext`] 在每次 draw action 前由宿主创建、经存活表
|
||||
//! [`LIVE_CONTEXTS`] 注册(句柄 = 堆地址;对应 [`crate::suites::tag`]
|
||||
//! 的句柄纪律),draw 返回后摘除。插件只在 draw action 内持有上下文
|
||||
//! ——ofxDrawSuite.h 各函数文档注明 "failure, e.g. if function is
|
||||
//! called outside kOfxInteractActionDraw" → 存活表查不到即
|
||||
//! kOfxStatFailed。
|
||||
//! - 状态(colour / lineWidth / stipple / viewport / pixelScale)保存在
|
||||
//! [`DrawContext`],draw action 之间不共享。
|
||||
//! - GL 命令要求上下文 current:宿主在调 `kOfxInteractActionDraw` 前经
|
||||
//! [`crate::gl_bridge::acquire`] 保持 current(draw action 全期)。
|
||||
//!
|
||||
//! ## 绘制实现(真实 GL)
|
||||
//!
|
||||
//! gl_bridge 的 CGL 上下文是 **3.2 core profile**(无固定管线,
|
||||
//! glBegin 不可用)——本套件的 draw 用最小着色器 + VAO/VBO 真实渲染:
|
||||
//! 正交投影把 canonical 坐标映射到 NDC(canonical 宽 = viewport /
|
||||
//! pixelScale),非不透明色按 "over" 合成(ofxDrawSuite.h setColour
|
||||
//! 文档要求)。非 macOS 无 GL 桥 → kOfxStatFailed。
|
||||
//!
|
||||
//! ## drawText
|
||||
//!
|
||||
//! 本宿主无字体光栅化器(GL 内画字形需要字库/曲线上采样,超出 Phase
|
||||
//! 1 范围),如实返回 kOfxStatErrUnsupported,不假装画了字。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, c_float, c_int, c_void, CStr};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use crate::suites::status;
|
||||
|
||||
/// `OfxStandardColour`(ofxDrawSuite.h:37-46)枚举值。
|
||||
pub mod std_colour {
|
||||
/// kOfxStandardColourOverlayBackground。
|
||||
pub const BACKGROUND: i32 = 0;
|
||||
/// kOfxStandardColourOverlayActive。
|
||||
pub const ACTIVE: i32 = 1;
|
||||
/// kOfxStandardColourOverlaySelected。
|
||||
pub const SELECTED: i32 = 2;
|
||||
/// kOfxStandardColourOverlayDeselected。
|
||||
pub const DESELECTED: i32 = 3;
|
||||
/// kOfxStandardColourOverlayMarqueeFG。
|
||||
pub const MARQUEE_FG: i32 = 4;
|
||||
/// kOfxStandardColourOverlayMarqueeBG。
|
||||
pub const MARQUEE_BG: i32 = 5;
|
||||
/// kOfxStandardColourOverlayText。
|
||||
pub const TEXT: i32 = 6;
|
||||
}
|
||||
|
||||
/// `OfxDrawLineStipplePattern`(ofxDrawSuite.h:49-56)枚举值。
|
||||
pub mod stipple {
|
||||
/// kOfxDrawLineStipplePatternSolid。
|
||||
pub const SOLID: i32 = 0;
|
||||
/// kOfxDrawLineStipplePatternDot。
|
||||
pub const DOT: i32 = 1;
|
||||
/// kOfxDrawLineStipplePatternDash。
|
||||
pub const DASH: i32 = 2;
|
||||
/// kOfxDrawLineStipplePatternAltDash。
|
||||
pub const ALT_DASH: i32 = 3;
|
||||
/// kOfxDrawLineStipplePatternDotDash。
|
||||
pub const DOT_DASH: i32 = 4;
|
||||
}
|
||||
|
||||
/// `OfxDrawPrimitive`(ofxDrawSuite.h:60-68)枚举值。
|
||||
pub mod primitive {
|
||||
/// kOfxDrawPrimitiveLines(n 点画 n/2 条独立线段)。
|
||||
pub const LINES: i32 = 0;
|
||||
/// kOfxDrawPrimitiveLineStrip。
|
||||
pub const LINE_STRIP: i32 = 1;
|
||||
/// kOfxDrawPrimitiveLineLoop。
|
||||
pub const LINE_LOOP: i32 = 2;
|
||||
/// kOfxDrawPrimitiveRectangle(轴对齐实心矩形,2 对角点)。
|
||||
pub const RECTANGLE: i32 = 3;
|
||||
/// kOfxDrawPrimitivePolygon(实心 n 边形)。
|
||||
pub const POLYGON: i32 = 4;
|
||||
/// kOfxDrawPrimitiveEllipse(2 对角点包围盒内的轴对齐椭圆**线框**)。
|
||||
pub const ELLIPSE: i32 = 5;
|
||||
}
|
||||
|
||||
/// `OfxRGBAColourF`(ofxPixels.h):Draw suite 的颜色值类型。
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OfxRGBAColourF {
|
||||
/// 红。
|
||||
pub r: f32,
|
||||
/// 绿。
|
||||
pub g: f32,
|
||||
/// 蓝。
|
||||
pub b: f32,
|
||||
/// alpha。
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
/// `OfxPointD`(ofxCore.h:819):canonical 坐标点。
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OfxPointD {
|
||||
/// x。
|
||||
pub x: f64,
|
||||
/// y。
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
/// Draw suite 上下文:一次 draw action 期间插件的绘制状态。
|
||||
///
|
||||
/// 宿主在 [`crate::suites::interact::Interact::draw`] 内创建并注册,
|
||||
/// draw 返回后摘除。`viewport`/`pixel_scale` 在创建时快照(canonical
|
||||
/// → NDC 正交投影用);颜色/线宽/虚线样式由插件经 suite 函数写入。
|
||||
pub struct DrawContext {
|
||||
/// 当前绘制颜色(RGBA;setColour 写入,draw 时作为着色器 uniform)。
|
||||
pub colour: [f32; 4],
|
||||
/// 当前线宽(setLineWidth;GL 线绘制 glLineWidth)。
|
||||
pub line_width: f32,
|
||||
/// 当前虚线样式(setLineStipple;`stipple::*` 枚举)。
|
||||
pub stipple: i32,
|
||||
/// 视口尺寸(像素;创建时快照)。
|
||||
pub viewport: (f64, f64),
|
||||
/// canonical→屏幕像素比例(创建时快照)。
|
||||
pub pixel_scale: (f64, f64),
|
||||
}
|
||||
|
||||
impl DrawContext {
|
||||
/// 新上下文(默认颜色不透明黑、线宽 1、实线)。
|
||||
pub fn new(viewport: (f64, f64), pixel_scale: (f64, f64)) -> Self {
|
||||
Self {
|
||||
colour: [0.0, 0.0, 0.0, 1.0],
|
||||
line_width: 1.0,
|
||||
stipple: stipple::SOLID,
|
||||
viewport,
|
||||
pixel_scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 存活绘制上下文表:draw action 期间注册(地址 → 盒值)。
|
||||
/// 插件只在 draw 内持有句柄,draw 返回即摘除(同
|
||||
/// [`crate::suites::gl_render`] 的 LIVE_TEXTURES 模式)。
|
||||
static LIVE_CONTEXTS: LazyLock<Mutex<HashMap<usize, Box<DrawContext>>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
fn lock() -> std::sync::MutexGuard<'static, HashMap<usize, Box<DrawContext>>> {
|
||||
LIVE_CONTEXTS.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// 注册上下文并返回句柄(宿主 draw action 调用;句柄写入 inArgs 的
|
||||
/// kOfxInteractPropDrawContext)。
|
||||
pub(crate) fn make_context(viewport: (f64, f64), pixel_scale: (f64, f64)) -> *mut c_void {
|
||||
let ctx = Box::new(DrawContext::new(viewport, pixel_scale));
|
||||
let addr = &*ctx as *const DrawContext as usize;
|
||||
lock().insert(addr, ctx);
|
||||
addr as *mut c_void
|
||||
}
|
||||
|
||||
/// 摘除上下文(draw action 返回后调用;后续 suite 调用 → Failed)。
|
||||
pub(crate) fn drop_context(handle: *mut c_void) {
|
||||
lock().remove(&(handle as usize));
|
||||
}
|
||||
|
||||
/// 公共入口模板: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),
|
||||
)
|
||||
}
|
||||
|
||||
/// 上下文句柄解析(存活表反查;draw action 外 → Failed,ofxDrawSuite.h
|
||||
/// 的 "outside kOfxInteractActionDraw" 语义)。
|
||||
fn resolve(handle: *mut c_void) -> Result<&'static DrawContext, c_int> {
|
||||
if handle.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
let addr = handle as usize;
|
||||
lock()
|
||||
.get(&addr)
|
||||
.map(|b| unsafe { &*(&**b as *const DrawContext) })
|
||||
.ok_or(status::FAILED)
|
||||
}
|
||||
|
||||
/// 可变访问(resolve 的写路径;draw 期单线程,存活保证同 resolve)。
|
||||
fn resolve_mut(handle: *mut c_void) -> Result<(), c_int> {
|
||||
resolve(handle).map(|_| ())
|
||||
}
|
||||
|
||||
// ---- 宿主标准颜色板(getColour)----
|
||||
|
||||
/// 宿主标准颜色板:按 `OfxStandardColour` 枚举序(ofxDrawSuite.h:37-46)。
|
||||
const PALETTE: [[f32; 4]; 7] = [
|
||||
// BACKGROUND:宿主视口背景(本宿主 Phase 1 无合成背景,黑)。
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
// ACTIVE:进行中/激活的 overlay 元素。
|
||||
[1.0, 0.84, 0.0, 1.0],
|
||||
// SELECTED:选中元素。
|
||||
[0.0, 1.0, 0.0, 1.0],
|
||||
// DESELECTED:未选中元素。
|
||||
[1.0, 1.0, 1.0, 0.5],
|
||||
// MARQUEE_FG:框选前景。
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
// MARQUEE_BG:框选背景。
|
||||
[0.0, 0.0, 0.0, 0.5],
|
||||
// TEXT:overlay 文本。
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
];
|
||||
|
||||
unsafe extern "C" fn get_colour(
|
||||
ctx: *mut c_void,
|
||||
std_colour: c_int,
|
||||
out: *mut OfxRGBAColourF,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let _ = resolve(ctx)?;
|
||||
let c = PALETTE
|
||||
.get(std_colour as usize)
|
||||
.ok_or(status::ERR_VALUE)?;
|
||||
unsafe {
|
||||
*out = OfxRGBAColourF {
|
||||
r: c[0],
|
||||
g: c[1],
|
||||
b: c[2],
|
||||
a: c[3],
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn set_colour(ctx: *mut c_void, colour: *const OfxRGBAColourF) -> c_int {
|
||||
caught(|| {
|
||||
if colour.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let _ = resolve_mut(ctx)?;
|
||||
let v = unsafe { &*colour };
|
||||
let addr = ctx as usize;
|
||||
let mut live = lock();
|
||||
let slot = live.get_mut(&addr).expect("resolve 已保证存活");
|
||||
slot.colour = [v.r, v.g, v.b, v.a];
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn set_line_width(ctx: *mut c_void, width: c_float) -> c_int {
|
||||
caught(|| {
|
||||
let _ = resolve_mut(ctx)?;
|
||||
let addr = ctx as usize;
|
||||
let mut live = lock();
|
||||
let slot = live.get_mut(&addr).expect("resolve 已保证存活");
|
||||
slot.line_width = width;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn set_line_stipple(ctx: *mut c_void, pattern: c_int) -> c_int {
|
||||
caught(|| {
|
||||
if !(0..=stipple::DOT_DASH).contains(&pattern) {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let _ = resolve_mut(ctx)?;
|
||||
let addr = ctx as usize;
|
||||
let mut live = lock();
|
||||
let slot = live.get_mut(&addr).expect("resolve 已保证存活");
|
||||
slot.stipple = pattern;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn draw(
|
||||
ctx: *mut c_void,
|
||||
prim: c_int,
|
||||
points: *const OfxPointD,
|
||||
count: c_int,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if !(0..=primitive::ELLIPSE).contains(&prim) {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
if count < 0 || (count > 0 && points.is_null()) {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
// 每原语的合法点数(ofxDrawSuite.h draw 文档)。
|
||||
let valid = match prim {
|
||||
primitive::LINES => count >= 2 && count % 2 == 0,
|
||||
primitive::LINE_STRIP | primitive::LINE_LOOP => count >= 2,
|
||||
primitive::RECTANGLE | primitive::ELLIPSE => count == 2,
|
||||
primitive::POLYGON => count >= 3,
|
||||
_ => false,
|
||||
};
|
||||
if !valid {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let c = resolve(ctx)?;
|
||||
let pts: Vec<OfxPointD> = (0..count as usize)
|
||||
.map(|i| unsafe { *points.add(i) })
|
||||
.collect();
|
||||
// 真实 GL 渲染(macOS 3.2 core;非 macOS stub → Failed)。
|
||||
gl_imp::render(c, prim, &pts)
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn draw_text(
|
||||
ctx: *mut c_void,
|
||||
text: *const c_char,
|
||||
pos: *const OfxPointD,
|
||||
_alignment: c_int,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if text.is_null() || pos.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let _ = resolve(ctx)?;
|
||||
// 确认文本有效(UTF-8);随后如实拒绝(无字体光栅化器)。
|
||||
let _ = unsafe { CStr::from_ptr(text) }
|
||||
.to_str()
|
||||
.map_err(|_| status::ERR_VALUE)?;
|
||||
Err(status::ERR_UNSUPPORTED)
|
||||
})
|
||||
}
|
||||
|
||||
/// 函数表布局(与 SDK `OfxDrawSuiteV1` 逐字段一致;ofxDrawSuite.h:85-177)。
|
||||
#[repr(C)]
|
||||
pub struct DrawSuiteV1 {
|
||||
/// getColour:宿主标准颜色板取色。
|
||||
pub get_colour: unsafe extern "C" fn(*mut c_void, c_int, *mut OfxRGBAColourF) -> c_int,
|
||||
/// setColour:设置后续绘制颜色。
|
||||
pub set_colour: unsafe extern "C" fn(*mut c_void, *const OfxRGBAColourF) -> c_int,
|
||||
/// setLineWidth:设置后续线宽。
|
||||
pub set_line_width: unsafe extern "C" fn(*mut c_void, c_float) -> c_int,
|
||||
/// setLineStipple:设置后续虚线样式。
|
||||
pub set_line_stipple: unsafe extern "C" fn(*mut c_void, c_int) -> c_int,
|
||||
/// draw:绘制原语(点数组,canonical 坐标)。
|
||||
pub draw: unsafe extern "C" fn(*mut c_void, c_int, *const OfxPointD, c_int) -> c_int,
|
||||
/// drawText:绘制文本(Phase 1 返回 kOfxStatErrUnsupported)。
|
||||
pub draw_text: unsafe extern "C" fn(*mut c_void, *const c_char, *const OfxPointD, c_int) -> c_int,
|
||||
}
|
||||
|
||||
/// 函数表实例。
|
||||
pub fn suite_v1() -> &'static DrawSuiteV1 {
|
||||
static SUITE: std::sync::OnceLock<DrawSuiteV1> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| DrawSuiteV1 {
|
||||
get_colour,
|
||||
set_colour,
|
||||
set_line_width,
|
||||
set_line_stipple,
|
||||
draw,
|
||||
draw_text,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 标准颜色板:getColour 按枚举取宿主色板(越界 → ErrValue)。
|
||||
#[test]
|
||||
fn get_colour_palette() {
|
||||
let ctx = make_context((640.0, 480.0), (1.0, 1.0));
|
||||
let mut out = OfxRGBAColourF { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
|
||||
unsafe {
|
||||
assert_eq!(get_colour(ctx, std_colour::ACTIVE, &mut out), status::OK);
|
||||
}
|
||||
assert_eq!((out.r, out.g, out.b, out.a), (1.0, 0.84, 0.0, 1.0));
|
||||
// 越界枚举 → ErrValue。
|
||||
unsafe {
|
||||
assert_eq!(get_colour(ctx, 99, &mut out), status::ERR_VALUE);
|
||||
assert_eq!(get_colour(ctx, -1, &mut out), status::ERR_VALUE);
|
||||
// 空输出指针 → ErrValue。
|
||||
assert_eq!(
|
||||
get_colour(ctx, std_colour::BACKGROUND, std::ptr::null_mut()),
|
||||
status::ERR_VALUE
|
||||
);
|
||||
}
|
||||
drop_context(ctx);
|
||||
}
|
||||
|
||||
/// setColour 真实写入状态:再 setLineWidth/setLineStipple 后从存活
|
||||
/// 表读回(draw action 期插件视图)。
|
||||
#[test]
|
||||
fn set_colour_and_line_state() {
|
||||
let ctx = make_context((100.0, 100.0), (2.0, 2.0));
|
||||
let col = OfxRGBAColourF { r: 0.9, g: 0.1, b: 0.2, a: 1.0 };
|
||||
unsafe {
|
||||
assert_eq!(set_colour(ctx, &col), status::OK);
|
||||
assert_eq!(set_line_width(ctx, 3.5), status::OK);
|
||||
assert_eq!(set_line_stipple(ctx, stipple::DASH), status::OK);
|
||||
assert_eq!(set_line_stipple(ctx, 99), status::ERR_VALUE);
|
||||
}
|
||||
|
||||
let addr = ctx as usize;
|
||||
let live = lock();
|
||||
let c = live.get(&addr).unwrap();
|
||||
assert_eq!(c.colour, [0.9, 0.1, 0.2, 1.0]);
|
||||
assert_eq!(c.line_width, 3.5);
|
||||
assert_eq!(c.stipple, stipple::DASH);
|
||||
drop(live);
|
||||
drop_context(ctx);
|
||||
}
|
||||
|
||||
/// 摘除后调用 → Failed("outside kOfxInteractActionDraw" 语义)。
|
||||
#[test]
|
||||
fn draw_suite_outside_draw_fails() {
|
||||
let ctx = make_context((10.0, 10.0), (1.0, 1.0));
|
||||
drop_context(ctx);
|
||||
let col = OfxRGBAColourF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
|
||||
let mut out = OfxRGBAColourF { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
|
||||
let s = std::ffi::CString::new("x").unwrap();
|
||||
let pos = OfxPointD { x: 0.0, y: 0.0 };
|
||||
let pts = [OfxPointD { x: 0.0, y: 0.0 }, OfxPointD { x: 10.0, y: 10.0 }];
|
||||
unsafe {
|
||||
assert_eq!(set_colour(ctx, &col), status::FAILED);
|
||||
assert_eq!(get_colour(ctx, std_colour::TEXT, &mut out), status::FAILED);
|
||||
assert_eq!(set_line_width(ctx, 1.0), status::FAILED);
|
||||
assert_eq!(set_line_stipple(ctx, stipple::SOLID), status::FAILED);
|
||||
// 合法参数(避开空指针检查)→ 摘除后 resolve 失败 → Failed。
|
||||
assert_eq!(draw(ctx, primitive::RECTANGLE, pts.as_ptr(), 2), status::FAILED);
|
||||
// drawText 同样 Failed(先 resolve 失败)。
|
||||
assert_eq!(draw_text(ctx, s.as_ptr(), &pos, 0), status::FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/// draw 原语参数校验(不触 GL):非法原语/点数 → ErrValue。
|
||||
#[test]
|
||||
fn draw_primitive_argument_validation() {
|
||||
let ctx = make_context((10.0, 10.0), (1.0, 1.0));
|
||||
let pts = [OfxPointD { x: 0.0, y: 0.0 }, OfxPointD { x: 10.0, y: 10.0 }];
|
||||
unsafe {
|
||||
// 非法原语枚举。
|
||||
assert_eq!(draw(ctx, 99, pts.as_ptr(), 2), status::ERR_VALUE);
|
||||
assert_eq!(draw(ctx, -1, pts.as_ptr(), 2), status::ERR_VALUE);
|
||||
// 点数不合法:LINES 奇数点。
|
||||
assert_eq!(draw(ctx, primitive::LINES, pts.as_ptr(), 3), status::ERR_VALUE);
|
||||
// RECTANGLE 需恰好 2 点。
|
||||
assert_eq!(draw(ctx, primitive::RECTANGLE, pts.as_ptr(), 3), status::ERR_VALUE);
|
||||
// POLYGON 需 ≥3 点。
|
||||
assert_eq!(draw(ctx, primitive::POLYGON, pts.as_ptr(), 2), status::ERR_VALUE);
|
||||
// 空指针 + 正点数。
|
||||
assert_eq!(draw(ctx, primitive::LINES, std::ptr::null(), 2), status::ERR_VALUE);
|
||||
// 合法参数:环境相关的 GL 路径(无 current 上下文时宿主可能
|
||||
// 失败)——参数校验已过即可,具体状态不在此断言。
|
||||
let st = draw(ctx, primitive::RECTANGLE, pts.as_ptr(), 2);
|
||||
assert!(
|
||||
st == status::OK || st == status::FAILED,
|
||||
"合法参数应 OK(有 GL)或 Failed(无 GL 上下文),got {st}"
|
||||
);
|
||||
}
|
||||
drop_context(ctx);
|
||||
}
|
||||
|
||||
/// drawText:有效参数 → Unsupported(如实拒绝);空指针 → ErrValue。
|
||||
#[test]
|
||||
fn draw_text_is_honest_unsupported() {
|
||||
let ctx = make_context((10.0, 10.0), (1.0, 1.0));
|
||||
let s = std::ffi::CString::new("hello").unwrap();
|
||||
let pos = OfxPointD { x: 1.0, y: 2.0 };
|
||||
unsafe {
|
||||
assert_eq!(draw_text(ctx, s.as_ptr(), &pos, 0), status::ERR_UNSUPPORTED);
|
||||
assert_eq!(draw_text(ctx, std::ptr::null(), &pos, 0), status::ERR_VALUE);
|
||||
}
|
||||
drop_context(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- GL 绘制实现(平台分派)--------------------------------------------
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod gl_imp {
|
||||
use super::{DrawContext, OfxPointD};
|
||||
use crate::suites::status;
|
||||
use std::ffi::c_void;
|
||||
|
||||
// ---- GL 常量(GL 规范值)----
|
||||
const GL_FALSE: i32 = 0;
|
||||
const GL_FLOAT: u32 = 0x1406;
|
||||
const GL_ARRAY_BUFFER: u32 = 0x8892;
|
||||
const GL_STATIC_DRAW: u32 = 0x88E4;
|
||||
const GL_FRAGMENT_SHADER: u32 = 0x8B30;
|
||||
const GL_VERTEX_SHADER: u32 = 0x8B31;
|
||||
const GL_COMPILE_STATUS: u32 = 0x8B81;
|
||||
const GL_LINK_STATUS: u32 = 0x8B82;
|
||||
const GL_TRIANGLES: u32 = 0x0004;
|
||||
const GL_LINES: u32 = 0x0001;
|
||||
const GL_LINE_STRIP: u32 = 0x0003;
|
||||
const GL_LINE_LOOP: u32 = 0x0002;
|
||||
const GL_BLEND: u32 = 0x0BE2;
|
||||
const GL_SRC_ALPHA: u32 = 0x0302;
|
||||
const GL_ONE_MINUS_SRC_ALPHA: u32 = 0x0303;
|
||||
const GL_NO_ERROR: u32 = 0;
|
||||
|
||||
// # Safety: 全部是 OpenGL.framework 导出函数;参数语义见各函数
|
||||
// 注释。调用前提:GL 上下文 current(宿主 draw action 已 acquire)。
|
||||
#[link(name = "OpenGL", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
fn glGenVertexArrays(n: i32, arrays: *mut u32);
|
||||
fn glBindVertexArray(array: u32);
|
||||
fn glGenBuffers(n: i32, buffers: *mut u32);
|
||||
fn glBindBuffer(target: u32, buffer: u32);
|
||||
fn glBufferData(target: u32, size: isize, data: *const c_void, usage: u32);
|
||||
fn glCreateShader(type_: u32) -> u32;
|
||||
fn glShaderSource(shader: u32, count: i32, string: *const *const i8, length: *const i32);
|
||||
fn glCompileShader(shader: u32);
|
||||
fn glGetShaderiv(shader: u32, pname: u32, params: *mut i32);
|
||||
fn glDeleteShader(shader: u32);
|
||||
fn glCreateProgram() -> u32;
|
||||
fn glAttachShader(program: u32, shader: u32);
|
||||
fn glLinkProgram(program: u32);
|
||||
fn glGetProgramiv(program: u32, pname: u32, params: *mut i32);
|
||||
fn glUseProgram(program: u32);
|
||||
fn glGetAttribLocation(program: u32, name: *const i8) -> i32;
|
||||
fn glVertexAttribPointer(
|
||||
index: u32,
|
||||
size: i32,
|
||||
type_: u32,
|
||||
normalized: u8,
|
||||
stride: i32,
|
||||
pointer: *const c_void,
|
||||
);
|
||||
fn glEnableVertexAttribArray(index: u32);
|
||||
fn glGetUniformLocation(program: u32, name: *const i8) -> i32;
|
||||
fn glUniformMatrix4fv(location: i32, count: i32, transpose: u8, value: *const f32);
|
||||
fn glUniform4f(location: i32, x: f32, y: f32, z: f32, w: f32);
|
||||
fn glDrawArrays(mode: u32, first: i32, count: i32);
|
||||
fn glLineWidth(width: f32);
|
||||
fn glEnable(cap: u32);
|
||||
fn glBlendFunc(sfactor: u32, dfactor: u32);
|
||||
fn glGetError() -> u32;
|
||||
}
|
||||
|
||||
/// 顶点着色器(canonical 坐标 + 颜色 uniform;投影映射到 NDC)。
|
||||
const VERT_SRC: &[u8] = b"#version 150\n\
|
||||
in vec2 a_pos;\n\
|
||||
uniform mat4 u_proj;\n\
|
||||
uniform vec4 u_col;\n\
|
||||
out vec4 v_col;\n\
|
||||
void main() {\n\
|
||||
v_col = u_col;\n\
|
||||
gl_Position = u_proj * vec4(a_pos, 0.0, 1.0);\n\
|
||||
}\n\0";
|
||||
|
||||
const FRAG_SRC: &[u8] = b"#version 150\n\
|
||||
in vec4 v_col;\n\
|
||||
out vec4 frag_color;\n\
|
||||
void main() { frag_color = v_col; }\n\0";
|
||||
|
||||
fn compile_shader(kind: u32, src: &[u8]) -> Result<u32, i32> {
|
||||
// SAFETY: glCreateShader/glShaderSource/glCompileShader 均为当前
|
||||
// 上下文的合法调用;src 是 NUL 结尾串(b"...\0")。
|
||||
let sh = unsafe { glCreateShader(kind) };
|
||||
if sh == 0 {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
unsafe {
|
||||
let ptr = src.as_ptr() as *const i8;
|
||||
glShaderSource(sh, 1, &ptr, std::ptr::null());
|
||||
glCompileShader(sh);
|
||||
}
|
||||
let mut ok: i32 = 0;
|
||||
// SAFETY: glGetShaderiv 写 ok。
|
||||
unsafe { glGetShaderiv(sh, GL_COMPILE_STATUS, &mut ok) };
|
||||
if ok == GL_FALSE {
|
||||
// SAFETY: sh 是本函数 glCreateShader 的产物。
|
||||
unsafe { glDeleteShader(sh) };
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
Ok(sh)
|
||||
}
|
||||
|
||||
/// 惰性资源组:(program, vao, vbo)。进程级缓存;VAO/VBO 固定复用,
|
||||
/// 每次 draw 用 glBufferData 重灌顶点(避免逐次分配/泄漏)。
|
||||
fn resources() -> Result<(u32, u32, u32), i32> {
|
||||
static RES: std::sync::OnceLock<Result<(u32, u32, u32), i32>> = std::sync::OnceLock::new();
|
||||
*RES.get_or_init(|| {
|
||||
let vs = match compile_shader(GL_VERTEX_SHADER, VERT_SRC) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let fs = match compile_shader(GL_FRAGMENT_SHADER, FRAG_SRC) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
// SAFETY: vs 是本闭包编译成功的着色器。
|
||||
unsafe { glDeleteShader(vs) };
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let prog = unsafe { glCreateProgram() };
|
||||
if prog == 0 {
|
||||
// SAFETY: vs/fs 均有效。
|
||||
unsafe {
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
}
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
unsafe {
|
||||
glAttachShader(prog, vs);
|
||||
glAttachShader(prog, fs);
|
||||
glLinkProgram(prog);
|
||||
}
|
||||
let mut ok: i32 = 0;
|
||||
// SAFETY: glGetProgramiv 写 ok。
|
||||
unsafe { glGetProgramiv(prog, GL_LINK_STATUS, &mut ok) };
|
||||
// 链接后可删着色器对象(程序保留可执行版本)。
|
||||
unsafe {
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
}
|
||||
if ok == GL_FALSE {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
// SAFETY: 生成并绑定固定 VAO/VBO(3.2 core)。
|
||||
let mut vao: u32 = 0;
|
||||
let mut vbo: u32 = 0;
|
||||
unsafe {
|
||||
glGenVertexArrays(1, &mut vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &mut vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
}
|
||||
Ok((prog, vao, vbo))
|
||||
})
|
||||
}
|
||||
|
||||
/// canonical→NDC 正交投影(列主序 4x4)。
|
||||
fn ortho(cw: f64, ch: f64) -> [f32; 16] {
|
||||
let (cw, ch) = (cw.max(1.0) as f32, ch.max(1.0) as f32);
|
||||
[
|
||||
2.0 / cw, 0.0, 0.0, 0.0,
|
||||
0.0, 2.0 / ch, 0.0, 0.0,
|
||||
0.0, 0.0, -1.0, 0.0,
|
||||
-1.0, -1.0, 0.0, 1.0,
|
||||
]
|
||||
}
|
||||
|
||||
/// 按原语生成 GL 顶点(canonical 坐标;已按图元类型合法)。
|
||||
fn vertices(prim: i32, pts: &[OfxPointD]) -> (u32, Vec<f32>) {
|
||||
match prim {
|
||||
super::primitive::LINES => (
|
||||
GL_LINES,
|
||||
pts.iter().flat_map(|p| [p.x as f32, p.y as f32]).collect(),
|
||||
),
|
||||
super::primitive::LINE_STRIP => (
|
||||
GL_LINE_STRIP,
|
||||
pts.iter().flat_map(|p| [p.x as f32, p.y as f32]).collect(),
|
||||
),
|
||||
super::primitive::LINE_LOOP => (
|
||||
GL_LINE_LOOP,
|
||||
pts.iter().flat_map(|p| [p.x as f32, p.y as f32]).collect(),
|
||||
),
|
||||
super::primitive::RECTANGLE => {
|
||||
let (x1, y1) = (pts[0].x as f32, pts[0].y as f32);
|
||||
let (x2, y2) = (pts[1].x as f32, pts[1].y as f32);
|
||||
// 两个三角形组成实心矩形(对角点定义)。
|
||||
(
|
||||
GL_TRIANGLES,
|
||||
vec![
|
||||
x1, y1, x2, y1, x1, y2,
|
||||
x1, y2, x2, y1, x2, y2,
|
||||
],
|
||||
)
|
||||
}
|
||||
super::primitive::POLYGON => {
|
||||
// 三角扇:0,i,i+1(凸多边形约定)。
|
||||
let mut v = Vec::new();
|
||||
for i in 1..pts.len() - 1 {
|
||||
v.extend_from_slice(&[
|
||||
pts[0].x as f32, pts[0].y as f32,
|
||||
pts[i].x as f32, pts[i].y as f32,
|
||||
pts[i + 1].x as f32, pts[i + 1].y as f32,
|
||||
]);
|
||||
}
|
||||
(GL_TRIANGLES, v)
|
||||
}
|
||||
super::primitive::ELLIPSE => {
|
||||
// 椭圆**线框**(文档),包围盒内 64 段线环。
|
||||
let (cx, cy) = ((pts[0].x + pts[1].x) / 2.0, (pts[0].y + pts[1].y) / 2.0);
|
||||
let (rx, ry) = (
|
||||
(pts[1].x - pts[0].x).abs() / 2.0,
|
||||
(pts[1].y - pts[0].y).abs() / 2.0,
|
||||
);
|
||||
let n = 64;
|
||||
let mut v = Vec::with_capacity(n * 2);
|
||||
for i in 0..n {
|
||||
let a = std::f64::consts::TAU * i as f64 / n as f64;
|
||||
v.push((cx + rx * a.cos()) as f32);
|
||||
v.push((cy + ry * a.sin()) as f32);
|
||||
}
|
||||
(GL_LINE_LOOP, v)
|
||||
}
|
||||
_ => (GL_TRIANGLES, Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 真实渲染:绑定固定 VAO/VBO、重灌顶点、设置 uniform、绘制。
|
||||
/// 要求 GL 上下文 current(宿主 draw action 已 acquire);无 current
|
||||
/// 上下文时不发任何 GL 命令(macOS 无 current 调 GL 是未定义行为,
|
||||
/// 直接失败)。
|
||||
pub fn render(ctx: &DrawContext, prim: i32, pts: &[OfxPointD]) -> Result<(), i32> {
|
||||
if !crate::gl_bridge::is_current() {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
let (prog, vao, vbo) = resources().map_err(|_| status::FAILED)?;
|
||||
let (mode, verts) = vertices(prim, pts);
|
||||
if verts.is_empty() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
// 合成:非不透明色 "over"(ofxDrawSuite.h setColour 文档)。
|
||||
if ctx.colour[3] < 1.0 {
|
||||
// SAFETY: 开启混合并设因子(上下文 current)。
|
||||
unsafe {
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
}
|
||||
// SAFETY: 以下 GL 调用均要求上下文 current(宿主已保证);对象
|
||||
// 名来自 resources()(本进程唯一 GL 使用方)。
|
||||
unsafe {
|
||||
glUseProgram(prog);
|
||||
glBindVertexArray(vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(
|
||||
GL_ARRAY_BUFFER,
|
||||
(verts.len() * 4) as isize,
|
||||
verts.as_ptr() as *const c_void,
|
||||
GL_STATIC_DRAW,
|
||||
);
|
||||
|
||||
let a_pos = glGetAttribLocation(prog, b"a_pos\0".as_ptr() as *const i8);
|
||||
if a_pos < 0 {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
glVertexAttribPointer(a_pos as u32, 2, GL_FLOAT, 0, 0, std::ptr::null());
|
||||
glEnableVertexAttribArray(a_pos as u32);
|
||||
|
||||
let u_proj = glGetUniformLocation(prog, b"u_proj\0".as_ptr() as *const i8);
|
||||
let u_col = glGetUniformLocation(prog, b"u_col\0".as_ptr() as *const i8);
|
||||
let cw = ctx.viewport.0 / ctx.pixel_scale.0.max(1e-6);
|
||||
let ch = ctx.viewport.1 / ctx.pixel_scale.1.max(1e-6);
|
||||
let m = ortho(cw, ch);
|
||||
glUniformMatrix4fv(u_proj, 1, 0, m.as_ptr());
|
||||
glUniform4f(u_col, ctx.colour[0], ctx.colour[1], ctx.colour[2], ctx.colour[3]);
|
||||
|
||||
if ctx.line_width > 0.0 {
|
||||
glLineWidth(ctx.line_width);
|
||||
}
|
||||
glDrawArrays(mode, 0, (verts.len() / 2) as i32);
|
||||
|
||||
// 解除绑定(对象保留复用)。
|
||||
glBindVertexArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
let err = unsafe { glGetError() };
|
||||
if err != GL_NO_ERROR {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 非 macOS:Draw suite 的 GL 绘制 stub(无 GL 桥 → Failed)。
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod gl_imp {
|
||||
use super::{DrawContext, OfxPointD};
|
||||
use crate::suites::status;
|
||||
|
||||
pub fn render(_ctx: &DrawContext, _prim: i32, _pts: &[OfxPointD]) -> Result<(), i32> {
|
||||
Err(status::FAILED)
|
||||
}
|
||||
}
|
||||
@@ -27,29 +27,36 @@
|
||||
//! RenderScale/PixelAspectRatio/Bounds/RegionOfDefinition/RowBytes/
|
||||
//! Field/UniqueIdentifier);宿主侧存强引用([`LIVE_TEXTURES`]),
|
||||
//! clipFreeTexture 摘除即释放(对应 HS 的 get/release 配对)。
|
||||
//! **OpenGLTextureIndex 是真实 GL 纹理名**(GL 模式,见
|
||||
//! [`crate::gl_bridge`] 方案 B):Output clip 是宿主为输出帧建的
|
||||
//! GL 纹理(经 GlCtx 注入,render 驱动负责建/FBO 挂载/回读/删除);
|
||||
//! 输入 clip 是本 suite 在 GL 渲染期经桥上传图像得到的 GL 纹理
|
||||
//! (clipFreeTexture 删除)。0 = CPU 回退(无 GL 名,插件按规范回退)。
|
||||
//! - 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::render`] 在渲染器上创建(CPU 帧 →
|
||||
//! GL 上传)。纹理格式:全链路 F32 约束下,像素深度按 clip 协商
|
||||
//! 结果(恒 F32);`format` 参数(kOfxImageEffectGLFormat*)若
|
||||
//! 请求的分量与协商分量不符,Phase 2 不做转换 → Failed(规范要求
|
||||
//! "host ensures it gives the requested format"——宁可显式失败也
|
||||
//! 不静默给错格式)。ofxGPURender.h 注明"宿主无需按 Clip
|
||||
//! Preferences 把图像重映射到插件请求的位深",插件以纹理句柄的
|
||||
//! PixelDepth/Components 为准。
|
||||
//! GL render 驱动在进入 render_gl 前已把输出 GL 纹理挂进 FBO 并
|
||||
//! 绑定(等价 C++ 的 `PluginRenderer::attach_output_texture`)。
|
||||
//! clipFreeTexture 对 Output 只释放句柄、不删纹理(宿主还要回读它,
|
||||
//! 回读后由 render 驱动删除)。
|
||||
//! - 输入纹理在 GL 模式经 [`crate::gl_bridge::create_input_texture`]
|
||||
//! 上传(CPU 帧 → GL 纹理);CPU 模式(无 GL 上下文)维持
|
||||
//! [`crate::render`] 的 CPU 纹理。纹理格式:全链路 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 取渲染器句柄。
|
||||
//! 期间要求上下文 current;本实现的约定是 render 驱动一次 acquire
|
||||
//! 整个 GL render action([`crate::gl_bridge::acquire`]),本 suite
|
||||
//! 回调期间上下文恒 current,经 [`crate::suites::gl_ctx`] TLS 取
|
||||
//! 渲染器句柄。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, c_double, c_int, c_void, CStr, CString};
|
||||
@@ -164,37 +171,64 @@ mod tests {
|
||||
// ---- 存活纹理表 -----------------------------------------------------------
|
||||
|
||||
/// 存活 GL 纹理表:clipLoadTexture 产出(props 地址 → 属性集 +
|
||||
/// 纹理值 + 是否输出 clip);clipFreeTexture 摘除即释放——对应
|
||||
/// HS 的 get/release 配对(HS: ofxhImageEffect.cpp:2336-2351)。
|
||||
/// 纹理是 oakrender 值(drop 自动释放后端 token;原
|
||||
/// `texture_free` 调用面随值模型删除)。
|
||||
/// 纹理值 + 是否输出 clip + 真实 GL 纹理名[可选]);clipFreeTexture
|
||||
/// 摘除即释放——对应 HS 的 get/release 配对
|
||||
/// (HS: ofxhImageEffect.cpp:2336-2351)。纹理是 oakrender 值(drop
|
||||
/// 自动释放后端 token;原 `texture_free` 调用面随值模型删除)。
|
||||
///
|
||||
/// `gl_texture`:GL 模式下输入 clip 经 [`crate::gl_bridge`] 建的真实
|
||||
/// GL 纹理名(输出 clip 恒 None——宿主自建并负责生命周期,见
|
||||
/// [`crate::gl_bridge`] 模块文档);摘除时经
|
||||
/// [`delete_gl_texture_if_gl`] 删除。
|
||||
///
|
||||
/// 属性集必须**装箱**(Box 稳定堆地址):纹理句柄指向它,函数返回后
|
||||
/// 必须仍存活;栈上临时变量会悬垂(phase-2 实现初版的 bug)。
|
||||
static LIVE_TEXTURES: std::sync::LazyLock<
|
||||
Mutex<HashMap<usize, (Box<PropertySet>, crate::render::Texture, bool)>>,
|
||||
Mutex<HashMap<usize, (Box<PropertySet>, crate::render::Texture, bool, Option<i32>)>>,
|
||||
> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// 登记纹理(clipLoadTexture 内部;`props` 装箱后取地址为句柄基址)。
|
||||
/// `gl_texture`:真实 GL 纹理名(输入 clip GL 模式;无则 None)。
|
||||
fn register(
|
||||
props: Box<PropertySet>,
|
||||
texture: crate::render::Texture,
|
||||
is_output: bool,
|
||||
gl_texture: Option<i32>,
|
||||
) -> 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));
|
||||
.insert(addr, (props, texture, is_output, gl_texture));
|
||||
addr
|
||||
}
|
||||
|
||||
/// 删除 GL 纹理(上下文 current 时;否则留给下次 GL 渲染前的兜底——
|
||||
/// 正常路径 clipFreeTexture/purge 都在 GL render 期,上下文必 current)。
|
||||
fn delete_gl_texture_if_gl(name: i32) {
|
||||
if crate::suites::gl_ctx().is_some() {
|
||||
crate::gl_bridge::delete_gl_texture(name);
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放全部残留输入纹理(GL render action 返回后的安全网:规范要求
|
||||
/// 插件在 action 返回前 clipFreeTexture 全部句柄;遗漏的输入纹理在
|
||||
/// 此释放,输出纹理保留——宿主还要读它)。
|
||||
/// 此释放,输出纹理保留——宿主还要读它)。GL 输入纹理同步删除
|
||||
/// (上下文 current;render_gl 在清 GlCtx TLS 前调用本函数)。
|
||||
pub(crate) fn purge_leftovers() {
|
||||
let mut live = LIVE_TEXTURES.lock().unwrap_or_else(|e| e.into_inner());
|
||||
live.retain(|_, (_, _, is_output)| *is_output);
|
||||
let dropped: Vec<Option<i32>> = live
|
||||
.iter()
|
||||
.filter(|(_, (_, _, is_output, _))| !*is_output)
|
||||
.map(|(_, (_, _, _, gl))| *gl)
|
||||
.collect();
|
||||
live.retain(|_, (_, _, is_output, _)| *is_output);
|
||||
drop(live);
|
||||
for gl in dropped {
|
||||
if let Some(gl) = gl {
|
||||
delete_gl_texture_if_gl(gl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
@@ -240,12 +274,11 @@ fn clip_string(clip: &ClipInstance, name: &str, default: &str) -> String {
|
||||
}
|
||||
|
||||
/// 构造纹理属性集(ofxGPURender.h 规定的属性;输入与输出共用,
|
||||
/// 只是数据来源不同)。`OpenGLTextureIndex` 经
|
||||
/// [`crate::render::texture_id`](桩恒 0——wgpu 后端无 GL 命名空间;
|
||||
/// CPU 回退下插件按规范回退,纹理内容仍可经 clipGetImage 取用)。
|
||||
/// 只是数据来源不同)。`OpenGLTextureIndex` 为真实 GL 纹理名
|
||||
/// (`texture_index`;0 = 无 GL 名——CPU 回退下插件按规范回退,纹理
|
||||
/// 内容仍可经 clipGetImage 取用)。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn make_texture_props(
|
||||
texture: &crate::render::Texture,
|
||||
width: f64,
|
||||
height: f64,
|
||||
components: crate::image::Components,
|
||||
@@ -254,12 +287,10 @@ fn make_texture_props(
|
||||
par: f64,
|
||||
scale: crate::instance::RenderScale,
|
||||
row_bytes: i32,
|
||||
texture_index: i32,
|
||||
) -> PropertySet {
|
||||
let props = PropertySet::new();
|
||||
props.set_one(
|
||||
GL_TEXTURE_INDEX,
|
||||
Value::Int(crate::render::texture_id(texture)),
|
||||
);
|
||||
props.set_one(GL_TEXTURE_INDEX, Value::Int(texture_index));
|
||||
props.set_one(GL_TEXTURE_TARGET, Value::Int(GL_TEXTURE_2D));
|
||||
props.set_one(
|
||||
crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
|
||||
@@ -343,8 +374,10 @@ unsafe extern "C" fn clip_load_texture(
|
||||
if w <= 0.0 || h <= 0.0 {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
// OpenGLTextureIndex = 宿主为输出帧建的真实 GL 纹理名
|
||||
// (render 驱动 GL 分支经 GlCtx 注入;0 = CPU 回退)。
|
||||
let index = gl.output_gl_texture.unwrap_or(0);
|
||||
let props = make_texture_props(
|
||||
&tex,
|
||||
w,
|
||||
h,
|
||||
crate::image::Components::Rgba,
|
||||
@@ -353,8 +386,9 @@ unsafe extern "C" fn clip_load_texture(
|
||||
clip_par(c),
|
||||
scale,
|
||||
(w as i32) * 4 * 4,
|
||||
index,
|
||||
);
|
||||
let addr = register(Box::new(props), tex, true);
|
||||
let addr = register(Box::new(props), tex, true, None);
|
||||
unsafe { *out = tag::make(addr as *const PropertySet, tag::PROPERTY_SET) };
|
||||
return Ok(());
|
||||
}
|
||||
@@ -386,6 +420,17 @@ unsafe extern "C" fn clip_load_texture(
|
||||
if w <= 0.0 || h <= 0.0 {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
// GL 模式:把输入图像上传成真实 GL 纹理(RGBA32F;上下文
|
||||
// current——render 驱动已 acquire),OpenGLTextureIndex 返回
|
||||
// 真实名;GL 上传失败 → 显式 Failed(插件按规范继续)。非 GL
|
||||
// 上下文(本函数仅在 GL 渲染期可达,此分支是 CPU 纹理兜底):
|
||||
// 仍建 CPU 纹理、索引 0。
|
||||
let gl_tex = crate::gl_bridge::create_input_texture(
|
||||
w as i32,
|
||||
h as i32,
|
||||
image.pixels(),
|
||||
)
|
||||
.ok();
|
||||
let params = crate::render::VideoParams {
|
||||
width: w as i32,
|
||||
height: h as i32,
|
||||
@@ -395,7 +440,6 @@ unsafe extern "C" fn clip_load_texture(
|
||||
let tex = crate::render::texture_create(¶ms, image.pixels(), image.row_bytes() as i32)
|
||||
.map_err(|_| status::ERR_MEMORY)?;
|
||||
let props = make_texture_props(
|
||||
&tex,
|
||||
w,
|
||||
h,
|
||||
components,
|
||||
@@ -404,8 +448,9 @@ unsafe extern "C" fn clip_load_texture(
|
||||
clip_par(c),
|
||||
scale,
|
||||
image.row_bytes() as i32,
|
||||
gl_tex.unwrap_or(0),
|
||||
);
|
||||
let addr = register(Box::new(props), tex, false);
|
||||
let addr = register(Box::new(props), tex, false, gl_tex);
|
||||
unsafe { *out = tag::make(addr as *const PropertySet, tag::PROPERTY_SET) };
|
||||
Ok(())
|
||||
})
|
||||
@@ -440,9 +485,9 @@ fn clip_par(c: &ClipInstance) -> f64 {
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
/// clipFreeTexture:释放纹理(输入 clip 删除纹理值;Output 只释放
|
||||
/// 句柄不删纹理——宿主还要读它)。纹理是值:摘除表条目即 drop
|
||||
/// (原 `texture_free` 调用面随值模型删除)。
|
||||
/// clipFreeTexture:释放纹理(输入 clip 删除纹理值并释放真实 GL
|
||||
/// 纹理;Output 只释放句柄不删纹理——宿主还要读它)。纹理是值:
|
||||
/// 摘除表条目即 drop(原 `texture_free` 调用面随值模型删除)。
|
||||
unsafe extern "C" fn clip_free_texture(texture_handle: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
if texture_handle.is_null() {
|
||||
@@ -454,7 +499,13 @@ unsafe extern "C" fn clip_free_texture(texture_handle: *mut c_void) -> c_int {
|
||||
live.remove(&addr)
|
||||
};
|
||||
match entry {
|
||||
Some((_props, _texture, _is_output)) => Ok(()),
|
||||
Some((_props, _texture, _is_output, gl_tex)) => {
|
||||
// 输入 clip 的真实 GL 纹理随释放删除(上下文 current)。
|
||||
if let Some(gl) = gl_tex {
|
||||
delete_gl_texture_if_gl(gl);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
None => Err(status::ERR_BAD_HANDLE),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -579,6 +579,7 @@ mod tests {
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
interact: std::sync::Mutex::new(None),
|
||||
});
|
||||
let ih = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
|
||||
unsafe {
|
||||
|
||||
@@ -0,0 +1,814 @@
|
||||
// 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/>.
|
||||
|
||||
//! OfxInteractSuiteV1 + [`Interact`] 宿主对象。
|
||||
//!
|
||||
//! Interact 是插件自定义 UI 的宿主侧对象(主进程 UI 事件宿主):与
|
||||
//! worker 进程里的渲染实例并存(OFX 允许同一插件多实例),经
|
||||
//! [`crate::instance::Instance::new_interact`] 创建、绑定到效果实例。
|
||||
//!
|
||||
//! ## 生命周期
|
||||
//!
|
||||
//! ```text
|
||||
//! new_interact → kOfxActionNewInteract(创建;插件拒绝 → None)
|
||||
//! describe → kOfxActionDescribe(kOfxActionDescribeInteract)
|
||||
//! create_instance→ kOfxActionCreateInstance(kOfxActionCreateInstanceInteract)
|
||||
//! draw/pen/key/idle → kOfxInteractAction*(宿主→插件)
|
||||
//! destroy → kOfxActionDestroyInstance(kOfxActionDestroyInstanceInteract;
|
||||
//! Instance 销毁时自动连带)
|
||||
//! ```
|
||||
//!
|
||||
//! 所有 action 走 `Interact::call` → 插件入口(overlay interact V2/V1
|
||||
//! 入口,未声明则插件 main entry;任务契约:new_interact 向插件入口
|
||||
//! 发 kOfxActionNewInteract)。返回值(OfxStatus)如实透传。
|
||||
//!
|
||||
//! ## 语义(ofxInteract.h)
|
||||
//!
|
||||
//! - pen/key action 的返回:kOfxStatOK = 插件已处理该事件,宿主不应再
|
||||
//! 把事件传给视图中其他交互对象;kOfxStatReplyDefault = 插件未处理,
|
||||
//! 宿主可自行处置。
|
||||
//! - pen 坐标:调用方给 viewport 像素;inArgs 另带 canonical 坐标
|
||||
//! (kOfxInteractPropPenPosition,= viewport 像素 / pixelScale)与
|
||||
//! pixelScale、pressure(两态笔按 ofxInteract.h 映射 0/1)。
|
||||
//! - key:kOfxPropKeySym(ofxKeySyms.h 关键码)+ kOfxPropKeyString
|
||||
//! (UTF-8;无 UTF8 编码的键为空串)。
|
||||
//! - draw:宿主在调用前经 [`crate::gl_bridge::acquire`] 保持 GL current
|
||||
//! 整个 action(插件发原生 GL 命令或经 Draw suite,ofxInteract.h
|
||||
//! "the openGL context for this interact has been set");inArgs 带
|
||||
//! viewport/pixelScale/time/backgroundImage/backgroundColour/
|
||||
//! drawContext。
|
||||
|
||||
use std::ffi::{c_int, c_void, CString};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::host::{EntryPoint, Plugin};
|
||||
use crate::property::{PropertySet, Value};
|
||||
use crate::suites::{status, tag};
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
/// Interact 实例。`Arc<Interact>` 管理生命周期(app 侧持句柄 +
|
||||
/// [`crate::instance::Instance`] 的 interact 字段)。
|
||||
///
|
||||
/// `#[repr(C)]` + props 在偏移 0(句柄约定:interact handle =
|
||||
/// `&props` | INTERACT 标签;property suite 剥标签后直读属性集)。
|
||||
#[repr(C)]
|
||||
pub struct Interact {
|
||||
/// interact 实例级属性集(偏移 0,句柄约定)。
|
||||
pub props: PropertySet,
|
||||
/// 所属插件。
|
||||
pub plugin: Arc<Plugin>,
|
||||
/// interact 的入口(overlay interact V2/V1 入口,未声明则插件
|
||||
/// main entry)。interact 的所有 action 都发到这里。
|
||||
pub entry: EntryPoint,
|
||||
/// 效果实例句柄(tagged INSTANCE;interact action inArgs 的
|
||||
/// kOfxPropEffectInstance)。不透明令牌,不解引用。
|
||||
pub effect_handle: *mut c_void,
|
||||
/// 插件经 interactSwapBuffers 请求的缓冲交换(app 侧轮询后清零)。
|
||||
pub swap_requested: AtomicBool,
|
||||
/// 插件经 interactRedraw 请求的重绘(app 侧轮询后清零)。
|
||||
pub redraw_requested: AtomicBool,
|
||||
/// destroy 是否已通知(幂等门;Instance 销毁连带触发)。
|
||||
pub destroyed: AtomicBool,
|
||||
}
|
||||
|
||||
// SAFETY: effect_handle 是不透明令牌(同 [`crate::property::Value::Pointer`],
|
||||
// 从不在此对象上解引用);entry 是函数指针,调用永远发生在宿主控制的
|
||||
// action 分发点;其余字段(Arc/Mutex/AtomicBool/PropertySet)均线程安全。
|
||||
// Interact 经 Instance.interact 字段跨线程可达,因此 Send/Sync 成立。
|
||||
unsafe impl Send for Interact {}
|
||||
unsafe impl Sync for Interact {}
|
||||
|
||||
impl Interact {
|
||||
/// 构造(宿主内部;[`crate::instance::Instance::new_interact`] 调)。
|
||||
/// `effect_handle` 为 tagged INSTANCE 句柄(不透明令牌)。
|
||||
pub(crate) fn new(
|
||||
plugin: Arc<Plugin>,
|
||||
entry: EntryPoint,
|
||||
effect_handle: *mut c_void,
|
||||
) -> Arc<Interact> {
|
||||
let i = Arc::new(Interact {
|
||||
props: PropertySet::new(),
|
||||
plugin,
|
||||
entry,
|
||||
effect_handle,
|
||||
swap_requested: AtomicBool::new(false),
|
||||
redraw_requested: AtomicBool::new(false),
|
||||
destroyed: AtomicBool::new(false),
|
||||
});
|
||||
init_interact_props(&i.props);
|
||||
i
|
||||
}
|
||||
|
||||
/// interact handle(tagged INTERACT)。
|
||||
pub(crate) fn handle(&self) -> *mut c_void {
|
||||
tag::make(&self.props as *const PropertySet, tag::INTERACT)
|
||||
}
|
||||
|
||||
/// 向插件入口发一个 interact action。
|
||||
///
|
||||
/// # Safety
|
||||
/// handle 与 action 匹配(Interact 生命周期由宿主控制;destroy 后
|
||||
/// 不再调用)。
|
||||
pub(crate) fn call(&self, action: &str, in_args: &PropertySet, out_args: &PropertySet) -> i32 {
|
||||
unsafe {
|
||||
self.plugin.call_entry(self.entry, action, self.handle(), in_args, out_args)
|
||||
}
|
||||
}
|
||||
|
||||
/// 取 effect 实例句柄(inArgs 的 kOfxPropEffectInstance)。
|
||||
fn effect_instance_prop(&self) -> Value {
|
||||
Value::Pointer(self.effect_handle)
|
||||
}
|
||||
|
||||
/// 当前 pixelScale(interact props,默认 1,1)。
|
||||
fn pixel_scale(&self) -> (f64, f64) {
|
||||
read_double2(&self.props, crate::host::PROP_INTERACT_PIXEL_SCALE)
|
||||
.unwrap_or((1.0, 1.0))
|
||||
}
|
||||
|
||||
/// describe(kOfxActionDescribe == kOfxActionDescribeInteract,
|
||||
/// ofxInteract.h:171)。inArgs/outArgs 冗余为 NULL。
|
||||
pub fn describe(&self) -> i32 {
|
||||
let empty = PropertySet::new();
|
||||
self.call(crate::host::ACTION_DESCRIBE, &empty, &empty)
|
||||
}
|
||||
|
||||
/// createInstance(kOfxActionCreateInstance ==
|
||||
/// kOfxActionCreateInstanceInteract,ofxInteract.h:198)。
|
||||
/// draw/pen/key 的前置(ofxInteract.h 各 action 的 \pre)。
|
||||
pub fn create_instance(&self) -> i32 {
|
||||
let empty = PropertySet::new();
|
||||
self.call(crate::host::ACTION_CREATE_INSTANCE, &empty, &empty)
|
||||
}
|
||||
|
||||
/// draw action(ofxInteract.h:265)。宿主在调用前
|
||||
/// [`crate::gl_bridge::acquire`] 保持 GL current 整个 action;inArgs
|
||||
/// 带 kOfxInteractPropPixelScale/ViewportSize/BackgroundColour/
|
||||
/// BackgroundImage/Time/RenderScale/DrawContext + kOfxPropEffectInstance。
|
||||
///
|
||||
/// `background_image`:宿主持有的背景图像句柄(本宿主 Phase 1 无
|
||||
/// 合成背景,传 None 即空——任务契约"可空")。
|
||||
///
|
||||
/// 返回值:插件的 OfxStatus 如实透传;GL 上下文不可用(非 macOS /
|
||||
/// 无上下文)时返回 kOfxStatErrMissingHostFeature(宿主侧失败,未
|
||||
/// 触达插件)。
|
||||
pub fn draw(
|
||||
&self,
|
||||
viewport_size: (f64, f64),
|
||||
pixel_scale: (f64, f64),
|
||||
time: f64,
|
||||
background_image: Option<*mut c_void>,
|
||||
) -> i32 {
|
||||
// 1. GL current 整个 draw action(插件发原生 GL 命令或经 Draw
|
||||
// suite;失败即无法绘制 → 不触达插件)。
|
||||
let _guard = match crate::gl_bridge::acquire() {
|
||||
Ok(g) => g,
|
||||
Err(_) => return status::ERR_MISSING_HOST_FEATURE,
|
||||
};
|
||||
|
||||
// 2. Draw suite 上下文(存活注册;draw 返回后摘除)。
|
||||
let draw_ctx = crate::suites::draw::make_context(viewport_size, pixel_scale);
|
||||
|
||||
let in_args = PropertySet::new();
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_PIXEL_SCALE,
|
||||
vec![Value::Double(pixel_scale.0), Value::Double(pixel_scale.1)],
|
||||
);
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_VIEWPORT_SIZE,
|
||||
vec![Value::Double(viewport_size.0), Value::Double(viewport_size.1)],
|
||||
);
|
||||
in_args.set_one(
|
||||
crate::host::PROP_INTERACT_BACKGROUND_IMAGE,
|
||||
Value::Pointer(background_image.unwrap_or(std::ptr::null_mut())),
|
||||
);
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_BACKGROUND_COLOUR,
|
||||
vec![Value::Double(0.0), Value::Double(0.0), Value::Double(0.0)],
|
||||
);
|
||||
in_args.set_one(crate::host::PROP_TIME, Value::Double(time));
|
||||
in_args.define(
|
||||
crate::host::PROP_RENDER_SCALE,
|
||||
vec![Value::Double(1.0), Value::Double(1.0)],
|
||||
);
|
||||
in_args.set_one(crate::host::PROP_INTERACT_DRAW_CONTEXT, Value::Pointer(draw_ctx));
|
||||
in_args.set_one(crate::host::PROP_EFFECT_INSTANCE, self.effect_instance_prop());
|
||||
|
||||
// 3. interact 实例属性同步(kOfxInteractPropPixelScale 等是实例
|
||||
// 属性集上只读的"当前状态",ofxInteract.h:58)。
|
||||
self.props.define(
|
||||
crate::host::PROP_INTERACT_PIXEL_SCALE,
|
||||
vec![Value::Double(pixel_scale.0), Value::Double(pixel_scale.1)],
|
||||
);
|
||||
self.props.define(
|
||||
crate::host::PROP_INTERACT_VIEWPORT_SIZE,
|
||||
vec![Value::Double(viewport_size.0), Value::Double(viewport_size.1)],
|
||||
);
|
||||
self.props.set_one(
|
||||
crate::host::PROP_INTERACT_BACKGROUND_IMAGE,
|
||||
Value::Pointer(background_image.unwrap_or(std::ptr::null_mut())),
|
||||
);
|
||||
|
||||
let out = PropertySet::new();
|
||||
let st = self.call(crate::host::ACTION_INTERACT_DRAW, &in_args, &out);
|
||||
crate::suites::draw::drop_context(draw_ctx);
|
||||
st
|
||||
}
|
||||
|
||||
/// 装配 pen 类 action 的 inArgs(PenMotion/PenDown/PenUp 共用,
|
||||
/// ofxInteract.h:274-321)。
|
||||
fn pen_in_args(&self, pen_viewport: (f64, f64), pressure: f64, time: f64) -> PropertySet {
|
||||
let (ps_x, ps_y) = self.pixel_scale();
|
||||
// canonical = viewport 像素 / pixelScale(pixelScale 是
|
||||
// canonical→屏幕像素的换算比例)。
|
||||
let in_args = PropertySet::new();
|
||||
in_args.set_one(crate::host::PROP_EFFECT_INSTANCE, self.effect_instance_prop());
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_PIXEL_SCALE,
|
||||
vec![Value::Double(ps_x), Value::Double(ps_y)],
|
||||
);
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_BACKGROUND_COLOUR,
|
||||
vec![Value::Double(0.0), Value::Double(0.0), Value::Double(0.0)],
|
||||
);
|
||||
in_args.set_one(crate::host::PROP_TIME, Value::Double(time));
|
||||
in_args.define(
|
||||
crate::host::PROP_RENDER_SCALE,
|
||||
vec![Value::Double(1.0), Value::Double(1.0)],
|
||||
);
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_PEN_POSITION,
|
||||
vec![
|
||||
Value::Double(pen_viewport.0 / ps_x.max(1e-9)),
|
||||
Value::Double(pen_viewport.1 / ps_y.max(1e-9)),
|
||||
],
|
||||
);
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_PEN_VIEWPORT_POSITION,
|
||||
vec![
|
||||
Value::Int(pen_viewport.0.round() as i32),
|
||||
Value::Int(pen_viewport.1.round() as i32),
|
||||
],
|
||||
);
|
||||
in_args.set_one(crate::host::PROP_INTERACT_PEN_PRESSURE, Value::Double(pressure));
|
||||
in_args
|
||||
}
|
||||
|
||||
/// pen_motion(ofxInteract.h:302)。`pen_viewport` 为视口像素坐标;
|
||||
/// `pen_down` 表示笔是否按下(两态笔按 ofxInteract.h:114 映射压力
|
||||
/// 1.0/0.0)。
|
||||
pub fn pen_motion(&self, pen_viewport: (f64, f64), pen_down: bool, time: f64) -> i32 {
|
||||
let pressure = if pen_down { 1.0 } else { 0.0 };
|
||||
let in_args = self.pen_in_args(pen_viewport, pressure, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_PEN_MOTION, &in_args, &out)
|
||||
}
|
||||
|
||||
/// pen_down(ofxInteract.h:340)。压力 1.0。
|
||||
pub fn pen_down(&self, pen_viewport: (f64, f64), time: f64) -> i32 {
|
||||
let in_args = self.pen_in_args(pen_viewport, 1.0, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_PEN_DOWN, &in_args, &out)
|
||||
}
|
||||
|
||||
/// pen_up(ofxInteract.h:376)。压力 0.0。
|
||||
pub fn pen_up(&self, pen_viewport: (f64, f64), time: f64) -> i32 {
|
||||
let in_args = self.pen_in_args(pen_viewport, 0.0, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_PEN_UP, &in_args, &out)
|
||||
}
|
||||
|
||||
/// 装配 key 类 action 的 inArgs(KeyDown/KeyUp 共用,
|
||||
/// ofxInteract.h:384-442)。
|
||||
fn key_in_args(&self, key_sym: i32, key_string: &str, time: f64) -> PropertySet {
|
||||
let in_args = PropertySet::new();
|
||||
in_args.set_one(crate::host::PROP_EFFECT_INSTANCE, self.effect_instance_prop());
|
||||
in_args.set_one(crate::host::PROP_KEY_SYM, Value::Int(key_sym));
|
||||
in_args.set_one(crate::host::PROP_KEY_STRING, Value::String(cs(key_string)));
|
||||
in_args.set_one(crate::host::PROP_TIME, Value::Double(time));
|
||||
in_args.define(
|
||||
crate::host::PROP_RENDER_SCALE,
|
||||
vec![Value::Double(1.0), Value::Double(1.0)],
|
||||
);
|
||||
in_args
|
||||
}
|
||||
|
||||
/// key_down(ofxInteract.h:410)。`key_sym` 为 ofxKeySyms.h 关键码;
|
||||
/// `key_string` 为 UTF-8 字符(无 UTF8 编码的键为空串)。
|
||||
pub fn key_down(&self, key_sym: i32, key_string: &str, time: f64) -> i32 {
|
||||
let in_args = self.key_in_args(key_sym, key_string, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_KEY_DOWN, &in_args, &out)
|
||||
}
|
||||
|
||||
/// key_up(ofxInteract.h:443)。
|
||||
pub fn key_up(&self, key_sym: i32, key_string: &str, time: f64) -> i32 {
|
||||
let in_args = self.key_in_args(key_sym, key_string, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_KEY_UP, &in_args, &out)
|
||||
}
|
||||
|
||||
/// idle(宿主空闲泵;任务契约的 kOfxInteractActionIdle,属本宿主
|
||||
/// 扩展)。插件未实现时返回 kOfxStatReplyDefault。
|
||||
pub fn idle(&self) -> i32 {
|
||||
let empty = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_IDLE, &empty, &empty)
|
||||
}
|
||||
|
||||
/// gain_focus(ofxInteract.h:501):pen/key 事件的前置
|
||||
/// (ofxInteract.h 各 action 的 \pre 要求 interact 已获焦点)。
|
||||
pub fn gain_focus(&self, time: f64) -> i32 {
|
||||
let in_args = self.pen_in_args((0.0, 0.0), 0.0, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_GAIN_FOCUS, &in_args, &out)
|
||||
}
|
||||
|
||||
/// lose_focus(ofxInteract.h:526)。
|
||||
pub fn lose_focus(&self, time: f64) -> i32 {
|
||||
let in_args = self.pen_in_args((0.0, 0.0), 0.0, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_LOSE_FOCUS, &in_args, &out)
|
||||
}
|
||||
|
||||
/// destroy(kOfxActionDestroyInstance == kOfxActionDestroyInstanceInteract,
|
||||
/// ofxInteract.h:227)。幂等:只通知一次。返回状态忽略(析构不可
|
||||
/// 回滚,ofxInteract.h "what is returned is moot")。
|
||||
pub fn destroy(&self) {
|
||||
if self
|
||||
.destroyed
|
||||
.swap(true, Ordering::Relaxed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let empty = PropertySet::new();
|
||||
self.call(crate::host::ACTION_DESTROY_INSTANCE, &empty, &empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// interact 实例属性表(ofxInteract.h 的 PropertiesInteract 子集)。
|
||||
fn init_interact_props(props: &PropertySet) {
|
||||
props.define(
|
||||
crate::host::PROP_INTERACT_PIXEL_SCALE,
|
||||
vec![Value::Double(1.0), Value::Double(1.0)],
|
||||
);
|
||||
props.define(
|
||||
crate::host::PROP_INTERACT_VIEWPORT_SIZE,
|
||||
vec![Value::Double(0.0), Value::Double(0.0)],
|
||||
);
|
||||
props.set_one(
|
||||
crate::host::PROP_INTERACT_BACKGROUND_IMAGE,
|
||||
Value::Pointer(std::ptr::null_mut()),
|
||||
);
|
||||
props.define(
|
||||
crate::host::PROP_INTERACT_SUGGESTED_COLOUR,
|
||||
vec![Value::Double(1.0), Value::Double(1.0), Value::Double(1.0)],
|
||||
);
|
||||
props.define(crate::host::PROP_INTERACT_SLAVE_TO_PARAM, vec![]);
|
||||
props.define(
|
||||
crate::host::PROP_INTERACT_BACKGROUND_COLOUR,
|
||||
vec![Value::Double(0.0), Value::Double(0.0), Value::Double(0.0)],
|
||||
);
|
||||
// 帧缓冲位深:离屏 FBO 是 RGBA32F → 每分量 32 bit,含 alpha。
|
||||
props.set_one(crate::host::PROP_INTERACT_BIT_DEPTH, Value::Int(32));
|
||||
props.set_one(crate::host::PROP_INTERACT_HAS_ALPHA, Value::Int(1));
|
||||
}
|
||||
|
||||
/// 从属性集读 Double×2(缺失/类型不符 → None)。
|
||||
fn read_double2(props: &PropertySet, name: &str) -> Option<(f64, f64)> {
|
||||
let x = match props.get(name, 0)? {
|
||||
Value::Double(d) => d,
|
||||
_ => return None,
|
||||
};
|
||||
let y = match props.get(name, 1)? {
|
||||
Value::Double(d) => d,
|
||||
_ => return None,
|
||||
};
|
||||
Some((x, y))
|
||||
}
|
||||
|
||||
// ---- OfxInteractSuiteV1(ofxInteract.h:534-544)------------------------
|
||||
|
||||
/// interact 句柄解析(tag INTERACT)。
|
||||
fn resolve_interact(handle: *mut c_void) -> Result<&'static Interact, c_int> {
|
||||
if handle.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
unsafe {
|
||||
if tag::kind(handle) != tag::INTERACT {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
// Interact 的 props 在偏移 0:剥标签后的地址即 &Interact。
|
||||
Ok(&*(tag::strip(handle) as *const Interact))
|
||||
}
|
||||
}
|
||||
|
||||
/// 公共入口模板: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),
|
||||
)
|
||||
}
|
||||
|
||||
/// interactSwapBuffers:插件请求宿主交换 GL 缓冲(宿主 UI 视图中)。
|
||||
/// 本宿主无 UI 视图,把请求记入 `swap_requested`(app 侧轮询后清零)并
|
||||
/// 返回 OK——宿主服务真实生效(请求被记录、可被 UI 消费)。
|
||||
unsafe extern "C" fn interact_swap_buffers(handle: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
let i = resolve_interact(handle)?;
|
||||
i.swap_requested.store(true, Ordering::Relaxed);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// interactRedraw:插件请求宿主重绘 interact 视图。同上记入
|
||||
/// `redraw_requested`(app 侧轮询)。
|
||||
unsafe extern "C" fn interact_redraw(handle: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
let i = resolve_interact(handle)?;
|
||||
i.redraw_requested.store(true, Ordering::Relaxed);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// interactGetPropertySet:返回 interact 的属性集句柄(tagged INTERACT)。
|
||||
unsafe extern "C" fn interact_get_property_set(
|
||||
handle: *mut c_void,
|
||||
property: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if property.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let i = resolve_interact(handle)?;
|
||||
unsafe { *property = tag::make(&i.props as *const PropertySet, tag::INTERACT) };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// 函数表布局(与 SDK `OfxInteractSuiteV1` 逐字段一致;ofxInteract.h:534)。
|
||||
#[repr(C)]
|
||||
pub struct InteractSuiteV1 {
|
||||
/// interactSwapBuffers。
|
||||
pub interact_swap_buffers: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
/// interactRedraw。
|
||||
pub interact_redraw: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
/// interactGetPropertySet。
|
||||
pub interact_get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
|
||||
}
|
||||
|
||||
/// 函数表实例。
|
||||
pub fn suite_v1() -> &'static InteractSuiteV1 {
|
||||
static SUITE: std::sync::OnceLock<InteractSuiteV1> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| InteractSuiteV1 {
|
||||
interact_swap_buffers,
|
||||
interact_redraw,
|
||||
interact_get_property_set,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::ffi::{c_char, CStr};
|
||||
use std::sync::Mutex;
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
/// interact 属性表:PixelScale/ViewportSize/SuggestedColour/
|
||||
/// SlaveToParam/BitDepth/HasAlpha 等按头文件预置,经 property suite
|
||||
/// 可读写。
|
||||
#[test]
|
||||
fn interact_props_initialized() {
|
||||
let props = PropertySet::new();
|
||||
init_interact_props(&props);
|
||||
// Value 无 PartialEq(指针/CString 成员),用 matches! 断言。
|
||||
assert!(
|
||||
matches!(
|
||||
props.get(crate::host::PROP_INTERACT_PIXEL_SCALE, 0),
|
||||
Some(Value::Double(1.0))
|
||||
),
|
||||
"PixelScale[0] 应为 1.0"
|
||||
);
|
||||
assert_eq!(props.dimension(crate::host::PROP_INTERACT_PIXEL_SCALE), 2);
|
||||
assert!(
|
||||
matches!(
|
||||
props.get(crate::host::PROP_INTERACT_SUGGESTED_COLOUR, 2),
|
||||
Some(Value::Double(1.0))
|
||||
),
|
||||
"SuggestedColour[2] 应为 1.0"
|
||||
);
|
||||
assert_eq!(props.dimension(crate::host::PROP_INTERACT_SLAVE_TO_PARAM), 0);
|
||||
assert!(
|
||||
matches!(
|
||||
props.get(crate::host::PROP_INTERACT_BIT_DEPTH, 0),
|
||||
Some(Value::Int(32))
|
||||
),
|
||||
"BitDepth 应为 32"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
props.get(crate::host::PROP_INTERACT_HAS_ALPHA, 0),
|
||||
Some(Value::Int(1))
|
||||
),
|
||||
"HasAlpha 应为 1"
|
||||
);
|
||||
}
|
||||
|
||||
/// read_double2 帮助函数。
|
||||
#[test]
|
||||
fn read_double2_helper() {
|
||||
let props = PropertySet::new();
|
||||
props.define("P", vec![Value::Double(2.0), Value::Double(3.0)]);
|
||||
assert_eq!(read_double2(&props, "P"), Some((2.0, 3.0)));
|
||||
assert_eq!(read_double2(&props, "Q"), None);
|
||||
}
|
||||
|
||||
/// 空指针/错标签 → ErrBadHandle(interact suite 的句柄校验)。
|
||||
#[test]
|
||||
fn interact_suite_handle_validation() {
|
||||
unsafe {
|
||||
assert_eq!(interact_swap_buffers(std::ptr::null_mut()), status::ERR_BAD_HANDLE);
|
||||
assert_eq!(interact_redraw(std::ptr::null_mut()), status::ERR_BAD_HANDLE);
|
||||
let mut out: *mut c_void = std::ptr::null_mut();
|
||||
assert_eq!(
|
||||
interact_get_property_set(std::ptr::null_mut(), &mut out),
|
||||
status::ERR_BAD_HANDLE
|
||||
);
|
||||
// 错标签:裸 PropertySet(标签 0)当 interact 用。
|
||||
let props = PropertySet::new();
|
||||
let raw = &props as *const PropertySet as *mut c_void;
|
||||
assert_eq!(interact_swap_buffers(raw), status::ERR_BAD_HANDLE);
|
||||
}
|
||||
}
|
||||
|
||||
/// interact suite 服务端:swap/redraw 请求被记录(app 侧轮询面),
|
||||
/// getPropertySet 返回 interact 属性集句柄。
|
||||
#[test]
|
||||
fn interact_suite_requests_are_recorded() {
|
||||
// 用假插件构造 Interact(不进插件入口——只测 suite 服务端)。
|
||||
let plugin = crate::host::Plugin {
|
||||
identifier: "fake".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::from("fake"),
|
||||
contexts: Vec::new(),
|
||||
descriptor: crate::descriptor::EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: dummy_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
};
|
||||
let interact = Interact::new(Arc::new(plugin), dummy_entry, std::ptr::null_mut());
|
||||
let handle = interact.handle();
|
||||
|
||||
unsafe {
|
||||
assert_eq!(interact_swap_buffers(handle), status::OK);
|
||||
assert_eq!(interact_redraw(handle), status::OK);
|
||||
}
|
||||
assert!(interact.swap_requested.load(Ordering::Relaxed));
|
||||
assert!(interact.redraw_requested.load(Ordering::Relaxed));
|
||||
|
||||
let mut props_handle: *mut c_void = std::ptr::null_mut();
|
||||
unsafe {
|
||||
assert_eq!(interact_get_property_set(handle, &mut props_handle), status::OK);
|
||||
}
|
||||
assert_eq!(tag::kind(props_handle), tag::INTERACT);
|
||||
assert_eq!(
|
||||
tag::strip(props_handle) as usize,
|
||||
&interact.props as *const PropertySet as usize
|
||||
);
|
||||
|
||||
// property suite 可直读 interact 属性集(句柄约定打通)。
|
||||
let addr = tag::strip(props_handle);
|
||||
let set: &PropertySet = unsafe { &*addr };
|
||||
assert!(
|
||||
matches!(
|
||||
set.get(crate::host::PROP_INTERACT_BIT_DEPTH, 0),
|
||||
Some(Value::Int(32))
|
||||
),
|
||||
"经 interact 句柄的 property suite 应读到 BitDepth=32"
|
||||
);
|
||||
|
||||
// 摘除引用计数:Arc 释放(interact 已无引用)。
|
||||
drop(interact);
|
||||
}
|
||||
|
||||
/// 假入口:所有 action 返回 ReplyDefault(不触达真实插件)。
|
||||
unsafe extern "C" fn dummy_entry(
|
||||
_action: *const c_char,
|
||||
_handle: *const c_void,
|
||||
_in: *mut c_void,
|
||||
_out: *mut c_void,
|
||||
) -> c_int {
|
||||
status::REPLY_DEFAULT
|
||||
}
|
||||
|
||||
/// call 面透传:entry 收到 action 与 tagged handle(假插件记录)。
|
||||
#[test]
|
||||
fn interact_call_dispatches_to_entry() {
|
||||
static CALLED_ACTION: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
unsafe extern "C" fn recording_entry(
|
||||
action: *const c_char,
|
||||
handle: *const c_void,
|
||||
_in: *mut c_void,
|
||||
_out: *mut c_void,
|
||||
) -> c_int {
|
||||
CALLED_ACTION
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(unsafe { CStr::from_ptr(action) }.to_string_lossy().into_owned());
|
||||
// handle 必须是 tagged INTERACT。
|
||||
assert_eq!(tag::kind(handle as *mut c_void), tag::INTERACT);
|
||||
status::OK
|
||||
}
|
||||
let plugin = crate::host::Plugin {
|
||||
identifier: "fake".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::from("fake"),
|
||||
contexts: Vec::new(),
|
||||
descriptor: crate::descriptor::EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: recording_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
};
|
||||
let interact = Interact::new(Arc::new(plugin), recording_entry, std::ptr::null_mut());
|
||||
|
||||
assert_eq!(interact.describe(), status::OK);
|
||||
assert_eq!(interact.create_instance(), status::OK);
|
||||
assert_eq!(interact.pen_motion((10.0, 20.0), true, 5.0), status::OK);
|
||||
assert_eq!(interact.pen_down((10.0, 20.0), 5.0), status::OK);
|
||||
assert_eq!(interact.pen_up((10.0, 20.0), 5.0), status::OK);
|
||||
assert_eq!(interact.key_down(crate::host::KEY_A, "a", 5.0), status::OK);
|
||||
assert_eq!(interact.key_up(crate::host::KEY_A, "a", 5.0), status::OK);
|
||||
assert_eq!(interact.idle(), status::OK);
|
||||
interact.destroy();
|
||||
|
||||
let calls = CALLED_ACTION.lock().unwrap().clone();
|
||||
let want = [
|
||||
crate::host::ACTION_DESCRIBE,
|
||||
crate::host::ACTION_CREATE_INSTANCE,
|
||||
crate::host::ACTION_INTERACT_PEN_MOTION,
|
||||
crate::host::ACTION_INTERACT_PEN_DOWN,
|
||||
crate::host::ACTION_INTERACT_PEN_UP,
|
||||
crate::host::ACTION_INTERACT_KEY_DOWN,
|
||||
crate::host::ACTION_INTERACT_KEY_UP,
|
||||
crate::host::ACTION_INTERACT_IDLE,
|
||||
crate::host::ACTION_DESTROY_INSTANCE,
|
||||
];
|
||||
assert_eq!(calls, want, "interact action 序列应按序透传到入口");
|
||||
}
|
||||
|
||||
/// pen 事件的真实参数:inArgs 的 canonical 位置/视口位置/压力按
|
||||
/// pixelScale 换算(假插件读取记录)。
|
||||
#[test]
|
||||
fn pen_in_args_canonical_conversion() {
|
||||
// 记录 pen_motion 的 inArgs 关键属性。
|
||||
static CAPTURED: Mutex<Option<(f64, f64, f64)>> = Mutex::new(None);
|
||||
unsafe extern "C" fn capture_entry(
|
||||
_action: *const c_char,
|
||||
_handle: *const c_void,
|
||||
in_args: *mut c_void,
|
||||
_out: *mut c_void,
|
||||
) -> c_int {
|
||||
if in_args.is_null() {
|
||||
return status::OK;
|
||||
}
|
||||
let set = unsafe { &*(in_args as *const PropertySet) };
|
||||
// 视口位置(Int×2)与 canonical 位置(Double×2)与压力。
|
||||
let vx = match set.get(crate::host::PROP_INTERACT_PEN_VIEWPORT_POSITION, 0) {
|
||||
Some(Value::Int(i)) => i,
|
||||
_ => 0,
|
||||
};
|
||||
let px = match set.get(crate::host::PROP_INTERACT_PEN_POSITION, 0) {
|
||||
Some(Value::Double(d)) => d,
|
||||
_ => 0.0,
|
||||
};
|
||||
let pressure = match set.get(crate::host::PROP_INTERACT_PEN_PRESSURE, 0) {
|
||||
Some(Value::Double(d)) => d,
|
||||
_ => 0.0,
|
||||
};
|
||||
*CAPTURED.lock().unwrap() = Some((vx as f64, px, pressure));
|
||||
status::OK
|
||||
}
|
||||
let plugin = crate::host::Plugin {
|
||||
identifier: "fake".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::from("fake"),
|
||||
contexts: Vec::new(),
|
||||
descriptor: crate::descriptor::EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: capture_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
};
|
||||
let interact = Interact::new(Arc::new(plugin), capture_entry, std::ptr::null_mut());
|
||||
// 设 pixelScale=2 → canonical = viewport/2。
|
||||
interact.props.define(
|
||||
crate::host::PROP_INTERACT_PIXEL_SCALE,
|
||||
vec![Value::Double(2.0), Value::Double(2.0)],
|
||||
);
|
||||
|
||||
assert_eq!(interact.pen_motion((10.0, 20.0), true, 0.0), status::OK);
|
||||
let (vx, px, pressure) = CAPTURED.lock().unwrap().take().unwrap();
|
||||
assert_eq!(vx, 10.0);
|
||||
assert_eq!(px, 5.0);
|
||||
assert_eq!(pressure, 1.0);
|
||||
|
||||
// pen_up 压力 0。
|
||||
assert_eq!(interact.pen_up((10.0, 20.0), 0.0), status::OK);
|
||||
let (_, _, pressure) = CAPTURED.lock().unwrap().take().unwrap();
|
||||
assert_eq!(pressure, 0.0);
|
||||
}
|
||||
|
||||
/// key 事件真实参数:keySym/keyString 进 inArgs。
|
||||
#[test]
|
||||
fn key_in_args_carry_sym_and_string() {
|
||||
static CAPTURED: Mutex<Option<(i32, String)>> = Mutex::new(None);
|
||||
unsafe extern "C" fn capture_entry(
|
||||
_action: *const c_char,
|
||||
_handle: *const c_void,
|
||||
in_args: *mut c_void,
|
||||
_out: *mut c_void,
|
||||
) -> c_int {
|
||||
let set = unsafe { &*(in_args as *const PropertySet) };
|
||||
let sym = match set.get(crate::host::PROP_KEY_SYM, 0) {
|
||||
Some(Value::Int(i)) => i,
|
||||
_ => 0,
|
||||
};
|
||||
let s = match set.get(crate::host::PROP_KEY_STRING, 0) {
|
||||
Some(Value::String(c)) => c.to_string_lossy().into_owned(),
|
||||
_ => String::new(),
|
||||
};
|
||||
*CAPTURED.lock().unwrap() = Some((sym, s));
|
||||
status::OK
|
||||
}
|
||||
let plugin = crate::host::Plugin {
|
||||
identifier: "fake".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::from("fake"),
|
||||
contexts: Vec::new(),
|
||||
descriptor: crate::descriptor::EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: capture_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
};
|
||||
let interact = Interact::new(Arc::new(plugin), capture_entry, std::ptr::null_mut());
|
||||
assert_eq!(interact.key_down(crate::host::KEY_RETURN, "", 1.0), status::OK);
|
||||
let (sym, s) = CAPTURED.lock().unwrap().take().unwrap();
|
||||
assert_eq!(sym, crate::host::KEY_RETURN);
|
||||
assert_eq!(s, "");
|
||||
assert_eq!(interact.key_up(crate::host::KEY_A, "a", 1.0), status::OK);
|
||||
let (sym, s) = CAPTURED.lock().unwrap().take().unwrap();
|
||||
assert_eq!(sym, crate::host::KEY_A);
|
||||
assert_eq!(s, "a");
|
||||
}
|
||||
|
||||
/// destroy 幂等:只发一次。
|
||||
#[test]
|
||||
fn destroy_is_idempotent() {
|
||||
static COUNT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
|
||||
unsafe extern "C" fn counting_entry(
|
||||
action: *const c_char,
|
||||
_handle: *const c_void,
|
||||
_in: *mut c_void,
|
||||
_out: *mut c_void,
|
||||
) -> c_int {
|
||||
let a = unsafe { CStr::from_ptr(action) }.to_string_lossy();
|
||||
if a == crate::host::ACTION_DESTROY_INSTANCE {
|
||||
COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
status::OK
|
||||
}
|
||||
let plugin = crate::host::Plugin {
|
||||
identifier: "fake".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::from("fake"),
|
||||
contexts: Vec::new(),
|
||||
descriptor: crate::descriptor::EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: counting_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
};
|
||||
let interact = Interact::new(Arc::new(plugin), counting_entry, std::ptr::null_mut());
|
||||
interact.destroy();
|
||||
interact.destroy();
|
||||
assert_eq!(COUNT.load(std::sync::atomic::Ordering::Relaxed), 1);
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,10 @@
|
||||
//!
|
||||
//! 参照:HS: ofxhImageEffect.cpp:2776(fetchSuite 分发表与版本协商)。
|
||||
|
||||
pub mod draw;
|
||||
pub mod gl_render;
|
||||
pub mod image_effect;
|
||||
pub mod interact;
|
||||
pub mod memory;
|
||||
pub mod message;
|
||||
pub mod multithread;
|
||||
@@ -103,6 +105,9 @@ pub mod tag {
|
||||
pub const CLIP: usize = 5;
|
||||
/// [`crate::image::Image`](clipGetImage 的产物)。
|
||||
pub const IMAGE: usize = 6;
|
||||
/// [`crate::suites::interact::Interact`](interact 实例 handle;
|
||||
/// 实例期 effect/param-set 之外的独立交互对象)。
|
||||
pub const INTERACT: usize = 7;
|
||||
|
||||
/// 打标签(宿主创建对象句柄用;公开供宿主/测试构造句柄)。
|
||||
pub fn make(props: *const crate::property::PropertySet, t: usize) -> *mut std::ffi::c_void {
|
||||
@@ -183,6 +188,11 @@ pub struct GlCtx {
|
||||
/// 全链路 F32,由 render_gl 按插件 kOfxOpenGLPropPixelDepth 协商
|
||||
/// 后填入——纹理句柄的 kOfxImageEffectPropPixelDepth 以它为准)。
|
||||
pub gl_pixel_depth: &'static str,
|
||||
/// 已附着的**真实 GL 输出纹理名**(GL 模式下宿主为输出帧建的
|
||||
/// GL 纹理 + FBO 颜色附件;clipLoadTexture(Output) 的
|
||||
/// OpenGLTextureIndex 以它为准——ofxGPURender.h 要求宿主把输出
|
||||
/// 纹理绑定为当前颜色缓冲)。None = 无真实 GL 名(CPU 回退)。
|
||||
pub output_gl_texture: Option<i32>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -213,6 +223,7 @@ pub(crate) const OFX_API_VERSION: i32 = 105;
|
||||
/// OfxMultiThreadSuite v1。
|
||||
/// 第 2 期追加:OfxImageEffectOpenGLRenderSuite v1(GL 路径);
|
||||
/// ofxColour 无 suite 表(纯属性 + GetOutputColourspace action)。
|
||||
/// 第 3 期追加:OfxInteractSuite v1、OfxDrawSuite v1(interact 宿主)。
|
||||
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()),
|
||||
@@ -226,6 +237,8 @@ pub fn fetch_suite(name: &str, version: i32) -> Option<*const std::ffi::c_void>
|
||||
("OfxTimeLineSuite", 1) => ptr(timeline::suite_v1()),
|
||||
("OfxMultiThreadSuite", 1) => ptr(multithread::suite_v1()),
|
||||
("OfxImageEffectOpenGLRenderSuite", 1) => ptr(gl_render::suite_v1()),
|
||||
("OfxInteractSuite", 1) => ptr(interact::suite_v1()),
|
||||
("OfxDrawSuite", 1) => ptr(draw::suite_v1()),
|
||||
_ => return None,
|
||||
};
|
||||
Some(suite)
|
||||
@@ -240,7 +253,7 @@ fn ptr<T>(p: &'static T) -> *const std::ffi::c_void {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 八张 suite 的分发表:版本精确匹配、未知版本/名字 → None。
|
||||
/// 十张 suite 的分发表:版本精确匹配、未知版本/名字 → None。
|
||||
#[test]
|
||||
fn fetch_suite_dispatch() {
|
||||
assert!(fetch_suite("OfxPropertySuite", 1).is_some());
|
||||
@@ -254,10 +267,14 @@ mod tests {
|
||||
assert!(fetch_suite("OfxTimeLineSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxMultiThreadSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxImageEffectOpenGLRenderSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxInteractSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxDrawSuite", 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("OfxInteractSuite", 2).is_none());
|
||||
assert!(fetch_suite("OfxDrawSuite", 2).is_none());
|
||||
assert!(fetch_suite("OfxBogusSuite", 1).is_none());
|
||||
assert!(fetch_suite("", 1).is_none());
|
||||
}
|
||||
@@ -276,6 +293,7 @@ mod tests {
|
||||
tag::PARAM_INSTANCE,
|
||||
tag::CLIP,
|
||||
tag::IMAGE,
|
||||
tag::INTERACT,
|
||||
] {
|
||||
let h = tag::make(props, t);
|
||||
assert_eq!(tag::kind(h), t);
|
||||
@@ -340,6 +358,7 @@ mod tests {
|
||||
renderer,
|
||||
output_texture: tex.clone(),
|
||||
gl_pixel_depth: "OfxBitDepthFloat",
|
||||
output_gl_texture: Some(42),
|
||||
}));
|
||||
let got = gl_ctx().unwrap();
|
||||
assert_eq!(
|
||||
@@ -348,6 +367,7 @@ mod tests {
|
||||
);
|
||||
assert!(got.output_texture.is_dummy());
|
||||
assert_eq!(got.gl_pixel_depth, "OfxBitDepthFloat");
|
||||
assert_eq!(got.output_gl_texture, Some(42));
|
||||
set_gl_ctx(None);
|
||||
assert!(gl_ctx().is_none());
|
||||
}
|
||||
|
||||
@@ -816,6 +816,7 @@ mod tests {
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
interact: std::sync::Mutex::new(None),
|
||||
});
|
||||
(inst.clone(), instance_handle(&inst))
|
||||
}
|
||||
|
||||
@@ -114,7 +114,14 @@ unsafe extern "C" fn progress_update_v1(_handle: *mut c_void, progress: c_double
|
||||
}
|
||||
|
||||
unsafe extern "C" fn progress_end_v1(_handle: *mut c_void) -> c_int {
|
||||
caught(|| status::OK)
|
||||
caught(|| {
|
||||
CURRENT.with(|c| {
|
||||
if let Some(r) = c.borrow().as_ref() {
|
||||
r.end_ui();
|
||||
}
|
||||
});
|
||||
status::OK
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn progress_start_v2(
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
// 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/>.
|
||||
|
||||
//! GL 渲染桥端到端:真实 CGL 离屏上下文 + 输出纹理/FBO + 回读。
|
||||
//!
|
||||
//! 链路(方案 B,见 [`oakplugin::gl_bridge`] 模块文档):扫描最小测试
|
||||
//! 插件的 GL 变体(org.oak.test-plugin.gl,声明 OpenGLRenderSupported=
|
||||
//! "true" + F32)→ 用 GL 后端渲染器(fake `GpuContextLike`,kind=Gl)
|
||||
//! 构造 RenderJob → `render_frame` 的 use_opengl 决策命中 → 桥建 CGL
|
||||
//! 上下文 + 输出 GL 纹理 + FBO → 插件 render 走 GL 路径(attach/detach、
|
||||
//! clipLoadTexture 拿真实纹理名、glClear 清成已知颜色)→ 宿主
|
||||
//! glReadPixels 回读装帧。
|
||||
//!
|
||||
//! 断言即"真实 GL 渲染"的证据:
|
||||
//! 1. 输出像素 = 插件 GL 清屏色 (0.1, 0.2, 0.3, 1.0)(CPU 路径恒填
|
||||
//! 0.5,二者可区分);
|
||||
//! 2. 插件经 message 上报的 gl-source-index / gl-output-index 都是
|
||||
//! 非零真实 GL 纹理名(CPU 桩恒 0);
|
||||
//! 3. gl-attached / gl-detached 动作发生(attach/detach 配对)。
|
||||
//!
|
||||
//! 门:macOS(Linux/Windows 的 GL 桥是 stub)+ `OAK_GPU_TESTS`(GL
|
||||
//! 验收约定,CI 一律跳过)。
|
||||
|
||||
mod common;
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void, CStr, CString};
|
||||
use std::sync::Arc;
|
||||
|
||||
use oakcore_rs::{PixelFormat, Rational};
|
||||
use oakplugin::host::Host;
|
||||
use oakplugin::render::{Renderer, Texture};
|
||||
use oakrender::backend::{BackendKind, GpuContextLike};
|
||||
use oakrender::texture::Frame;
|
||||
|
||||
const GL_PLUGIN_ID: &str = "org.oak.test-plugin.gl";
|
||||
|
||||
/// GL 后端渲染器(fake `GpuContextLike`;只声明 kind=Gl,让 use_opengl
|
||||
/// 决策命中。GL 路径不用它的 upload/download/blit——输出是 CPU 帧,
|
||||
/// 渲染发生在桥自建的 CGL 上下文)。
|
||||
struct FakeGlRenderer;
|
||||
|
||||
impl GpuContextLike for FakeGlRenderer {
|
||||
fn kind(&self) -> BackendKind {
|
||||
BackendKind::Gl
|
||||
}
|
||||
fn destroy_texture(&self, _token: u64) {}
|
||||
fn upload(&self, _token: u64, _frame: &Frame) -> oakrender::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn download(&self, _token: u64) -> oakrender::error::Result<Frame> {
|
||||
Ok(Frame::new())
|
||||
}
|
||||
fn blit(
|
||||
&self,
|
||||
_src: u64,
|
||||
_dst: u64,
|
||||
_processor: Option<&oakrender::color::ColorProcessor>,
|
||||
) -> oakrender::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 捕获插件 message(gl-test 消息流)。
|
||||
unsafe extern "C" fn capture_msg(
|
||||
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
|
||||
}
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
fn first_pixel(texture: &Texture) -> [f32; 4] {
|
||||
let Texture::Cpu(frame) = texture else {
|
||||
panic!("期望 CPU 帧");
|
||||
};
|
||||
let mut out = [0f32; 4];
|
||||
for i in 0..4 {
|
||||
out[i] = f32::from_le_bytes(frame.data[i * 4..i * 4 + 4].try_into().unwrap());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// 扫描 + 注册(幂等;宿主不可用返回 false = skip)。
|
||||
fn scan_and_register() -> bool {
|
||||
let Some(dir) = common::test_plugin_scan_dir() else {
|
||||
common::skip("最小测试插件未构建");
|
||||
return false;
|
||||
};
|
||||
if Host::global().cache.scan_path(&dir).is_err() {
|
||||
common::skip("测试插件扫描失败");
|
||||
return false;
|
||||
}
|
||||
oakplugin::node_factory::register_plugin_nodes();
|
||||
true
|
||||
}
|
||||
|
||||
/// GL 端到端:插件 GL 变体真实走 GL 路径并输出已知颜色。
|
||||
#[test]
|
||||
fn gl_plugin_renders_through_real_gl_path() {
|
||||
common::with_host(|| {
|
||||
if !cfg!(target_os = "macos") {
|
||||
common::skip("GL 桥仅 macOS 实现(Linux/Windows stub)");
|
||||
return;
|
||||
}
|
||||
if !common::gpu_available() {
|
||||
common::skip("GL 测试需 OAK_GPU_TESTS(本机 GPU 验收;CI 一律跳过)");
|
||||
return;
|
||||
}
|
||||
if !oakplugin::gl_bridge::gl_available() {
|
||||
common::skip("本机无可用 GL 上下文");
|
||||
return;
|
||||
}
|
||||
if !scan_and_register() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut captured: Vec<(String, String)> = Vec::new();
|
||||
oakplugin::suites::message::set_handler(
|
||||
Some(capture_msg),
|
||||
&mut captured as *mut _ as *mut c_void,
|
||||
);
|
||||
|
||||
let inst = Host::global()
|
||||
.create_instance(GL_PLUGIN_ID, None)
|
||||
.expect("GL 变体实例应可建");
|
||||
let reg = oakplugin::node_factory::register_instance(inst.clone());
|
||||
|
||||
let dst = oakrender::eval::generate_frame(Rational::new(0, 1), (4, 4), PixelFormat::F32)
|
||||
.unwrap();
|
||||
let src = oakrender::eval::generate_frame(Rational::new(0, 1), (4, 4), PixelFormat::F32)
|
||||
.unwrap();
|
||||
let renderer: Renderer = Arc::new(FakeGlRenderer);
|
||||
let job = oakplugin::render_driver::RenderJob {
|
||||
time: 0.0,
|
||||
dst: Texture::wrap_frame(dst),
|
||||
src: Some(Texture::wrap_frame(src)),
|
||||
effect_input_id: Some("Source".into()),
|
||||
inputs: Vec::new(),
|
||||
values: Vec::new(),
|
||||
renderer: Some(renderer),
|
||||
clear_destination: false,
|
||||
interactive: false,
|
||||
};
|
||||
let (out, _rois) = oakplugin::render_driver::render_frame(&inst.value, &job)
|
||||
.expect("GL render_frame 应成功");
|
||||
|
||||
oakplugin::node_factory::unregister_instance(reg);
|
||||
oakplugin::suites::message::set_handler(None, std::ptr::null_mut());
|
||||
Host::global().shutdown();
|
||||
|
||||
// 1. 输出像素 = 插件 GL 清屏色(CPU 路径恒 0.5,可区分)。
|
||||
let px = first_pixel(&out);
|
||||
for (i, v) in [0.1, 0.2, 0.3, 1.0].iter().enumerate() {
|
||||
assert!(
|
||||
(px[i] - v).abs() < 1e-4,
|
||||
"GL 回读像素[{i}] = {},期望 {v}(GL 路径未生效?)",
|
||||
px[i]
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 真实 GL 纹理名(非零)+ 3. attach/detach 配对。
|
||||
let msg: Vec<&str> = captured.iter().map(|(_, m)| m.as_str()).collect();
|
||||
for need in [
|
||||
"gl-attached",
|
||||
"gl-detached",
|
||||
"gl-source-index=",
|
||||
"gl-output-index=",
|
||||
] {
|
||||
assert!(
|
||||
msg.iter().any(|m| m.contains(need)),
|
||||
"应捕获到 {need},实际 {msg:?}"
|
||||
);
|
||||
}
|
||||
for m in msg.iter().filter(|m| m.contains("gl-source-index=")) {
|
||||
let v: i32 = m.rsplit('=').next().unwrap().parse().unwrap();
|
||||
assert!(v > 0, "输入 clip 的 OpenGLTextureIndex 应为真实非零 GL 名,got {v}");
|
||||
}
|
||||
for m in msg.iter().filter(|m| m.contains("gl-output-index=")) {
|
||||
let v: i32 = m.rsplit('=').next().unwrap().parse().unwrap();
|
||||
assert!(v > 0, "输出 clip 的 OpenGLTextureIndex 应为真实非零 GL 名,got {v}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// CPU 回退对照:无 GL 渲染器(renderer=None)时,GL 变体插件走 CPU
|
||||
/// 路径(actionRenderGL 的 !gl_enabled 回退 actionRender),输出恒
|
||||
/// 0.5——与 [`gl_plugin_renders_through_real_gl_path`] 的 0.1,0.2,0.3
|
||||
/// 区分,证明 GL 分支只在 renderer 为 GL 时启用。
|
||||
#[test]
|
||||
fn gl_plugin_falls_back_to_cpu_without_gl_renderer() {
|
||||
common::with_host(|| {
|
||||
if !scan_and_register() {
|
||||
return;
|
||||
}
|
||||
let mut captured: Vec<(String, String)> = Vec::new();
|
||||
oakplugin::suites::message::set_handler(
|
||||
Some(capture_msg),
|
||||
&mut captured as *mut _ as *mut c_void,
|
||||
);
|
||||
|
||||
let inst = Host::global()
|
||||
.create_instance(GL_PLUGIN_ID, None)
|
||||
.expect("GL 变体实例应可建");
|
||||
let reg = oakplugin::node_factory::register_instance(inst.clone());
|
||||
|
||||
let dst = oakrender::eval::generate_frame(Rational::new(0, 1), (4, 4), PixelFormat::F32)
|
||||
.unwrap();
|
||||
let src = oakrender::eval::generate_frame(Rational::new(0, 1), (4, 4), PixelFormat::F32)
|
||||
.unwrap();
|
||||
let job = oakplugin::render_driver::RenderJob {
|
||||
time: 0.0,
|
||||
dst: Texture::wrap_frame(dst),
|
||||
src: Some(Texture::wrap_frame(src)),
|
||||
effect_input_id: Some("Source".into()),
|
||||
inputs: Vec::new(),
|
||||
values: Vec::new(),
|
||||
renderer: None,
|
||||
clear_destination: false,
|
||||
interactive: false,
|
||||
};
|
||||
let (out, _rois) = oakplugin::render_driver::render_frame(&inst.value, &job)
|
||||
.expect("CPU 回退 render_frame 应成功");
|
||||
|
||||
oakplugin::node_factory::unregister_instance(reg);
|
||||
oakplugin::suites::message::set_handler(None, std::ptr::null_mut());
|
||||
Host::global().shutdown();
|
||||
|
||||
let px = first_pixel(&out);
|
||||
assert_eq!(px, [0.5, 0.5, 0.5, 1.0], "无 GL 渲染器应走 CPU 恒 0.5");
|
||||
// 未走 GL:不应有 gl-* 消息流。
|
||||
assert!(
|
||||
!captured.iter().any(|(_, m)| m.contains("gl-output-index")),
|
||||
"CPU 回退不应产生 GL suite 消息"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
// 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/>.
|
||||
|
||||
//! Interact 宿主端到端:生命周期 + action 调用面 + Draw suite 真实 GL。
|
||||
//!
|
||||
//! 链路:扫描最小测试插件的 interact 变体(org.oak.test-plugin.interact,
|
||||
//! describe 期声明 kOfxImageEffectPluginPropOverlayInteractV2 → 指向
|
||||
//! mainEntryInteract)→ `new_interact`(发 kOfxActionNewInteract)→
|
||||
//! describe → create_instance → pen/key/idle 事件 → draw(宿主
|
||||
//! gl_bridge 保持 GL current,插件经 Draw suite setColour + 原生 GL 画
|
||||
//! 已知色块)→ 回读断言。
|
||||
//!
|
||||
//! 断言即"真实调用到插件"的证据:插件把每次 action 的实参写入
|
||||
//! `OAK_TEST_PLUGIN_INTERACT_MARKER` 指向的标记文件(unix 下
|
||||
//! `va_list` 由 stdarg.h 承担),测试逐行断言;GL 断言另读回 FBO 像素
|
||||
//! 比对插件绘制颜色(与 CPU 路径可区分)。
|
||||
//!
|
||||
//! 门:事件用例不依赖 GL,任何平台可跑;draw 用例要求 macOS +
|
||||
//! `OAK_GPU_TESTS`(GL 验收约定,CI 一律跳过)。
|
||||
|
||||
mod common;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use oakplugin::suites::{interact::Interact, status};
|
||||
|
||||
const INTERACT_PLUGIN_ID: &str = "org.oak.test-plugin.interact";
|
||||
const BASE_PLUGIN_ID: &str = "org.oak.test-plugin";
|
||||
/// 插件把 interact action 记录写入该环境变量指向的文件。
|
||||
const MARKER_ENV: &str = "OAK_TEST_PLUGIN_INTERACT_MARKER";
|
||||
|
||||
/// 扫描测试插件 + 注册节点(幂等;不可用 → false = skip)。
|
||||
fn scan_and_register() -> bool {
|
||||
let Some(dir) = common::test_plugin_scan_dir() else {
|
||||
common::skip("最小测试插件未构建");
|
||||
return false;
|
||||
};
|
||||
if oakplugin::host::Host::global().cache.scan_path(&dir).is_err() {
|
||||
common::skip("测试插件扫描失败");
|
||||
return false;
|
||||
}
|
||||
oakplugin::node_factory::register_plugin_nodes();
|
||||
true
|
||||
}
|
||||
|
||||
/// 独立标记文件路径(临时目录,进程 id 防串)。
|
||||
fn marker_path(tag: &str) -> PathBuf {
|
||||
std::env::temp_dir().join(format!(
|
||||
"oak-interact-{}-{}.log",
|
||||
std::process::id(),
|
||||
tag
|
||||
))
|
||||
}
|
||||
|
||||
/// 读标记文件全部行。
|
||||
fn read_marker(path: &PathBuf) -> Vec<String> {
|
||||
std::fs::read_to_string(path)
|
||||
.unwrap_or_default()
|
||||
.lines()
|
||||
.map(|l| l.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 无 interact 的插件:new_interact → None(main entry 对
|
||||
/// kOfxActionNewInteract 返回 ReplyDefault 且未声明 overlay 入口)。
|
||||
#[test]
|
||||
fn base_plugin_has_no_interact() {
|
||||
common::with_host(|| {
|
||||
if !scan_and_register() {
|
||||
return;
|
||||
}
|
||||
let inst = oakplugin::host::Host::global()
|
||||
.create_instance(BASE_PLUGIN_ID, None)
|
||||
.expect("base 插件实例应可建");
|
||||
assert!(
|
||||
inst.value.new_interact().is_none(),
|
||||
"无 interact 的插件 new_interact 应为 None"
|
||||
);
|
||||
oakplugin::host::Host::global().shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
/// 生命周期 + 事件调用面:new_interact → describe → create → pen/key/
|
||||
/// idle → destroy。逐条断言插件侧真实记录与参数。
|
||||
#[test]
|
||||
fn interact_lifecycle_and_events() {
|
||||
common::with_host(|| {
|
||||
if !scan_and_register() {
|
||||
return;
|
||||
}
|
||||
let marker = marker_path("events");
|
||||
let _ = std::fs::remove_file(&marker);
|
||||
unsafe { std::env::set_var(MARKER_ENV, &marker) };
|
||||
|
||||
let inst = oakplugin::host::Host::global()
|
||||
.create_instance(INTERACT_PLUGIN_ID, None)
|
||||
.expect("interact 变体实例应可建");
|
||||
|
||||
let interact_arc = inst
|
||||
.value
|
||||
.new_interact()
|
||||
.expect("interact 变体应创建 interact");
|
||||
let interact: &Interact = &interact_arc;
|
||||
assert_eq!(interact.describe(), status::OK);
|
||||
assert_eq!(interact.create_instance(), status::OK);
|
||||
|
||||
// 事件:坐标为视口像素;pixelScale 默认 1 → canonical == 视口。
|
||||
assert_eq!(interact.pen_motion((10.0, 20.0), true, 5.0), status::OK);
|
||||
assert_eq!(interact.pen_down((30.0, 40.0), 5.0), status::OK);
|
||||
assert_eq!(interact.pen_up((30.0, 40.0), 5.0), status::OK);
|
||||
assert_eq!(
|
||||
interact.key_down(oakplugin::host::KEY_A, "a", 5.0),
|
||||
status::OK
|
||||
);
|
||||
assert_eq!(interact.key_up(oakplugin::host::KEY_A, "a", 5.0), status::OK);
|
||||
assert_eq!(interact.idle(), status::OK);
|
||||
interact.destroy();
|
||||
|
||||
unsafe { std::env::remove_var(MARKER_ENV) };
|
||||
let lines = read_marker(&marker);
|
||||
let _ = std::fs::remove_file(&marker);
|
||||
|
||||
// 生命周期序列(按序)。
|
||||
let seq = ["new_interact", "describe", "create", "destroy"];
|
||||
let pos: Vec<Option<usize>> = seq
|
||||
.iter()
|
||||
.map(|s| lines.iter().position(|l| l == s))
|
||||
.collect();
|
||||
assert!(
|
||||
pos.iter().all(|p| p.is_some()),
|
||||
"标记文件缺生命周期动作:{lines:?}"
|
||||
);
|
||||
let pos: Vec<usize> = pos.into_iter().map(|p| p.unwrap()).collect();
|
||||
assert!(
|
||||
pos.windows(2).all(|w| w[0] < w[1]),
|
||||
"生命周期顺序应为 new_interact→describe→create→destroy:{lines:?}"
|
||||
);
|
||||
|
||||
// pen 事件:真实参数(视口位置/canonical/压力;C %g 无尾零)。
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "pen_motion vp=10,20 canon=10,20 pressure=1"),
|
||||
"pen_motion 参数不符:{lines:?}"
|
||||
);
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "pen_down vp=30,40 canon=30,40 pressure=1"),
|
||||
"pen_down 参数不符:{lines:?}"
|
||||
);
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "pen_up vp=30,40 canon=30,40 pressure=0"),
|
||||
"pen_up 参数不符:{lines:?}"
|
||||
);
|
||||
|
||||
// key 事件:真实 keySym/keyString。
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "key_down sym=97 str=a"),
|
||||
"key_down 参数不符:{lines:?}"
|
||||
);
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "key_up sym=97 str=a"),
|
||||
"key_up 参数不符:{lines:?}"
|
||||
);
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "idle"),
|
||||
"idle 未记录:{lines:?}"
|
||||
);
|
||||
|
||||
oakplugin::host::Host::global().shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
/// 返回值如实透传:插件对 Escape 的 key_down 返回 kOfxStatReplyDefault
|
||||
/// (未处理),宿主原样返回 14。
|
||||
#[test]
|
||||
fn interact_passthrough_plugin_status() {
|
||||
common::with_host(|| {
|
||||
if !scan_and_register() {
|
||||
return;
|
||||
}
|
||||
let marker = marker_path("status");
|
||||
let _ = std::fs::remove_file(&marker);
|
||||
unsafe { std::env::set_var(MARKER_ENV, &marker) };
|
||||
|
||||
let inst = oakplugin::host::Host::global()
|
||||
.create_instance(INTERACT_PLUGIN_ID, None)
|
||||
.expect("interact 变体实例应可建");
|
||||
let interact = inst.value.new_interact().expect("interact 应可建");
|
||||
|
||||
// Escape 的 key_down:插件返回 ReplyDefault → 宿主透传。
|
||||
let st = interact.key_down(oakplugin::host::KEY_ESCAPE, "", 0.0);
|
||||
assert_eq!(st, status::REPLY_DEFAULT, "插件 Escape → ReplyDefault 应透传");
|
||||
|
||||
unsafe { std::env::remove_var(MARKER_ENV) };
|
||||
let lines = read_marker(&marker);
|
||||
let _ = std::fs::remove_file(&marker);
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "key_down sym=65307 str="),
|
||||
"Escape key_down 应真实到达插件:{lines:?}"
|
||||
);
|
||||
|
||||
oakplugin::host::Host::global().shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
/// 实例销毁连带销毁 interact(同一生命周期):drop 实例 → 插件收到
|
||||
/// destroy(interact 的 kOfxActionDestroyInstance)。
|
||||
#[test]
|
||||
fn instance_destroy_cleans_up_interact() {
|
||||
common::with_host(|| {
|
||||
if !scan_and_register() {
|
||||
return;
|
||||
}
|
||||
let marker = marker_path("destroy");
|
||||
let _ = std::fs::remove_file(&marker);
|
||||
unsafe { std::env::set_var(MARKER_ENV, &marker) };
|
||||
|
||||
let inst = oakplugin::host::Host::global()
|
||||
.create_instance(INTERACT_PLUGIN_ID, None)
|
||||
.expect("interact 变体实例应可建");
|
||||
let _interact = inst.value.new_interact().expect("interact 应可建");
|
||||
|
||||
// drop 实例(Arc 归零 → Drop → notify_destroy → interact destroy)。
|
||||
drop(inst);
|
||||
|
||||
unsafe { std::env::remove_var(MARKER_ENV) };
|
||||
let lines = read_marker(&marker);
|
||||
let _ = std::fs::remove_file(&marker);
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "destroy"),
|
||||
"实例销毁应连带 interact destroy:{lines:?}"
|
||||
);
|
||||
|
||||
oakplugin::host::Host::global().shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
/// draw 端到端(真实 GL):宿主 acquire → 建输出纹理/FBO → 调 draw →
|
||||
/// 插件 glClear 暗背景 + Draw suite setColour(0.9,0.1,0.2,1) +
|
||||
/// draw(Rectangle 10..30) → 回读断言矩形区域颜色与背景色。
|
||||
///
|
||||
/// 同时断言 Draw suite 状态真实往返:setColour/getColour 在插件侧返回
|
||||
/// OK(记录在标记文件)。
|
||||
#[test]
|
||||
fn interact_draw_renders_plugin_colours() {
|
||||
common::with_host(|| {
|
||||
if !cfg!(target_os = "macos") {
|
||||
common::skip("GL 桥仅 macOS 实现");
|
||||
return;
|
||||
}
|
||||
if !common::gpu_available() {
|
||||
common::skip("GL 测试需 OAK_GPU_TESTS");
|
||||
return;
|
||||
}
|
||||
if !oakplugin::gl_bridge::gl_available() {
|
||||
common::skip("本机无可用 GL 上下文");
|
||||
return;
|
||||
}
|
||||
if !scan_and_register() {
|
||||
return;
|
||||
}
|
||||
let marker = marker_path("draw");
|
||||
let _ = std::fs::remove_file(&marker);
|
||||
unsafe { std::env::set_var(MARKER_ENV, &marker) };
|
||||
|
||||
let inst = oakplugin::host::Host::global()
|
||||
.create_instance(INTERACT_PLUGIN_ID, None)
|
||||
.expect("interact 变体实例应可建");
|
||||
let interact = inst.value.new_interact().expect("interact 应可建");
|
||||
|
||||
let (w, h) = (64i32, 64i32);
|
||||
let params = oakplugin::render::VideoParams {
|
||||
width: w,
|
||||
height: h,
|
||||
format: oakplugin::render::PIXEL_FORMAT_F32,
|
||||
..Default::default()
|
||||
};
|
||||
// 一次 acquire 覆盖 FBO 装配 → draw(内部嵌套 acquire)→ 回读。
|
||||
let _guard = oakplugin::gl_bridge::acquire().expect("acquire 应成功");
|
||||
let tex = oakplugin::gl_bridge::create_output_texture(w, h, ¶ms)
|
||||
.expect("输出纹理应可建");
|
||||
let fbo = oakplugin::gl_bridge::create_fbo(tex, w, h).expect("FBO 应完整");
|
||||
oakplugin::gl_bridge::bind_fbo(fbo);
|
||||
oakplugin::gl_bridge::set_viewport(w, h);
|
||||
|
||||
let st = interact.draw((64.0, 64.0), (1.0, 1.0), 5.0, None);
|
||||
assert_eq!(st, status::OK, "draw 应返回插件 OK");
|
||||
|
||||
let img = oakplugin::gl_bridge::read_pixels_to_image(w, h, ¶ms).expect("回读应成功");
|
||||
oakplugin::gl_bridge::delete_fbo(fbo);
|
||||
oakplugin::gl_bridge::delete_gl_texture(tex);
|
||||
drop(_guard);
|
||||
|
||||
unsafe { std::env::remove_var(MARKER_ENV) };
|
||||
let lines = read_marker(&marker);
|
||||
let _ = std::fs::remove_file(&marker);
|
||||
|
||||
// Draw suite 状态真实往返(插件侧记录)。
|
||||
assert!(
|
||||
lines.iter().any(|l| {
|
||||
l.starts_with("draw vp=64x64") && l.contains("setcol_st=0")
|
||||
&& l.contains("setcol=0.9,0.1,0.2,1")
|
||||
&& l.contains("getcol_st=0") && l.contains("getcol=0,0,0,1")
|
||||
&& l.contains("draw_st=0")
|
||||
}),
|
||||
"draw 记录不符(setcol/getcol/draw 应 OK):{lines:?}"
|
||||
);
|
||||
|
||||
// 像素断言:矩形区域 (10..30)² 的 GL 像素 → 翻转后 frame y 33..53。
|
||||
let px = |x: usize, y: usize| -> [f32; 4] {
|
||||
let p = &img.pixels()[y * (w as usize) * 4 + x * 4..y * (w as usize) * 4 + x * 4 + 4];
|
||||
[
|
||||
f32::from_ne_bytes(p[0..4].try_into().unwrap()),
|
||||
f32::from_ne_bytes(p[4..8].try_into().unwrap()),
|
||||
f32::from_ne_bytes(p[8..12].try_into().unwrap()),
|
||||
f32::from_ne_bytes(p[12..16].try_into().unwrap()),
|
||||
]
|
||||
};
|
||||
// 角上(rect 外)应为插件清屏暗背景。
|
||||
let corner = px(0, 0);
|
||||
for (i, v) in [0.05f32, 0.05, 0.05, 1.0].iter().enumerate() {
|
||||
assert!(
|
||||
(corner[i] - v).abs() < 1e-4,
|
||||
"角像素[{i}] = {},期望暗背景 {v}",
|
||||
corner[i]
|
||||
);
|
||||
}
|
||||
// rect 内部(frame (20,40) = GL (20,23))应为 setColour 颜色。
|
||||
let inside = px(20, 40);
|
||||
for (i, v) in [0.9f32, 0.1, 0.2, 1.0].iter().enumerate() {
|
||||
assert!(
|
||||
(inside[i] - v).abs() < 1e-4,
|
||||
"矩形内像素[{i}] = {},期望 {v}",
|
||||
inside[i]
|
||||
);
|
||||
}
|
||||
// 整帧只应有两种颜色(背景 + 矩形),矩形覆盖 ~400 像素。
|
||||
let (mut rect_n, mut clear_n) = (0usize, 0usize);
|
||||
for y in 0..h as usize {
|
||||
for x in 0..w as usize {
|
||||
let c = px(x, y);
|
||||
let is_rect = (0..4).all(|i| (c[i] - [0.9, 0.1, 0.2, 1.0][i]).abs() < 1e-3);
|
||||
let is_clear = (0..4).all(|i| (c[i] - [0.05, 0.05, 0.05, 1.0][i]).abs() < 1e-3);
|
||||
if is_rect {
|
||||
rect_n += 1;
|
||||
} else if is_clear {
|
||||
clear_n += 1;
|
||||
} else {
|
||||
panic!("意外颜色像素 ({x},{y}): {c:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
(rect_n as i32 - 400).abs() <= 16,
|
||||
"矩形应覆盖 ~400 像素,got {rect_n}"
|
||||
);
|
||||
assert_eq!(rect_n + clear_n, (w * h) as usize, "整帧应为背景+矩形两色");
|
||||
|
||||
oakplugin::host::Host::global().shutdown();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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/>.
|
||||
|
||||
//! push button → `kOfxActionInstanceChanged` 路由(端到端)。
|
||||
//!
|
||||
//! 链路:`scan_path`(最小测试插件,cbits/oak_test_plugin.c,describe
|
||||
//! 里定义了 push button 参数 "button",instanceChanged 时按
|
||||
//! `OAK_TEST_PLUGIN_INSTANCECHANGED_MARKER` 指向的文件追加一行)→
|
||||
//! `create_instance` → `push_button_clicked`(置值 + 路由
|
||||
//! UserEdited instanceChanged)→ 插件记录回调次数。
|
||||
//!
|
||||
//! 测试插件未构建时 skip(common 约定)。宿主单例经 `common::with_host`
|
||||
//! 串行化。
|
||||
|
||||
mod common;
|
||||
|
||||
use oakplugin::host::Host;
|
||||
|
||||
const PLUGIN_ID: &str = "org.oak.test-plugin";
|
||||
|
||||
#[test]
|
||||
fn push_button_press_routes_instance_changed() {
|
||||
common::with_host(|| {
|
||||
let Some(dir) = common::test_plugin_scan_dir() else {
|
||||
common::skip("最小测试插件未构建");
|
||||
return;
|
||||
};
|
||||
if Host::global().cache.scan_path(&dir).is_err() {
|
||||
common::skip("测试插件扫描失败");
|
||||
return;
|
||||
}
|
||||
let inst = Host::global()
|
||||
.create_instance(PLUGIN_ID, None)
|
||||
.expect("实例");
|
||||
let id = oakplugin::node_factory::register_instance(inst.clone());
|
||||
|
||||
// The C plugin appends one line per instanceChanged("button") to the
|
||||
// marker file; the assertion reads it back.
|
||||
let marker = std::env::temp_dir().join(format!("oak-push-{}.log", std::process::id()));
|
||||
let _ = std::fs::remove_file(&marker);
|
||||
std::env::set_var("OAK_TEST_PLUGIN_INSTANCECHANGED_MARKER", &marker);
|
||||
|
||||
// The button param exists and is a push button.
|
||||
let p = inst.value.params.find("button").expect("button 参数");
|
||||
assert_eq!(p.def.ofx_type, oakplugin::param::TYPE_PUSHBUTTON);
|
||||
|
||||
// First press: set + routed to the plugin.
|
||||
assert!(oakplugin::node_factory::push_button_clicked(id, "button"));
|
||||
// Unknown / non-button params are rejected without touching the entry.
|
||||
assert!(!oakplugin::node_factory::push_button_clicked(id, "gain"));
|
||||
assert!(!oakplugin::node_factory::push_button_clicked(id, "nope"));
|
||||
assert!(!oakplugin::node_factory::push_button_clicked(u64::MAX, "button"));
|
||||
// Second press on the real button: routed again.
|
||||
assert!(oakplugin::node_factory::push_button_clicked(id, "button"));
|
||||
|
||||
// Exactly the two real presses reached the plugin's instanceChanged.
|
||||
let log = std::fs::read_to_string(&marker).expect("marker 文件应存在");
|
||||
assert_eq!(log.lines().count(), 2, "marker log:\n{log}");
|
||||
assert!(log.contains("instanceChanged"), "marker log:\n{log}");
|
||||
|
||||
std::env::remove_var("OAK_TEST_PLUGIN_INSTANCECHANGED_MARKER");
|
||||
let _ = std::fs::remove_file(&marker);
|
||||
oakplugin::node_factory::unregister_instance(id);
|
||||
Host::global().shutdown();
|
||||
});
|
||||
}
|
||||
@@ -124,6 +124,7 @@ fn make_instance() -> (std::sync::Arc<Instance>, *mut c_void) {
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(oakplugin::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
interact: std::sync::Mutex::new(None),
|
||||
});
|
||||
let h = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
|
||||
(inst, h)
|
||||
@@ -309,6 +310,7 @@ fn image_effect_clip_image_pairing() {
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(oakplugin::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
interact: std::sync::Mutex::new(None),
|
||||
});
|
||||
let ih = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
|
||||
let mut clip_h: *mut c_void = std::ptr::null_mut();
|
||||
|
||||
@@ -119,6 +119,15 @@ pub const TYPE_FRAME_FAILED: &str = "frame_failed";
|
||||
/// `"render_audio_batch"` (protocol v2, M15 S3): a batch of audio range
|
||||
/// pulls rendered into the same shm slot transport as video frames.
|
||||
pub const TYPE_RENDER_AUDIO_BATCH: &str = "render_audio_batch";
|
||||
/// `"plugin_progress"` (protocol v2): worker->main — one OFX plugin
|
||||
/// progress event (progressStart/Update/End forwarded over the control
|
||||
/// plane). The main process drains it into the plugin-progress dialog.
|
||||
pub const TYPE_PLUGIN_PROGRESS: &str = "plugin_progress";
|
||||
/// `"plugin_cancel"` (protocol v2): main->worker — the user cancelled the
|
||||
/// plugin render. The worker sets its sticky cancel flag; every live
|
||||
/// progress reporter then answers false (the plugin aborts at its next
|
||||
/// progressUpdate).
|
||||
pub const TYPE_PLUGIN_CANCEL: &str = "plugin_cancel";
|
||||
|
||||
/// Wire-format slot format for 8-bit BGRA frames (M15 S1). The viewer
|
||||
/// preview path requests BGRA8 so the worker converts its F32 pipeline
|
||||
@@ -389,6 +398,46 @@ pub struct FrameFailedMsg {
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
/// `plugin_progress` (worker->main) — one OFX plugin progress event
|
||||
/// forwarded over the control plane.
|
||||
///
|
||||
/// The oakplugin progress suite runs in the worker process (plugin
|
||||
/// rendering is process-isolated); the worker installs a progress reporter
|
||||
/// factory whose reporters push these messages to stdout. Wire shape is
|
||||
/// intentionally the same as the main-process `PluginProgressEvent`:
|
||||
/// progressStart arrives with fraction 0 and label/message set,
|
||||
/// progressUpdate with the fraction, progressEnd with fraction 1.0 (the
|
||||
/// app closes the dialog on >= 1.0, mirroring the main-process reporter —
|
||||
/// the `UiProgressReporter` trait has no end hook, so completion is
|
||||
/// inferred from the fraction there too).
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct PluginProgressMsg {
|
||||
/// The plugin's progressStart label.
|
||||
pub label: String,
|
||||
/// The plugin's progressStart message.
|
||||
pub message: String,
|
||||
/// Progress fraction in 0.0..=1.0.
|
||||
pub fraction: f64,
|
||||
}
|
||||
|
||||
impl PluginProgressMsg {
|
||||
/// Build the wire `plugin_progress` value.
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"type": TYPE_PLUGIN_PROGRESS,
|
||||
"label": self.label,
|
||||
"message": self.message,
|
||||
"fraction": self.fraction,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The wire `plugin_cancel` message (main->worker; no payload).
|
||||
pub fn plugin_cancel_json() -> Value {
|
||||
json!({ "type": TYPE_PLUGIN_CANCEL })
|
||||
}
|
||||
|
||||
/// Build a worker-side error report, mirroring `error_message()` in
|
||||
/// worker.cpp: `{"type":"error","message":...}` plus `"ticket"` when
|
||||
/// non-zero.
|
||||
@@ -1227,6 +1276,43 @@ mod tests {
|
||||
assert_eq!(round, value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_progress_message_round_trips() {
|
||||
let msg = PluginProgressMsg {
|
||||
label: "render".into(),
|
||||
message: "pass 1".into(),
|
||||
fraction: 0.5,
|
||||
};
|
||||
let value = msg.to_json();
|
||||
assert_eq!(value["type"], TYPE_PLUGIN_PROGRESS);
|
||||
assert_eq!(value["label"], "render");
|
||||
assert_eq!(value["message"], "pass 1");
|
||||
assert_eq!(value["fraction"], 0.5);
|
||||
|
||||
// The NDJSON line parses back to the same payload (the wire
|
||||
// contract the worker writes and the dispatcher reads).
|
||||
let line = serde_json::to_string(&value).unwrap();
|
||||
let parsed: PluginProgressMsg = serde_json::from_str(&line).unwrap();
|
||||
assert_eq!(parsed, msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_progress_defaults_on_missing_fields() {
|
||||
// A minimal message (e.g. progressEnd forwarded as fraction 1.0
|
||||
// without touching label/message) deserializes cleanly.
|
||||
let m: PluginProgressMsg =
|
||||
serde_json::from_str(r#"{"type":"plugin_progress","fraction":1.0}"#).unwrap();
|
||||
assert_eq!(m.fraction, 1.0);
|
||||
assert!(m.label.is_empty());
|
||||
assert!(m.message.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_cancel_wire_shape() {
|
||||
let value = plugin_cancel_json();
|
||||
assert_eq!(value["type"], TYPE_PLUGIN_CANCEL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_frame_parse_accepts_cpp_field_names() {
|
||||
let json = r#"{"type":"render_frame","ticket":42,"node":"abcd","time_num":1,"time_den":24,"width":1920,"height":1080,"format":-1,"channels":0,"mode":0,"input_slot":-1,"input_slots":[],"has_color_transform":false,"color_output":"","color_view":"","color_look":""}"#;
|
||||
|
||||
@@ -62,7 +62,7 @@ use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::mpsc;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, Weak};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::{json, Value};
|
||||
@@ -70,10 +70,11 @@ use serde_json::{json, Value};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ipc::{
|
||||
write_message, AudioTicketSpec, BatchAcceptedMsg, BatchTicketSpec, FrameFailedMsg, FrameReadyMsg,
|
||||
FrameSlotMeta, FrameSlotPool, HandshakeMsg, HelloCapsMsg, RenderAudioBatchMsg, RenderBatchMsg,
|
||||
SharedMemoryRegion, ShmMode, WireMontageClip, SLOT_FORMAT_BGRA8, TYPE_BATCH_ACCEPTED,
|
||||
TYPE_ERROR, TYPE_FRAME_FAILED, TYPE_FRAME_READY, TYPE_HANDSHAKE, TYPE_HELLO_CAPS,
|
||||
TYPE_RENDER_AUDIO_BATCH,
|
||||
FrameSlotMeta, FrameSlotPool, HandshakeMsg, HelloCapsMsg, PluginProgressMsg, RenderAudioBatchMsg,
|
||||
RenderBatchMsg, SharedMemoryRegion, ShmMode, WireMontageClip, SLOT_FORMAT_BGRA8,
|
||||
TYPE_BATCH_ACCEPTED, TYPE_ERROR, TYPE_FRAME_FAILED, TYPE_FRAME_READY, TYPE_HANDSHAKE,
|
||||
TYPE_HELLO_CAPS, TYPE_PLUGIN_CANCEL, TYPE_PLUGIN_PROGRESS, TYPE_RENDER_AUDIO_BATCH,
|
||||
plugin_cancel_json,
|
||||
};
|
||||
use crate::scheduler::{FrameKey, FrameRequest, PreviewScheduler, SubmitOutcome};
|
||||
use crate::ticket::{
|
||||
@@ -118,6 +119,59 @@ pub fn reset_main_heap_frame_copies() {
|
||||
MAIN_FRAME_COPIES.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin-progress forwarding (worker -> main) and cancel broadcast
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The app-facing plugin-progress callback: invoked by the dispatcher (on
|
||||
/// the UI tick's poll) for every worker-forwarded `plugin_progress` line
|
||||
/// (label, message, fraction). The app's [`crate::oakui::ofx`] wiring
|
||||
/// forwards these into its `PluginProgressEvent` channel. `Arc` so the
|
||||
/// registry can hand out cheap clones (the callback is `Fn`, not `Clone`).
|
||||
pub type PluginProgressCb = Arc<dyn Fn(String, String, f64) + Send + Sync>;
|
||||
|
||||
static PLUGIN_PROGRESS_CB: OnceLock<Mutex<Option<PluginProgressCb>>> = OnceLock::new();
|
||||
|
||||
/// Register (or clear) the plugin-progress forwarding callback.
|
||||
pub fn set_plugin_progress_cb(cb: Option<PluginProgressCb>) {
|
||||
*PLUGIN_PROGRESS_CB
|
||||
.get_or_init(|| Mutex::new(None))
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = cb;
|
||||
}
|
||||
|
||||
fn plugin_progress_cb() -> Option<PluginProgressCb> {
|
||||
PLUGIN_PROGRESS_CB
|
||||
.get_or_init(|| Mutex::new(None))
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Weak handle to the live dispatcher, registered by
|
||||
/// [`ProcessDispatcher::new`] so the cancel broadcast can reach the
|
||||
/// workers without threading a handle through the app.
|
||||
static DISPATCHER: OnceLock<Mutex<Weak<ProcessDispatcher>>> = OnceLock::new();
|
||||
|
||||
fn dispatcher_slot() -> &'static Mutex<Weak<ProcessDispatcher>> {
|
||||
DISPATCHER.get_or_init(|| Mutex::new(Weak::new()))
|
||||
}
|
||||
|
||||
/// Broadcast a `plugin_cancel` message to every alive worker: the user
|
||||
/// cancelled the plugin render; the workers set their sticky cancel flag
|
||||
/// and their live progress reporters answer false from then on (the
|
||||
/// plugin aborts at its next progressUpdate). Falls back to a no-op when
|
||||
/// no dispatcher is live (inline/test backends).
|
||||
pub fn request_plugin_cancel_all() {
|
||||
let dispatcher = dispatcher_slot()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.upgrade();
|
||||
if let Some(dispatcher) = dispatcher {
|
||||
dispatcher.broadcast_plugin_cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ShmRegionView — one worker segment as seen from the main process
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -718,7 +772,7 @@ impl ProcessDispatcher {
|
||||
};
|
||||
let bin = resolve_worker_bin(&config)?;
|
||||
let (events_tx, events_rx) = mpsc::channel();
|
||||
Ok(Arc::new(ProcessDispatcher {
|
||||
let dispatcher = Arc::new(ProcessDispatcher {
|
||||
inner: Mutex::new(Inner {
|
||||
config,
|
||||
bin,
|
||||
@@ -734,7 +788,12 @@ impl ProcessDispatcher {
|
||||
started: false,
|
||||
shutting_down: false,
|
||||
}),
|
||||
}))
|
||||
});
|
||||
// Register the weak handle for the plugin-cancel broadcast.
|
||||
*dispatcher_slot()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Arc::downgrade(&dispatcher);
|
||||
Ok(dispatcher)
|
||||
}
|
||||
|
||||
/// Spawn all workers and wait for the handshakes (bounded by
|
||||
@@ -902,6 +961,22 @@ impl ProcessDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
/// Broadcast the plugin-cancel signal to every alive worker (the user
|
||||
/// cancelled the plugin render from the progress dialog). The message
|
||||
/// is a fire-and-forget control line; the worker sets its sticky cancel
|
||||
/// flag and the next reporter update answers false. A send failure
|
||||
/// recycles that worker (it will restart on the next pump).
|
||||
pub fn broadcast_plugin_cancel(&self) {
|
||||
let mut inner = lock(&self.inner);
|
||||
for handle in inner.workers.iter_mut() {
|
||||
if matches!(handle.state, WorkerState::Alive | WorkerState::Starting) {
|
||||
if self.send_json(handle, &plugin_cancel_json()).is_err() {
|
||||
handle.state = WorkerState::Dead;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- internals ------------------------------------------------------
|
||||
|
||||
fn pump(&self, inner: &mut Inner, fired: &mut Vec<(Completion, TicketResult)>) {
|
||||
@@ -1046,6 +1121,16 @@ impl ProcessDispatcher {
|
||||
}
|
||||
}
|
||||
}
|
||||
TYPE_PLUGIN_PROGRESS => {
|
||||
// A worker forwarded an OFX plugin progress event; hand it
|
||||
// to the app's registered callback (which drives the
|
||||
// plugin-progress dialog).
|
||||
if let Ok(progress) = serde_json::from_value::<PluginProgressMsg>(msg) {
|
||||
if let Some(cb) = plugin_progress_cb() {
|
||||
cb(progress.label, progress.message, progress.fraction);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1902,4 +1987,55 @@ mod tests {
|
||||
reset_main_heap_frame_copies();
|
||||
assert_eq!(main_heap_frame_copies(), 0);
|
||||
}
|
||||
|
||||
/// A worker's `plugin_progress` NDJSON line is forwarded to the
|
||||
/// registered app callback as (label, message, fraction) — the seam
|
||||
/// that drives the main-process plugin-progress dialog.
|
||||
#[test]
|
||||
fn plugin_progress_line_forwards_to_callback() {
|
||||
let config = DispatcherConfig {
|
||||
worker_bin: Some(std::path::PathBuf::from("/bin/true")),
|
||||
workers: 1,
|
||||
slots_per_worker: 2,
|
||||
width: 16,
|
||||
height: 16,
|
||||
batch_size: 1,
|
||||
..Default::default()
|
||||
};
|
||||
let dispatcher = ProcessDispatcher::new(config).expect("dispatcher");
|
||||
// `new` registered the weak handle (the cancel broadcast seam).
|
||||
assert!(dispatcher_slot().lock().unwrap().upgrade().is_some());
|
||||
|
||||
let received: Arc<Mutex<Vec<(String, String, f64)>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
set_plugin_progress_cb(Some(Arc::new({
|
||||
let received = received.clone();
|
||||
move |label, message, fraction| {
|
||||
received.lock().unwrap().push((label, message, fraction));
|
||||
}
|
||||
})));
|
||||
|
||||
// A fake worker handle so on_line has a target (no spawn needed).
|
||||
{
|
||||
let mut inner = dispatcher.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let key = SharedMemoryRegion::make_key(std::process::id() as i64, 999);
|
||||
let shm = ShmRegionView::create(&key, 2, 256).expect("shm");
|
||||
inner.workers.push(WorkerHandle::shell(0, 0, shm, 2, 256));
|
||||
}
|
||||
|
||||
let mut fired = Vec::new();
|
||||
{
|
||||
let mut inner = dispatcher.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
dispatcher.on_line(
|
||||
&mut inner,
|
||||
0,
|
||||
r#"{"type":"plugin_progress","label":"render","message":"pass 1","fraction":0.5}"#,
|
||||
&mut fired,
|
||||
);
|
||||
}
|
||||
set_plugin_progress_cb(None);
|
||||
|
||||
let events = received.lock().unwrap().clone();
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_eq!(events[0], ("render".to_string(), "pass 1".to_string(), 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user