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
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(