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:
2026-08-19 22:14:54 +08:00
parent 29696479b5
commit b4ceaa9cab
28 changed files with 5481 additions and 161 deletions
+117 -3
View File
@@ -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
View File
@@ -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 OfxImageEffectOpenGLRenderSuiteV1M11 §4 新增)
draw.rs OfxDrawSuiteV1OFX 1.5interact 绘制;真实 GL 渲染)
interact.rs OfxInteractSuiteV1 + Interact 宿主对象(interact 宿主)
host.rs Host 单例:bundle 扫描、插件缓存、action 分发
descriptor.rs EffectDescriptor/ClipDescriptordescribe 的产物)
instance.rs Instanceaction 调用面(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 → renderin 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(对齐现有失败语义)。LinuxEGL/WindowsWGL
预留 cfg stub。
- 验证:`gl_bridge.rs` 单元测试(clear 已知颜色 → 回读逐像素断言,
`OAK_GPU_TESTS` 门)+ `tests/gl_render_test.rs` 端到端(GL 测试插件
真实走 GL 路径、输出已知颜色)。
### ofxColourOFX 1.4
@@ -150,6 +170,63 @@ src/
(输入 clip 纹理)两个 POD 随附。GL 契约(上下文 current + 输出
附着)见头文件文档注释。
## OFX Interact 宿主(interact + Draw suiteM11 §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 suitePixelScale / ViewportSize /
BackgroundImage / SuggestedColour / SlaveToParam / BackgroundColour /
BitDepth / HasAlphaofxInteract.h 的 PropertiesInteractViewportSize
取 OFX 1.3 名 "OfxInteractPropViewport"、BackgroundImage 与 Idle 为
任务契约扩展)。
- **`suites/draw.rs`**`OfxDrawSuiteV1`OFX 1.5vendored 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)——
官方 openfxBSD-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 互
操作评估——方案 Awgpu-hal GL 互操作)在 macOS 不可行(Metal
后端无 GL 命名空间);方案 B(独立离屏 GL 上下文 + 回读 +
wgpu upload)技术可行但暂缓(无真实 GL 插件可验证 + 需新 GL 依
赖 + 每帧同步回读 stall)。`texture_id` 保持恒 0GL 插件经 CPU
render action 正确出帧。
- **`gl_bridge.rs`方案 B 落地**`texture_id` 桩的 GL 互操作评估
结论——方案 Awgpu-hal GL 互操作)在 macOS 不可行(Metal 后端无
GL 命名空间);**方案 B(独立离屏 GL 上下文 + 回读)已落地**
macOS CGL 离屏上下文(进程级共享、`acquire()` 串行 + current)、
输出 GL 纹理/FBO 挂载、glReadPixels 回读装帧(垂直翻转、格式转
换)、输入 clip 真实 GL 纹理上传。use_opengl 决策与 GL suite 的
OpenGLTextureIndex 接通真实 GL 名。LinuxEGL/WindowsWGL
预留 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`
+15 -8
View File
@@ -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(), "测试插件编译失败");
+282 -1
View File
@@ -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", &paramProps);
if (st != kOfxStatOK)
return st;
propSetString(paramProps, kOfxPropLabel, 0, "Button");
/* clipSource + 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 buttonkOfxPropName == "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:读 inArgsviewport/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 entrydescribe 建参数/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;
}
+183
View File
@@ -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
+529
View File
@@ -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
+62
View File
@@ -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
+200
View File
@@ -181,9 +181,155 @@ pub(crate) const ACTION_GL_CONTEXT_ATTACHED: &str = "OfxActionOpenGLContextAttac
pub(crate) const ACTION_GL_CONTEXT_DETACHED: &str = "kOfxActionOpenGLContextDetached";
/// kOfxImageEffectActionGetOutputColourspaceofxColour.h:283)。
pub(crate) const ACTION_GET_OUTPUT_COLOURSPACE: &str = "OfxImageEffectActionGetOutputColourspace";
/// kOfxActionInstanceChangedofxCore.h:449):宿主侧参数/时间变更
/// 通知。按下 push button 后宿主须以 kOfxChangeUserEdited 的原因调用
/// 它(ofxCore.h:405-435 的 inArgs 契约)。
pub(crate) const ACTION_INSTANCE_CHANGED: &str = "OfxActionInstanceChanged";
// ---- InteractOFX 自定义交互;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";
/// kOfxInteractActionDrawofxInteract.h:265)。
pub(crate) const ACTION_INTERACT_DRAW: &str = "OfxInteractActionDraw";
/// kOfxInteractActionPenMotionofxInteract.h:302)。
pub(crate) const ACTION_INTERACT_PEN_MOTION: &str = "OfxInteractActionPenMotion";
/// kOfxInteractActionPenDownofxInteract.h:340)。
pub(crate) const ACTION_INTERACT_PEN_DOWN: &str = "OfxInteractActionPenDown";
/// kOfxInteractActionPenUpofxInteract.h:376)。
pub(crate) const ACTION_INTERACT_PEN_UP: &str = "OfxInteractActionPenUp";
/// kOfxInteractActionKeyDownofxInteract.h:410)。
pub(crate) const ACTION_INTERACT_KEY_DOWN: &str = "OfxInteractActionKeyDown";
/// kOfxInteractActionKeyUpofxInteract.h:443)。
pub(crate) const ACTION_INTERACT_KEY_UP: &str = "OfxInteractActionKeyUp";
/// kOfxInteractActionGainFocusofxInteract.h:501)。
pub(crate) const ACTION_INTERACT_GAIN_FOCUS: &str = "OfxInteractActionGainFocus";
/// kOfxInteractActionLoseFocusofxInteract.h:526)。
pub(crate) const ACTION_INTERACT_LOSE_FOCUS: &str = "OfxInteractActionLoseFocus";
/// kOfxInteractPropPixelScaleofxInteract.h:58):canonical→屏幕像素
/// 换算比例(Double×2)。
pub(crate) const PROP_INTERACT_PIXEL_SCALE: &str = "OfxInteractPropPixelScale";
/// kOfxInteractPropViewportSizeOFX 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";
/// kOfxInteractPropBackgroundColourofxInteract.h:71):宿主视口背景色
/// Double×3)。
pub(crate) const PROP_INTERACT_BACKGROUND_COLOUR: &str = "OfxInteractPropBackgroundColour";
/// kOfxInteractPropSuggestedColourofxInteract.h:86):宿主建议的 overlay
/// 颜色(Double×3;宿主不支持颜色选择时返回 ReplyDefault)。
pub(crate) const PROP_INTERACT_SUGGESTED_COLOUR: &str = "OfxInteractPropSuggestedColour";
/// kOfxInteractPropSlaveToParamofxInteract.h:50):值变化触发 interact
/// 重绘的参数名(String×N)。
pub(crate) const PROP_INTERACT_SLAVE_TO_PARAM: &str = "OfxInteractPropSlaveToParam";
/// kOfxInteractPropPenPositionofxInteract.h:95):笔的 canonical 位置
/// Double×2,只读 inArgs)。
pub(crate) const PROP_INTERACT_PEN_POSITION: &str = "OfxInteractPropPenPosition";
/// kOfxInteractPropPenViewportPositionofxInteract.h:104):笔的视口像素
/// 位置(Int×2,只读 inArgs)。
pub(crate) const PROP_INTERACT_PEN_VIEWPORT_POSITION: &str = "OfxInteractPropPenViewportPosition";
/// kOfxInteractPropPenPressureofxInteract.h:114):笔压(Double×1
/// 0..1;两态笔映射 0/1)。
pub(crate) const PROP_INTERACT_PEN_PRESSURE: &str = "OfxInteractPropPenPressure";
/// kOfxInteractPropBitDepthofxInteract.h:122):interact 帧缓冲位深
/// Int×1,只读)。
pub(crate) const PROP_INTERACT_BIT_DEPTH: &str = "OfxInteractPropBitDepth";
/// kOfxInteractPropHasAlphaofxInteract.h:132):interact 帧缓冲是否含
/// alphaInt×1,只读)。
pub(crate) const PROP_INTERACT_HAS_ALPHA: &str = "OfxInteractPropHasAlpha";
/// kOfxInteractPropDrawContextofxDrawSuite.h:34):Draw suite 上下文句柄
/// Pointerdraw inArgs 携带,插件取来传给 Draw suite 函数)。
pub(crate) const PROP_INTERACT_DRAW_CONTEXT: &str = "OfxInteractPropDrawContext";
/// kOfxPropKeySymofxKeySyms.h:30):键盘事件的关键码(Int×1)。
pub(crate) const PROP_KEY_SYM: &str = "kOfxPropKeySym";
/// kOfxPropKeyStringofxKeySyms.h:49):键盘事件的 UTF-8 字符(String×1)。
pub(crate) const PROP_KEY_STRING: &str = "kOfxPropKeyString";
/// kOfxImageEffectPluginPropOverlayInteractV2ofxImageEffect.h:825):
/// 插件声明的 overlay interact 入口(Pointer→OfxPluginEntryPointV2
/// 要求 Draw suite 绘制)。
pub(crate) const PROP_OVERLAY_INTERACT_V2: &str = "OfxImageEffectPluginPropOverlayInteractV2";
/// kOfxImageEffectPluginPropOverlayInteractV1ofxImageEffect.h:812)。
pub(crate) const PROP_OVERLAY_INTERACT_V1: &str = "OfxImageEffectPluginPropOverlayInteractV1";
/// kOfxImageEffectPropSupportsOverlaysofxImageEffect.h:801):宿主是否
/// 允许插件在输出图像上绘制 overlay(能力宣告)。
pub(crate) const PROP_SUPPORTS_OVERLAYS: &str = "OfxImageEffectPropSupportsOverlays";
// ---- OFX 关键码(ofxKeySyms.hX11 keysym 值,测试/宿主常用子集)----
//
// 公共:app 侧(WG3b)经 [`crate::suites::interact::Interact::key_down`]/
// `key_up` 传关键码。
/// kOfxKey_UnknownofxKeySyms.h:121)。
pub const KEY_UNKNOWN: i32 = 0x0;
/// kOfxKey_BackSpaceofxKeySyms.h:128)。
pub const KEY_BACKSPACE: i32 = 0xFF08;
/// kOfxKey_TabofxKeySyms.h:129)。
pub const KEY_TAB: i32 = 0xFF09;
/// kOfxKey_ReturnofxKeySyms.h:132)。
pub const KEY_RETURN: i32 = 0xFF0D;
/// kOfxKey_EscapeofxKeySyms.h:136)。
pub const KEY_ESCAPE: i32 = 0xFF1B;
/// kOfxKey_DeleteofxKeySyms.h:137)。
pub const KEY_DELETE: i32 = 0xFFFF;
/// kOfxKey_HomeofxKeySyms.h:172)。
pub const KEY_HOME: i32 = 0xFF50;
/// kOfxKey_LeftofxKeySyms.h:173)。
pub const KEY_LEFT: i32 = 0xFF51;
/// kOfxKey_UpofxKeySyms.h:174)。
pub const KEY_UP: i32 = 0xFF52;
/// kOfxKey_RightofxKeySyms.h:175)。
pub const KEY_RIGHT: i32 = 0xFF53;
/// kOfxKey_DownofxKeySyms.h:176)。
pub const KEY_DOWN: i32 = 0xFF54;
/// kOfxKey_Page_UpofxKeySyms.h:178)。
pub const KEY_PAGE_UP: i32 = 0xFF55;
/// kOfxKey_Page_DownofxKeySyms.h:180)。
pub const KEY_PAGE_DOWN: i32 = 0xFF56;
/// kOfxKey_EndofxKeySyms.h:181)。
pub const KEY_END: i32 = 0xFF57;
/// kOfxKey_F1ofxKeySyms.h:252)。
pub const KEY_F1: i32 = 0xFFBE;
/// kOfxKey_Shift_LofxKeySyms.h:315)。
pub const KEY_SHIFT_L: i32 = 0xFFE1;
/// kOfxKey_Control_LofxKeySyms.h:317)。
pub const KEY_CONTROL_L: i32 = 0xFFE3;
/// kOfxKey_Alt_LofxKeySyms.h:323)。
pub const KEY_ALT_L: i32 = 0xFFE9;
/// kOfxKey_spaceofxKeySyms.h:331)。
pub const KEY_SPACE: i32 = 0x020;
/// kOfxKey_aofxKeySyms.h:398)。
pub const KEY_A: i32 = 0x061;
/// kOfxKey_zofxKeySyms.h:423)。
pub const KEY_Z: i32 = 0x07a;
/// kOfxPropChangeReasonofxCore.h:763):instanceChanged 的 inArgs 里
/// 说明变更来源(UserEdited / PluginEdited / Time)。
pub(crate) const PROP_CHANGE_REASON: &str = "OfxPropChangeReason";
/// kOfxChangeUserEditedofxCore.h:792)。
pub(crate) const CHANGE_USER_EDITED: &str = "OfxChangeUserEdited";
/// kOfxChangePluginEditedofxCore.h:795)。
pub(crate) const CHANGE_PLUGIN_EDITED: &str = "OfxChangePluginEdited";
/// kOfxChangeTimeofxCore.h:798)。
pub(crate) const CHANGE_TIME: &str = "OfxChangeTime";
/// kOfxPropTimeofxCore.h:613)。
pub(crate) const PROP_TIME: &str = "OfxPropTime";
/// kOfxPropEffectInstanceofxCore.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 时为
/// descriptorrender 时为 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 V2ofxImageEffect.h:825):插件 describe 期声明
// 自定义交互入口(Pointer→OfxPluginEntryPointV2 要求 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 §4ofxGPURender.h "OpenGL House Keeping"
// 宿主在描述符置 "true")。
props.set_one(PROP_GL_RENDER_SUPPORTED, Value::String(cs("true")));
// overlay 能力宣告(ofxImageEffect.h:801):宿主允许插件在输出
// 图像上绘制 overlayinteract 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(
+158 -1
View File
@@ -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 → renderin args
/// 带 kOfxImageEffectPropOpenGLEnabled=1)→
/// kOfxActionOpenGLContextDetachedofxGPURender.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`:插件无 interactNewInteract 返回 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)
}
/// 已创建的 interactNone = 未创建)。
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
+5 -3
View File
@@ -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 纹理保持恒 0wgpu/Metal 无
//! GL 命名空间),use_opengl 决策与 GL suite 的 OpenGLTextureIndex
//! 改从 [`gl_bridge`] 取真实名。
//!
//! ## 句柄纪律(全 crate 最高优先级约定)
//!
+57 -14
View File
@@ -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
/// UserEditedinArgs 带 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);
}
}
+11
View File
@@ -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-opapp 可据此关闭进度 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();
}
}
}
+14 -11
View File
@@ -33,10 +33,11 @@
//! - 像素格式常量直接别名 [`oakcore_rs::PixelFormat`]。
//!
//! 保留桩(GPU 相关、wgpu 模型无直接 Rust 等价物):
//! [`texture_id`]——wgpu 后端没有 OpenGL 纹理名(旧 C ABI 的
//! `oakrender_texture_id` 语义是 GL 命名空间),恒 0GL suite 的
//! `OpenGLTextureIndex` 属性与 render 驱动的 use_opengl 决策据此
//! 回退 CPU 路径。
//! [`texture_id`]——oakrender 纹理(wgpu/CPU没有 OpenGL 纹理名
//! 恒 0。**GL 模式的真实 GL 纹理名来自 [`crate::gl_bridge`]**(方案 B
//! 已落地):render 驱动为输出帧建 GL 纹理 + FBOGL 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 等价物。恒 0GL 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
}
+105 -15
View File
@@ -47,8 +47,10 @@
//! 10. 参数覆盖(pluginrenderer.cpp:132-290 apply_param_overrides);
//! 11. render actionCPU 路径经 [`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_openglpluginrenderer.cpp:1446-1457):插件声明 GL 支持
// 且渲染器是 OpenGL 且目标纹理有 GL id 且像素深度协商可行
// (管线 F32 满足插件 kOfxOpenGLPropPixelDepth 声明)
// `texture_id` 为桩恒 0wgpu 无 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 actionOpenGLEnabled=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
+795
View File
@@ -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/>.
//! OfxDrawSuiteV1OFX 1.5vendored 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`] 保持 currentdraw action 全期)。
//!
//! ## 绘制实现(真实 GL
//!
//! gl_bridge 的 CGL 上下文是 **3.2 core profile**(无固定管线,
//! glBegin 不可用)——本套件的 draw 用最小着色器 + VAO/VBO 真实渲染:
//! 正交投影把 canonical 坐标映射到 NDCcanonical 宽 = 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 {
/// kOfxDrawPrimitiveLinesn 点画 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;
/// kOfxDrawPrimitiveEllipse2 对角点包围盒内的轴对齐椭圆**线框**)。
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 {
/// 当前绘制颜色(RGBAsetColour 写入,draw 时作为着色器 uniform)。
pub colour: [f32; 4],
/// 当前线宽(setLineWidthGL 线绘制 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 外 → FailedofxDrawSuite.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],
// TEXToverlay 文本。
[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/VBO3.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(())
}
}
/// 非 macOSDraw 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)
}
}
+90 -39
View File
@@ -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 输入纹理同步删除
/// (上下文 currentrender_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(&params, 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 {
+814
View File
@@ -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 → kOfxActionDescribekOfxActionDescribeInteract
//! create_instance→ kOfxActionCreateInstancekOfxActionCreateInstanceInteract
//! draw/pen/key/idle → kOfxInteractAction*(宿主→插件)
//! destroy → kOfxActionDestroyInstancekOfxActionDestroyInstanceInteract
//! 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)。
//! - keykOfxPropKeySymofxKeySyms.h 关键码)+ kOfxPropKeyString
//! UTF-8;无 UTF8 编码的键为空串)。
//! - draw:宿主在调用前经 [`crate::gl_bridge::acquire`] 保持 GL current
//! 整个 action(插件发原生 GL 命令或经 Draw suiteofxInteract.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 INSTANCEinteract 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 handletagged 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)
}
/// 当前 pixelScaleinteract 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))
}
/// describekOfxActionDescribe == kOfxActionDescribeInteract
/// ofxInteract.h:171)。inArgs/outArgs 冗余为 NULL。
pub fn describe(&self) -> i32 {
let empty = PropertySet::new();
self.call(crate::host::ACTION_DESCRIBE, &empty, &empty)
}
/// createInstancekOfxActionCreateInstance ==
/// kOfxActionCreateInstanceInteractofxInteract.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 actionofxInteract.h:265)。宿主在调用前
/// [`crate::gl_bridge::acquire`] 保持 GL current 整个 actioninArgs
/// 带 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 的 inArgsPenMotion/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 像素 / pixelScalepixelScale 是
// 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_motionofxInteract.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_downofxInteract.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_upofxInteract.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 的 inArgsKeyDown/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_downofxInteract.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_upofxInteract.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_focusofxInteract.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_focusofxInteract.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)
}
/// destroykOfxActionDestroyInstance == 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))
}
// ---- OfxInteractSuiteV1ofxInteract.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);
}
/// 空指针/错标签 → ErrBadHandleinteract 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);
}
}
+21 -1
View File
@@ -26,8 +26,10 @@
//!
//! 参照:HS: ofxhImageEffect.cpp:2776fetchSuite 分发表与版本协商)。
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 v1GL 路径);
/// ofxColour 无 suite 表(纯属性 + GetOutputColourspace action)。
/// 第 3 期追加:OfxInteractSuite v1、OfxDrawSuite v1interact 宿主)。
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());
}
+1
View File
@@ -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))
}
+8 -1
View File
@@ -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(
+262
View File
@@ -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 配对)。
//!
//! 门:macOSLinux/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(())
}
}
/// 捕获插件 messagegl-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 消息"
);
});
}
+372
View File
@@ -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 → Nonemain 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 实例 → 插件收到
/// destroyinteract 的 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, &params)
.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, &params).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.cdescribe
//! 里定义了 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();
});
}
+2
View File
@@ -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();
+86
View File
@@ -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":""}"#;
+143 -7
View File
@@ -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));
}
}