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:
File diff suppressed because it is too large
Load Diff
@@ -181,9 +181,155 @@ pub(crate) const ACTION_GL_CONTEXT_ATTACHED: &str = "OfxActionOpenGLContextAttac
|
||||
pub(crate) const ACTION_GL_CONTEXT_DETACHED: &str = "kOfxActionOpenGLContextDetached";
|
||||
/// kOfxImageEffectActionGetOutputColourspace(ofxColour.h:283)。
|
||||
pub(crate) const ACTION_GET_OUTPUT_COLOURSPACE: &str = "OfxImageEffectActionGetOutputColourspace";
|
||||
/// kOfxActionInstanceChanged(ofxCore.h:449):宿主侧参数/时间变更
|
||||
/// 通知。按下 push button 后宿主须以 kOfxChangeUserEdited 的原因调用
|
||||
/// 它(ofxCore.h:405-435 的 inArgs 契约)。
|
||||
pub(crate) const ACTION_INSTANCE_CHANGED: &str = "OfxActionInstanceChanged";
|
||||
// ---- Interact(OFX 自定义交互;ofxInteract.h / ofxDrawSuite.h / ofxKeySyms.h)----
|
||||
|
||||
/// kOfxActionNewInteract:宿主创建 interact 时发给 interact 入口的
|
||||
/// 首个 action。vendored ofxInteract.h 未定义该宏(官方头文件无此
|
||||
/// action;任务契约按 "向插件 main entry 发 kOfxActionNewInteract"
|
||||
/// 命名)——取值 "OfxActionNewInteract",与 OFX action 命名惯例一致。
|
||||
/// 插件不实现该 action 时返回 kOfxStatReplyDefault(宿主视为"无
|
||||
/// interact"或"不参与 NewInteract 协议",见 [`crate::instance::Instance::new_interact`])。
|
||||
pub(crate) const ACTION_NEW_INTERACT: &str = "OfxActionNewInteract";
|
||||
/// kOfxInteractActionIdle:宿主空闲泵(任务契约;vendored ofxInteract.h
|
||||
/// 未收录——OFX 官方规范无 Idle action,属本宿主扩展,取值
|
||||
/// "OfxInteractActionIdle")。插件实现与否自愿;未处理返回
|
||||
/// kOfxStatReplyDefault。
|
||||
pub(crate) const ACTION_INTERACT_IDLE: &str = "OfxInteractActionIdle";
|
||||
/// kOfxInteractActionDraw(ofxInteract.h:265)。
|
||||
pub(crate) const ACTION_INTERACT_DRAW: &str = "OfxInteractActionDraw";
|
||||
/// kOfxInteractActionPenMotion(ofxInteract.h:302)。
|
||||
pub(crate) const ACTION_INTERACT_PEN_MOTION: &str = "OfxInteractActionPenMotion";
|
||||
/// kOfxInteractActionPenDown(ofxInteract.h:340)。
|
||||
pub(crate) const ACTION_INTERACT_PEN_DOWN: &str = "OfxInteractActionPenDown";
|
||||
/// kOfxInteractActionPenUp(ofxInteract.h:376)。
|
||||
pub(crate) const ACTION_INTERACT_PEN_UP: &str = "OfxInteractActionPenUp";
|
||||
/// kOfxInteractActionKeyDown(ofxInteract.h:410)。
|
||||
pub(crate) const ACTION_INTERACT_KEY_DOWN: &str = "OfxInteractActionKeyDown";
|
||||
/// kOfxInteractActionKeyUp(ofxInteract.h:443)。
|
||||
pub(crate) const ACTION_INTERACT_KEY_UP: &str = "OfxInteractActionKeyUp";
|
||||
/// kOfxInteractActionGainFocus(ofxInteract.h:501)。
|
||||
pub(crate) const ACTION_INTERACT_GAIN_FOCUS: &str = "OfxInteractActionGainFocus";
|
||||
/// kOfxInteractActionLoseFocus(ofxInteract.h:526)。
|
||||
pub(crate) const ACTION_INTERACT_LOSE_FOCUS: &str = "OfxInteractActionLoseFocus";
|
||||
|
||||
/// kOfxInteractPropPixelScale(ofxInteract.h:58):canonical→屏幕像素
|
||||
/// 换算比例(Double×2)。
|
||||
pub(crate) const PROP_INTERACT_PIXEL_SCALE: &str = "OfxInteractPropPixelScale";
|
||||
/// kOfxInteractPropViewportSize(OFX 1.3 命名 "OfxInteractPropViewport";
|
||||
/// vendored 1.5 头文件已删,任务契约要求 draw inArgs 携带视口尺寸)。
|
||||
pub(crate) const PROP_INTERACT_VIEWPORT_SIZE: &str = "OfxInteractPropViewport";
|
||||
/// kOfxInteractPropBackgroundImage:任务契约的 draw inArgs 背景图像
|
||||
/// 句柄(Pointer;无背景时为空——本宿主 Phase 1 无合成背景,恒空)。
|
||||
/// 非官方 OFX 属性(官方只有 BackgroundColour),属本宿主扩展。
|
||||
pub(crate) const PROP_INTERACT_BACKGROUND_IMAGE: &str = "OfxInteractPropBackgroundImage";
|
||||
/// kOfxInteractPropBackgroundColour(ofxInteract.h:71):宿主视口背景色
|
||||
/// (Double×3)。
|
||||
pub(crate) const PROP_INTERACT_BACKGROUND_COLOUR: &str = "OfxInteractPropBackgroundColour";
|
||||
/// kOfxInteractPropSuggestedColour(ofxInteract.h:86):宿主建议的 overlay
|
||||
/// 颜色(Double×3;宿主不支持颜色选择时返回 ReplyDefault)。
|
||||
pub(crate) const PROP_INTERACT_SUGGESTED_COLOUR: &str = "OfxInteractPropSuggestedColour";
|
||||
/// kOfxInteractPropSlaveToParam(ofxInteract.h:50):值变化触发 interact
|
||||
/// 重绘的参数名(String×N)。
|
||||
pub(crate) const PROP_INTERACT_SLAVE_TO_PARAM: &str = "OfxInteractPropSlaveToParam";
|
||||
/// kOfxInteractPropPenPosition(ofxInteract.h:95):笔的 canonical 位置
|
||||
/// (Double×2,只读 inArgs)。
|
||||
pub(crate) const PROP_INTERACT_PEN_POSITION: &str = "OfxInteractPropPenPosition";
|
||||
/// kOfxInteractPropPenViewportPosition(ofxInteract.h:104):笔的视口像素
|
||||
/// 位置(Int×2,只读 inArgs)。
|
||||
pub(crate) const PROP_INTERACT_PEN_VIEWPORT_POSITION: &str = "OfxInteractPropPenViewportPosition";
|
||||
/// kOfxInteractPropPenPressure(ofxInteract.h:114):笔压(Double×1,
|
||||
/// 0..1;两态笔映射 0/1)。
|
||||
pub(crate) const PROP_INTERACT_PEN_PRESSURE: &str = "OfxInteractPropPenPressure";
|
||||
/// kOfxInteractPropBitDepth(ofxInteract.h:122):interact 帧缓冲位深
|
||||
/// (Int×1,只读)。
|
||||
pub(crate) const PROP_INTERACT_BIT_DEPTH: &str = "OfxInteractPropBitDepth";
|
||||
/// kOfxInteractPropHasAlpha(ofxInteract.h:132):interact 帧缓冲是否含
|
||||
/// alpha(Int×1,只读)。
|
||||
pub(crate) const PROP_INTERACT_HAS_ALPHA: &str = "OfxInteractPropHasAlpha";
|
||||
/// kOfxInteractPropDrawContext(ofxDrawSuite.h:34):Draw suite 上下文句柄
|
||||
/// (Pointer;draw inArgs 携带,插件取来传给 Draw suite 函数)。
|
||||
pub(crate) const PROP_INTERACT_DRAW_CONTEXT: &str = "OfxInteractPropDrawContext";
|
||||
/// kOfxPropKeySym(ofxKeySyms.h:30):键盘事件的关键码(Int×1)。
|
||||
pub(crate) const PROP_KEY_SYM: &str = "kOfxPropKeySym";
|
||||
/// kOfxPropKeyString(ofxKeySyms.h:49):键盘事件的 UTF-8 字符(String×1)。
|
||||
pub(crate) const PROP_KEY_STRING: &str = "kOfxPropKeyString";
|
||||
/// kOfxImageEffectPluginPropOverlayInteractV2(ofxImageEffect.h:825):
|
||||
/// 插件声明的 overlay interact 入口(Pointer→OfxPluginEntryPoint;V2
|
||||
/// 要求 Draw suite 绘制)。
|
||||
pub(crate) const PROP_OVERLAY_INTERACT_V2: &str = "OfxImageEffectPluginPropOverlayInteractV2";
|
||||
/// kOfxImageEffectPluginPropOverlayInteractV1(ofxImageEffect.h:812)。
|
||||
pub(crate) const PROP_OVERLAY_INTERACT_V1: &str = "OfxImageEffectPluginPropOverlayInteractV1";
|
||||
/// kOfxImageEffectPropSupportsOverlays(ofxImageEffect.h:801):宿主是否
|
||||
/// 允许插件在输出图像上绘制 overlay(能力宣告)。
|
||||
pub(crate) const PROP_SUPPORTS_OVERLAYS: &str = "OfxImageEffectPropSupportsOverlays";
|
||||
|
||||
// ---- OFX 关键码(ofxKeySyms.h;X11 keysym 值,测试/宿主常用子集)----
|
||||
//
|
||||
// 公共:app 侧(WG3b)经 [`crate::suites::interact::Interact::key_down`]/
|
||||
// `key_up` 传关键码。
|
||||
|
||||
/// kOfxKey_Unknown(ofxKeySyms.h:121)。
|
||||
pub const KEY_UNKNOWN: i32 = 0x0;
|
||||
/// kOfxKey_BackSpace(ofxKeySyms.h:128)。
|
||||
pub const KEY_BACKSPACE: i32 = 0xFF08;
|
||||
/// kOfxKey_Tab(ofxKeySyms.h:129)。
|
||||
pub const KEY_TAB: i32 = 0xFF09;
|
||||
/// kOfxKey_Return(ofxKeySyms.h:132)。
|
||||
pub const KEY_RETURN: i32 = 0xFF0D;
|
||||
/// kOfxKey_Escape(ofxKeySyms.h:136)。
|
||||
pub const KEY_ESCAPE: i32 = 0xFF1B;
|
||||
/// kOfxKey_Delete(ofxKeySyms.h:137)。
|
||||
pub const KEY_DELETE: i32 = 0xFFFF;
|
||||
/// kOfxKey_Home(ofxKeySyms.h:172)。
|
||||
pub const KEY_HOME: i32 = 0xFF50;
|
||||
/// kOfxKey_Left(ofxKeySyms.h:173)。
|
||||
pub const KEY_LEFT: i32 = 0xFF51;
|
||||
/// kOfxKey_Up(ofxKeySyms.h:174)。
|
||||
pub const KEY_UP: i32 = 0xFF52;
|
||||
/// kOfxKey_Right(ofxKeySyms.h:175)。
|
||||
pub const KEY_RIGHT: i32 = 0xFF53;
|
||||
/// kOfxKey_Down(ofxKeySyms.h:176)。
|
||||
pub const KEY_DOWN: i32 = 0xFF54;
|
||||
/// kOfxKey_Page_Up(ofxKeySyms.h:178)。
|
||||
pub const KEY_PAGE_UP: i32 = 0xFF55;
|
||||
/// kOfxKey_Page_Down(ofxKeySyms.h:180)。
|
||||
pub const KEY_PAGE_DOWN: i32 = 0xFF56;
|
||||
/// kOfxKey_End(ofxKeySyms.h:181)。
|
||||
pub const KEY_END: i32 = 0xFF57;
|
||||
/// kOfxKey_F1(ofxKeySyms.h:252)。
|
||||
pub const KEY_F1: i32 = 0xFFBE;
|
||||
/// kOfxKey_Shift_L(ofxKeySyms.h:315)。
|
||||
pub const KEY_SHIFT_L: i32 = 0xFFE1;
|
||||
/// kOfxKey_Control_L(ofxKeySyms.h:317)。
|
||||
pub const KEY_CONTROL_L: i32 = 0xFFE3;
|
||||
/// kOfxKey_Alt_L(ofxKeySyms.h:323)。
|
||||
pub const KEY_ALT_L: i32 = 0xFFE9;
|
||||
/// kOfxKey_space(ofxKeySyms.h:331)。
|
||||
pub const KEY_SPACE: i32 = 0x020;
|
||||
/// kOfxKey_a(ofxKeySyms.h:398)。
|
||||
pub const KEY_A: i32 = 0x061;
|
||||
/// kOfxKey_z(ofxKeySyms.h:423)。
|
||||
pub const KEY_Z: i32 = 0x07a;
|
||||
|
||||
/// kOfxPropChangeReason(ofxCore.h:763):instanceChanged 的 inArgs 里
|
||||
/// 说明变更来源(UserEdited / PluginEdited / Time)。
|
||||
pub(crate) const PROP_CHANGE_REASON: &str = "OfxPropChangeReason";
|
||||
/// kOfxChangeUserEdited(ofxCore.h:792)。
|
||||
pub(crate) const CHANGE_USER_EDITED: &str = "OfxChangeUserEdited";
|
||||
/// kOfxChangePluginEdited(ofxCore.h:795)。
|
||||
pub(crate) const CHANGE_PLUGIN_EDITED: &str = "OfxChangePluginEdited";
|
||||
/// kOfxChangeTime(ofxCore.h:798)。
|
||||
pub(crate) const CHANGE_TIME: &str = "OfxChangeTime";
|
||||
|
||||
/// kOfxPropTime(ofxCore.h:613)。
|
||||
pub(crate) const PROP_TIME: &str = "OfxPropTime";
|
||||
/// kOfxPropEffectInstance(ofxCore.h:776):interact/渲染 inArgs 里指向
|
||||
/// 效果实例句柄的指针属性。
|
||||
pub(crate) const PROP_EFFECT_INSTANCE: &str = "OfxPropEffectInstance";
|
||||
/// kOfxImageEffectPropRenderScale。
|
||||
pub(crate) const PROP_RENDER_SCALE: &str = "OfxImageEffectPropRenderScale";
|
||||
/// kOfxImageEffectPropRenderWindow。
|
||||
@@ -341,6 +487,30 @@ pub struct Plugin {
|
||||
unsafe impl Send for Plugin {}
|
||||
unsafe impl Sync for Plugin {}
|
||||
|
||||
/// 插件入口函数类型(`OfxPluginEntryPoint`,ofxCore.h:84)。
|
||||
pub(crate) type EntryPoint = unsafe extern "C" fn(
|
||||
action: *const c_char,
|
||||
handle: *const c_void,
|
||||
in_args: *mut c_void,
|
||||
out_args: *mut c_void,
|
||||
) -> i32;
|
||||
|
||||
/// 插件声明的 overlay interact 入口(ofxImageEffect.h:825/812):
|
||||
/// V2 优先、V1 次之;两者都未声明返回 None。属性值是
|
||||
/// `Pointer→OfxPluginEntryPoint`(宿主预定义,插件 describe 期写入)。
|
||||
pub(crate) fn overlay_interact_entry(props: &PropertySet) -> Option<EntryPoint> {
|
||||
for name in [PROP_OVERLAY_INTERACT_V2, PROP_OVERLAY_INTERACT_V1] {
|
||||
if let Some(Value::Pointer(p)) = props.get(name, 0) {
|
||||
if !p.is_null() {
|
||||
// 属性值是函数指针(void* 存放);转换与 dlsym_fn 同款
|
||||
// (调用方保证类型正确)。
|
||||
return Some(unsafe { std::mem::transmute_copy(&p) });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
/// 调插件的 action。`handle` 视 action 而定(describe 时为
|
||||
/// descriptor,render 时为 instance)。`in_args`/`out_args` 按
|
||||
@@ -364,6 +534,27 @@ impl Plugin {
|
||||
let action = cs(action);
|
||||
unsafe { (self.entry)(action.as_ptr(), handle as *const c_void, in_ptr, out_ptr) }
|
||||
}
|
||||
|
||||
/// 按指定入口函数调用(interact 的 overlay 入口与 main entry 可
|
||||
/// 不同——插件经 kOfxImageEffectPluginPropOverlayInteractV2 声明)。
|
||||
/// 语义同 [`Plugin::call_action`]。
|
||||
///
|
||||
/// # Safety
|
||||
/// `entry` 必须是插件导出的合法 OfxPluginEntryPoint;`handle`/参数
|
||||
/// 集与 action 匹配。
|
||||
pub(crate) unsafe fn call_entry(
|
||||
&self,
|
||||
entry: EntryPoint,
|
||||
action: &str,
|
||||
handle: *mut c_void,
|
||||
in_args: &PropertySet,
|
||||
out_args: &PropertySet,
|
||||
) -> i32 {
|
||||
let in_ptr = in_args as *const PropertySet as *mut c_void;
|
||||
let out_ptr = out_args as *const PropertySet as *mut c_void;
|
||||
let action = cs(action);
|
||||
unsafe { (entry)(action.as_ptr(), handle as *const c_void, in_ptr, out_ptr) }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- PluginCache ---------------------------------------------------------
|
||||
@@ -730,6 +921,11 @@ fn init_descriptor_props(props: &PropertySet, bundle: &Path) {
|
||||
"OfxImageEffectPluginPropOverlayInteractV1",
|
||||
Value::Pointer(std::ptr::null_mut()),
|
||||
);
|
||||
// overlay interact V2(ofxImageEffect.h:825):插件 describe 期声明
|
||||
// 自定义交互入口(Pointer→OfxPluginEntryPoint;V2 要求 Draw suite
|
||||
// 绘制);宿主预定义空指针默认(propSet 不创建属性,宿主预定义
|
||||
// 属性宇宙,与 V1 同款)。
|
||||
props.set_one(PROP_OVERLAY_INTERACT_V2, Value::Pointer(std::ptr::null_mut()));
|
||||
props.set_one("OfxImageEffectPropSupportsMultiResolution", Value::Int(1));
|
||||
props.set_one(PROP_SUPPORTS_TILES, Value::Int(1));
|
||||
props.set_one("OfxImageEffectPropTemporalClipAccess", Value::Int(0));
|
||||
@@ -886,6 +1082,7 @@ impl Host {
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
interact: std::sync::Mutex::new(None),
|
||||
};
|
||||
init_instance_props(&instance.props, &instance);
|
||||
|
||||
@@ -989,6 +1186,9 @@ fn init_host_props(props: &PropertySet) {
|
||||
// GL 能力宣告(M11 §4;ofxGPURender.h "OpenGL House Keeping":
|
||||
// 宿主在描述符置 "true")。
|
||||
props.set_one(PROP_GL_RENDER_SUPPORTED, Value::String(cs("true")));
|
||||
// overlay 能力宣告(ofxImageEffect.h:801):宿主允许插件在输出
|
||||
// 图像上绘制 overlay(interact draw 的前置)。
|
||||
props.set_one(PROP_SUPPORTS_OVERLAYS, Value::Int(1));
|
||||
// ofxColour 能力宣告(M11 §4):OCIO 模式 + native 配置列表。
|
||||
props.set_one(PROP_COLOUR_STYLE, Value::String(cs(COLOUR_STYLE_OCIO)));
|
||||
props.define(
|
||||
|
||||
@@ -122,6 +122,9 @@ pub struct Instance {
|
||||
/// pluginrenderer.cpp:1436-1444 的 instance_lock——渲染路径非
|
||||
/// 线程安全,并发 render 必须互斥)。
|
||||
pub render_lock: std::sync::Mutex<()>,
|
||||
/// 关联的 interact(`new_interact` 创建;与实例同生命周期,
|
||||
/// notify_destroy 时连带销毁)。
|
||||
pub interact: std::sync::Mutex<Option<std::sync::Arc<crate::suites::interact::Interact>>>,
|
||||
}
|
||||
|
||||
/// 实例销毁路径:先通知 destroyInstance action,再摘除 param 回写登记。
|
||||
@@ -641,12 +644,16 @@ impl Instance {
|
||||
/// 要求上下文 current——本实现的约定是 oakrender 的 PluginJob
|
||||
/// 路径在进入前做好),且 `output_texture` 已附着为渲染器输出
|
||||
/// 目标(等价 C++ `PluginRenderer::attach_output_texture`)。
|
||||
/// `output_gl_texture` 为宿主为输出帧建的真实 GL 纹理名(GL 模式
|
||||
/// 下 clipLoadTexture(Output) 的 OpenGLTextureIndex 以它为准);
|
||||
/// None = CPU 回退语义。
|
||||
///
|
||||
/// action 序列:kOfxActionOpenGLContextAttached → render(in args
|
||||
/// 带 kOfxImageEffectPropOpenGLEnabled=1)→
|
||||
/// kOfxActionOpenGLContextDetached(ofxGPURender.h:345-371;
|
||||
/// attach/detach 必须配对)。渲染结果留在 GL 输出纹理上(插件
|
||||
/// 直接画进附着目标),宿主不做 CPU 回读;GL 模式下
|
||||
/// 直接画进附着目标),宿主经 glReadPixels 回读(render 驱动
|
||||
/// 负责,见 [`crate::gl_bridge`]);GL 模式下
|
||||
/// clipGetImage(Output) 不可用(插件按规范走 OpenGL suite——
|
||||
/// ofxGPURender.h "the effect SHOULD access all its images through
|
||||
/// the OpenGL suite")。render 返回前对未释放的输入 GL 纹理做
|
||||
@@ -658,6 +665,7 @@ impl Instance {
|
||||
window: OfxRectD,
|
||||
renderer: crate::render::Renderer,
|
||||
output_texture: crate::render::Texture,
|
||||
output_gl_texture: Option<i32>,
|
||||
) -> crate::error::Result<()> {
|
||||
use crate::host::ACTION_RENDER;
|
||||
|
||||
@@ -682,6 +690,7 @@ impl Instance {
|
||||
renderer,
|
||||
output_texture,
|
||||
gl_pixel_depth,
|
||||
output_gl_texture,
|
||||
}));
|
||||
// GL 模式无 CPU 输出图像(current_output 保持 None)。
|
||||
|
||||
@@ -963,6 +972,143 @@ impl Instance {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// kOfxActionInstanceChanged 的宿主侧分发(ofxCore.h:405-435 的
|
||||
/// inArgs 契约):
|
||||
///
|
||||
/// - kOfxPropType = kOfxTypeParameter(参数值变更)
|
||||
/// - kOfxPropName = 变更的参数名
|
||||
/// - kOfxPropChangeReason = kOfxChange*(UserEdited / PluginEdited /
|
||||
/// Time)
|
||||
/// - kOfxPropTime = 变更发生时的效果时间(Image Effect 插件专属)
|
||||
/// - kOfxImageEffectPropRenderScale = 当前渲染比例
|
||||
///
|
||||
/// 宿主按下 push button 后以 UserEdited 调用它(push button 无值,
|
||||
/// 插件的反应全在 instanceChanged 里;真实插件如 CImg 依赖此动作
|
||||
/// 刷新内部状态)。返回非 OK/ReplyDefault 状态码时为 Err。
|
||||
pub fn instance_changed(
|
||||
&self,
|
||||
param_name: &str,
|
||||
reason: crate::param::ChangeReason,
|
||||
time: f64,
|
||||
scale: RenderScale,
|
||||
) -> crate::error::Result<()> {
|
||||
use crate::host::{
|
||||
ACTION_INSTANCE_CHANGED, CHANGE_PLUGIN_EDITED, CHANGE_TIME, CHANGE_USER_EDITED,
|
||||
PROP_CHANGE_REASON, PROP_RENDER_SCALE, PROP_TIME,
|
||||
};
|
||||
use crate::property::Value;
|
||||
|
||||
let in_args = PropertySet::new();
|
||||
in_args.set_one(
|
||||
crate::param::PROP_TYPE,
|
||||
Value::String(CString::new(crate::param::TYPE_PARAMETER).unwrap()),
|
||||
);
|
||||
in_args.set_one(
|
||||
crate::param::PROP_NAME,
|
||||
Value::String(CString::new(param_name).unwrap()),
|
||||
);
|
||||
let reason_str = match reason {
|
||||
crate::param::ChangeReason::UserEdited => CHANGE_USER_EDITED,
|
||||
crate::param::ChangeReason::PluginEdited => CHANGE_PLUGIN_EDITED,
|
||||
crate::param::ChangeReason::TimeChanged => CHANGE_TIME,
|
||||
};
|
||||
in_args.set_one(
|
||||
PROP_CHANGE_REASON,
|
||||
Value::String(CString::new(reason_str).unwrap()),
|
||||
);
|
||||
in_args.set_one(PROP_TIME, Value::Double(time));
|
||||
in_args.define(
|
||||
PROP_RENDER_SCALE,
|
||||
vec![Value::Double(scale.x), Value::Double(scale.y)],
|
||||
);
|
||||
|
||||
let inst_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let out = PropertySet::new();
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
.call_action(ACTION_INSTANCE_CHANGED, inst_handle, &in_args, &out)
|
||||
};
|
||||
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
|
||||
return Err(crate::error::Error::Failed(format!(
|
||||
"instanceChanged 失败:{stat}"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 创建关联的 interact(主进程 UI 事件宿主;任务契约的
|
||||
/// `new_interact(instance)`)。与 worker 进程里的渲染实例并存——
|
||||
/// OFX 允许同一插件多实例,interact 是独立对象(独立 handle),
|
||||
/// 经 [`crate::suites::interact::Interact::handle`] 与效果实例区分。
|
||||
///
|
||||
/// 流程:插件声明的 overlay interact 入口(V2 优先、V1 次之;
|
||||
/// ofxImageEffect.h:825/812)作为 interact 的入口——未声明则用插件
|
||||
/// main entry(任务契约)→ 建 [`Interact`](属性表 + tagged handle)
|
||||
/// → 向入口发 `kOfxActionNewInteract`。
|
||||
///
|
||||
/// 返回值:
|
||||
/// - `Some(interact)`:创建成功(NewInteract 返回 OK;或插件未实现
|
||||
/// NewInteract 但声明了 overlay interact——真实 overlay 插件不认
|
||||
/// NewInteract,走 describe/create 序列)。
|
||||
/// - `None`:插件无 interact(NewInteract 返回 kOfxStatReplyDefault
|
||||
/// 且未声明 overlay 入口)或返回错误状态。
|
||||
///
|
||||
/// 创建后调用方继续 [`Interact::describe`] → [`Interact::create_instance`]
|
||||
/// 完成实例化;销毁随实例自动连带([`Instance::notify_destroy`])。
|
||||
pub fn new_interact(&self) -> Option<std::sync::Arc<crate::suites::interact::Interact>> {
|
||||
use crate::host::overlay_interact_entry;
|
||||
use crate::suites::interact::Interact;
|
||||
|
||||
let overlay = overlay_interact_entry(&self.plugin.descriptor.props);
|
||||
let entry = overlay.unwrap_or(self.plugin.entry);
|
||||
let has_overlay = overlay.is_some();
|
||||
|
||||
let effect_handle = crate::suites::tag::make(
|
||||
&self.props as *const PropertySet,
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let interact = Interact::new(self.plugin.clone(), entry, effect_handle);
|
||||
|
||||
let empty = PropertySet::new();
|
||||
let st = interact.call(crate::host::ACTION_NEW_INTERACT, &empty, &empty);
|
||||
let accepted = match st {
|
||||
crate::suites::status::OK => true,
|
||||
// 插件未实现 NewInteract 但声明了 overlay interact → 仍创建
|
||||
//(走 describe/create 的官方序列)。
|
||||
crate::suites::status::REPLY_DEFAULT if has_overlay => true,
|
||||
_ => false,
|
||||
};
|
||||
if !accepted {
|
||||
return None;
|
||||
}
|
||||
*self
|
||||
.interact
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(interact.clone());
|
||||
Some(interact)
|
||||
}
|
||||
|
||||
/// 已创建的 interact(None = 未创建)。
|
||||
pub fn interact(&self) -> Option<std::sync::Arc<crate::suites::interact::Interact>> {
|
||||
self.interact
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// describe_interact(任务契约):对已创建的 interact 发
|
||||
/// `kOfxActionDescribe`(kOfxActionDescribeInteract)。未创建 →
|
||||
/// kOfxStatErrBadHandle。
|
||||
pub fn describe_interact(&self) -> i32 {
|
||||
match self.interact() {
|
||||
Some(i) => i.describe(),
|
||||
None => crate::suites::status::ERR_BAD_HANDLE,
|
||||
}
|
||||
}
|
||||
|
||||
/// 销毁(destroyInstance action)。析构由 `Arc<RefBox<Instance>>`
|
||||
/// 归零驱动;此处只做 action 通知,幂等([`Instance::drop`] 的
|
||||
/// `destroyed` 门保证只发一次)。
|
||||
@@ -973,6 +1119,17 @@ impl Instance {
|
||||
crate::suites::tag::INSTANCE,
|
||||
);
|
||||
let empty = PropertySet::new();
|
||||
// interact 与实例同生命周期:实例销毁前先销毁 interact
|
||||
//(ofxInteract.h kOfxActionDestroyInstanceInteract 的 \pre 要求
|
||||
// 实例成员尚未销毁)。
|
||||
if let Some(i) = self
|
||||
.interact
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.take()
|
||||
{
|
||||
i.destroy();
|
||||
}
|
||||
// 通知失败只记日志(销毁路径不可回滚)。
|
||||
let stat = unsafe {
|
||||
self.plugin
|
||||
|
||||
@@ -33,9 +33,11 @@
|
||||
//! 删除:oaknode/oakrender/oakundo 均以 path 依赖直接链接。桥以
|
||||
//! 直接 Rust 类型重建([`node`] 的身份注册表与
|
||||
//! `set_input_*_undoable`;[`render`] 的 `Texture`/`Frame`/`Renderer`
|
||||
//! 值类型),仅 GPU 相关且 wgpu 模型无等价物的调用面保留标注桩
|
||||
//! ([`render::texture_id`]——GL 命名空间不存在,见 `// STUB`
|
||||
//! 标记)。
|
||||
//! 值类型)。GL 纹理名(旧 `oakrender_texture_id` 的 GL 命名空间)
|
||||
//! 由 [`gl_bridge`](方案 B:离屏 CGL 上下文 + 回读)提供;
|
||||
//! [`render::texture_id`] 对 oakrender 纹理保持恒 0(wgpu/Metal 无
|
||||
//! GL 命名空间),use_opengl 决策与 GL suite 的 OpenGLTextureIndex
|
||||
//! 改从 [`gl_bridge`] 取真实名。
|
||||
//!
|
||||
//! ## 句柄纪律(全 crate 最高优先级约定)
|
||||
//!
|
||||
|
||||
@@ -110,15 +110,14 @@ pub fn registered_instance_count() -> usize {
|
||||
/// inspector's button widget).
|
||||
///
|
||||
/// OFX push buttons carry no value: the host's press signal is a single
|
||||
/// set on the parameter (the plugin reacts in its own instanceChanged
|
||||
/// action). Locates the instance and parameter, type-checks, then
|
||||
/// `set_ofx`. Returns false when the instance/parameter is unknown or
|
||||
/// the parameter is not a push button.
|
||||
///
|
||||
/// TODO(instanceChanged): per the OFX contract the press should be
|
||||
/// routed to the plugin as kOfxActionInstanceChanged (UserEdited);
|
||||
/// oakplugin has no dispatcher for that action yet — for now the value
|
||||
/// is only marked, and the plugin-side reaction is future work.
|
||||
/// set on the parameter, then the press is routed to the plugin as
|
||||
/// `kOfxActionInstanceChanged` (UserEdited) so the plugin reacts in its
|
||||
/// own instanceChanged action (the OFX contract, ofxCore.h:405-435;
|
||||
/// real plugins such as CImg depend on that action). Locates the
|
||||
/// instance and parameter, type-checks, marks the value, then dispatches
|
||||
/// the action. Returns false when the instance/parameter is unknown or
|
||||
/// the parameter is not a push button; an instanceChanged failure is
|
||||
/// logged and still reported as a successful press (the value was set).
|
||||
pub fn push_button_clicked(instance: u64, param_name: &str) -> bool {
|
||||
let Some(inst) = instance_from_id(instance) else {
|
||||
return false;
|
||||
@@ -130,6 +129,19 @@ pub fn push_button_clicked(instance: u64, param_name: &str) -> bool {
|
||||
return false;
|
||||
}
|
||||
p.set_ofx(ParamValue::PushButton);
|
||||
// The current render context supplies the time / render scale when the
|
||||
// press happens during a render; outside one, time 0 and scale 1:1.
|
||||
let (time, scale) = crate::suites::render_ctx()
|
||||
.map(|ctx| (ctx.time, ctx.scale))
|
||||
.unwrap_or((0.0, crate::instance::RenderScale { x: 1.0, y: 1.0 }));
|
||||
if let Err(e) = inst.value.instance_changed(
|
||||
param_name,
|
||||
crate::param::ChangeReason::UserEdited,
|
||||
time,
|
||||
scale,
|
||||
) {
|
||||
eprintln!("push_button_clicked: instanceChanged failed for \"{param_name}\": {e}");
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -853,6 +865,10 @@ pub fn install_render_executor() {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Records what the mock plugin entry saw, so the push-button test can
|
||||
/// assert the kOfxActionInstanceChanged routing and its inArgs.
|
||||
static PUSH_ENTRY_CALLS: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
#[test]
|
||||
fn input_type_table_covers_all_ofx_kinds() {
|
||||
assert_eq!(
|
||||
@@ -1000,11 +1016,31 @@ mod tests {
|
||||
use crate::param::{ParamDef, ParamInstance, ParamSetInstance};
|
||||
|
||||
unsafe extern "C" fn dummy_entry(
|
||||
_: *const c_char,
|
||||
action: *const c_char,
|
||||
_: *const c_void,
|
||||
_: *mut c_void,
|
||||
in_args: *mut c_void,
|
||||
_: *mut c_void,
|
||||
) -> i32 {
|
||||
if !action.is_null() {
|
||||
let action = unsafe {
|
||||
std::ffi::CStr::from_ptr(action).to_string_lossy().into_owned()
|
||||
};
|
||||
if action == crate::host::ACTION_INSTANCE_CHANGED {
|
||||
let mut calls = PUSH_ENTRY_CALLS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
calls.push(action);
|
||||
// The instanceChanged inArgs contract (ofxCore.h:405-435):
|
||||
// kOfxPropChangeReason = kOfxChangeUserEdited.
|
||||
let props = unsafe { &*(in_args as *const crate::property::PropertySet) };
|
||||
let reason = props.get(crate::host::PROP_CHANGE_REASON, 0);
|
||||
let reason = match reason {
|
||||
Some(crate::property::Value::String(s)) => {
|
||||
s.to_string_lossy().into_owned()
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
calls.push(reason);
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
let plugin = Arc::new(Plugin {
|
||||
@@ -1039,6 +1075,7 @@ mod tests {
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
interact: std::sync::Mutex::new(None),
|
||||
};
|
||||
register_instance(Arc::new(RefBox {
|
||||
refs: AtomicU32::new(1),
|
||||
@@ -1047,15 +1084,21 @@ mod tests {
|
||||
}
|
||||
|
||||
/// push_button_clicked:实例/参数查无与类型不匹配 → false;命中 →
|
||||
/// true 并触发一次 set。
|
||||
/// true、置 PushButton 值并把按下路由为 kOfxActionInstanceChanged
|
||||
/// (UserEdited,inArgs 带 change reason / name / type / time /
|
||||
/// render scale)。
|
||||
#[test]
|
||||
fn push_button_clicked_requires_a_push_button_param() {
|
||||
fn push_button_clicked_routes_instance_changed() {
|
||||
*PUSH_ENTRY_CALLS.lock().unwrap_or_else(|e| e.into_inner()) = Vec::new();
|
||||
let id = instance_with_push_button();
|
||||
assert!(push_button_clicked(id, "button"));
|
||||
// 类型不匹配 / 查无参数 / 查无实例。
|
||||
// 类型不匹配 / 查无参数 / 查无实例:不触发 entry。
|
||||
assert!(!push_button_clicked(id, "gain"));
|
||||
assert!(!push_button_clicked(id, "nope"));
|
||||
assert!(!push_button_clicked(u64::MAX, "button"));
|
||||
// 只 "button" 那次按下进了插件 entry,且 reason 是 UserEdited。
|
||||
let calls = PUSH_ENTRY_CALLS.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
assert_eq!(calls, vec!["OfxActionInstanceChanged", "OfxChangeUserEdited"]);
|
||||
unregister_instance(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ use std::sync::{Arc, Mutex, OnceLock};
|
||||
pub trait UiProgressReporter: Send {
|
||||
/// 上报进度(0.0..=1.0);false = 取消。
|
||||
fn update(&mut self, progress: f64) -> bool;
|
||||
/// progressEnd 通知(默认 no-op;app 可据此关闭进度 UI / 把完成
|
||||
/// 事件转发过 IPC——worker 侧的进度回传依赖它)。
|
||||
fn end(&mut self) {}
|
||||
}
|
||||
|
||||
/// 报告器工厂:progressStart 携 (label, message) 调用,现造一个
|
||||
@@ -160,4 +163,12 @@ impl ProgressReporter {
|
||||
pub(crate) fn is_cancelled(&self) -> bool {
|
||||
self.cancelled.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// progressEnd 钩子:转发给 UI 报告器(无报告器时 no-op)。
|
||||
pub(crate) fn end_ui(&self) {
|
||||
let mut slot = self.ui.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(ui) = slot.as_mut() {
|
||||
ui.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,11 @@
|
||||
//! - 像素格式常量直接别名 [`oakcore_rs::PixelFormat`]。
|
||||
//!
|
||||
//! 保留桩(GPU 相关、wgpu 模型无直接 Rust 等价物):
|
||||
//! [`texture_id`]——wgpu 后端没有 OpenGL 纹理名(旧 C ABI 的
|
||||
//! `oakrender_texture_id` 语义是 GL 命名空间),恒 0;GL suite 的
|
||||
//! `OpenGLTextureIndex` 属性与 render 驱动的 use_opengl 决策据此
|
||||
//! 回退 CPU 路径。
|
||||
//! [`texture_id`]——oakrender 纹理(wgpu/CPU)没有 OpenGL 纹理名,
|
||||
//! 恒 0。**GL 模式的真实 GL 纹理名来自 [`crate::gl_bridge`]**(方案 B
|
||||
//! 已落地):render 驱动为输出帧建 GL 纹理 + FBO,GL suite 的
|
||||
//! `OpenGLTextureIndex` 属性据此返回真实名;use_opengl 决策改用
|
||||
//! [`crate::gl_bridge::gl_available`] 作"目标纹理有有效 GL 名"的门。
|
||||
|
||||
/// `oakrender_video_params` POD — single-lib unification: aliases the
|
||||
/// oakrender crate's struct (identical layout;
|
||||
@@ -124,13 +125,15 @@ pub fn texture_create(
|
||||
Ok(Texture::wrap_frame(frame))
|
||||
}
|
||||
|
||||
/// STUB: oakrender C ABI deleted (single-lib),且 wgpu 后端没有
|
||||
/// OpenGL 纹理名——旧 `oakrender_texture_id` 的 GL 命名空间语义无
|
||||
/// Rust 等价物。恒 0(GL suite 的 `OpenGLTextureIndex` 属性与
|
||||
/// render 驱动的 use_opengl 决策据此回退 CPU 路径;GPU 上传若落地
|
||||
/// 走 `oakrender::backend::GpuContextLike::upload` 的 wgpu token,
|
||||
/// 不暴露 GL id)。真实化的评估与方案见 [`crate::gl_bridge`]
|
||||
/// (阶段 6a spike:方案 A 不可行,方案 B 暂缓)。
|
||||
/// oakrender 纹理在 GL 命名空间中的纹理名。oakrender 纹理(wgpu
|
||||
/// Metal / CPU 帧)没有 OpenGL 纹理名,恒 0。
|
||||
///
|
||||
/// **GL 模式的真实纹理名不由本函数提供**:宿主自建的离屏 GL 纹理
|
||||
/// (输出帧 + FBO 挂载、输入 clip 上传)由 [`crate::gl_bridge`] 产生,
|
||||
/// 经 GL suite 的 `OpenGLTextureIndex` 属性直接写出;use_opengl 决策
|
||||
/// 用 [`crate::gl_bridge::gl_available`] 当"目标纹理有有效 GL 名"的
|
||||
/// 门(桥能为目标帧建出真实 GL 纹理 ⟺ 可用)。评估历史见
|
||||
/// [`crate::gl_bridge`] 模块文档。
|
||||
pub fn texture_id(_texture: &Texture) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
@@ -47,8 +47,10 @@
|
||||
//! 10. 参数覆盖(pluginrenderer.cpp:132-290 apply_param_overrides);
|
||||
//! 11. render action:CPU 路径经 [`crate::instance::Instance::render`]
|
||||
//! (输出装配:图像 → 目标纹理帧,行跨度感知);GL 路径经
|
||||
//! [`crate::instance::Instance::render_gl`](插件直接画进已附着
|
||||
//! 的输出纹理,无 CPU 回读)。
|
||||
//! [`crate::instance::Instance::render_gl`] + [`crate::gl_bridge`]
|
||||
//! (方案 B:宿主建离屏上下文,插件画进 FBO 附着的输出 GL 纹理,
|
||||
//! render 返回后 glReadPixels 回读装配——输出格式与 CPU 路径一致;
|
||||
//! GL 失败回退 CPU)。
|
||||
//!
|
||||
//! ## begin/end 序列括号
|
||||
//!
|
||||
@@ -112,6 +114,10 @@ pub fn begin_sequence(
|
||||
gl: Option<Renderer>,
|
||||
) -> crate::error::Result<()> {
|
||||
if gl.is_some() {
|
||||
// GL 模式:ofxGPURender.h "OpenGL Current Context" 要求
|
||||
// BeginSequenceRender 期间上下文 current(插件可能在此分配 GL
|
||||
// 资源)。acquire 覆盖整个 action;返回即清 current。
|
||||
let _guard = crate::gl_bridge::acquire()?;
|
||||
inst.begin_sequence_render_gl(range)
|
||||
} else {
|
||||
inst.begin_sequence_render(range)
|
||||
@@ -125,6 +131,7 @@ pub fn end_sequence(
|
||||
gl: Option<Renderer>,
|
||||
) -> crate::error::Result<()> {
|
||||
if gl.is_some() {
|
||||
let _guard = crate::gl_bridge::acquire()?;
|
||||
inst.end_sequence_render_gl(range)
|
||||
} else {
|
||||
inst.end_sequence_render(range)
|
||||
@@ -153,18 +160,18 @@ pub fn render_frame(
|
||||
}
|
||||
|
||||
// 2. use_opengl(pluginrenderer.cpp:1446-1457):插件声明 GL 支持
|
||||
// 且渲染器是 OpenGL 且目标纹理有 GL id 且像素深度协商可行
|
||||
// (管线 F32 满足插件 kOfxOpenGLPropPixelDepth 声明)。
|
||||
// `texture_id` 为桩恒 0(wgpu 无 GL 命名空间)→ 本决策恒回退
|
||||
// CPU 路径,GL 分支保留给 GL 后端落地。
|
||||
// 且渲染器是 OpenGL 且像素深度协商可行(管线 F32 满足插件
|
||||
// kOfxOpenGLPropPixelDepth 声明)且目标纹理有有效 GL 名(= 桥能为
|
||||
// 目标帧建出真实 GL 纹理 ⟺ 离屏上下文可用,[`crate::gl_bridge`])。
|
||||
// 任一不满足 → 回退 CPU 路径(GL 分支在下方真正渲染)。
|
||||
let use_opengl = match job.renderer.as_ref() {
|
||||
Some(r) if render::renderer_is_open_gl(r) => {
|
||||
let plugin_gl = plugin_supports_opengl(inst);
|
||||
let depth_ok =
|
||||
crate::suites::gl_render::pick_gl_pixel_depth(&inst.plugin.descriptor.props)
|
||||
.is_some();
|
||||
let dst_id = render::texture_id(&job.dst);
|
||||
plugin_gl && depth_ok && dst_id != 0
|
||||
let gl_name_ok = crate::gl_bridge::gl_available();
|
||||
plugin_gl && depth_ok && gl_name_ok
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
@@ -276,20 +283,103 @@ pub fn render_frame(
|
||||
// 输出装配(pluginrenderer.cpp:1762-1834 的 CPU 路径)。
|
||||
write_output_frame(&mut dst, &output)?;
|
||||
} else {
|
||||
// GL 路径:插件直接画进已附着的输出纹理(
|
||||
// pluginrenderer.cpp:1784-1834 的 GL 分支);无 CPU 回读。
|
||||
inst.render_gl(
|
||||
job.time,
|
||||
// GL 路径(方案 B,见 [`crate::gl_bridge`]):宿主自建离屏
|
||||
// 上下文,为输出帧建 GL 纹理 + FBO(插件直接画进附着的输出
|
||||
// 纹理,等价 C++ attach_output_texture),render 返回后
|
||||
// glReadPixels 回读装帧(pluginrenderer.cpp:1784-1834 的 GL
|
||||
// 分支 + 本实现的回读装配)。GL 失败回退 CPU(对齐现有失败
|
||||
// 语义;最终失败仍上抛)。
|
||||
match render_gl_frame(
|
||||
inst,
|
||||
job,
|
||||
RenderScale { x: 1.0, y: 1.0 },
|
||||
render_window,
|
||||
job.renderer.clone().unwrap(),
|
||||
dst.clone(),
|
||||
)?;
|
||||
w,
|
||||
h,
|
||||
&dst_params,
|
||||
) {
|
||||
Ok(image) => write_output_frame(&mut dst, &image)?,
|
||||
Err(gl_err) => {
|
||||
eprintln!("[PLUGIN] GL 渲染失败,回退 CPU:{gl_err}");
|
||||
let output = std::sync::Arc::new(Image::allocate(
|
||||
crate::image::BitDepth::Float,
|
||||
components,
|
||||
OfxRectD {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
x2: w,
|
||||
y2: h,
|
||||
},
|
||||
));
|
||||
inst.render(
|
||||
job.time,
|
||||
RenderScale { x: 1.0, y: 1.0 },
|
||||
render_window,
|
||||
output.clone(),
|
||||
)?;
|
||||
write_output_frame(&mut dst, &output)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((dst, zip_rois(inst, &rois)))
|
||||
}
|
||||
|
||||
/// GL 渲染一帧(render_frame 的 GL 分支主体;返回回读装配完成的
|
||||
/// F32 RGBA 图像)。
|
||||
///
|
||||
/// 流程:acquire 离屏上下文(本线程 current,全局串行)→ 建输出 GL
|
||||
/// 纹理(尺寸 = 目标帧,格式按 `dst_params`)→ FBO 挂载并绑定 →
|
||||
/// 视口 → [`Instance::render_gl`](插件画进附着纹理;真实纹理名经
|
||||
/// GlCtx 注入,clipLoadTexture(Output) 返回它)→ glReadPixels 回读
|
||||
/// 装配(垂直翻转 + 格式转换)→ 清理 FBO/纹理。任何一步失败返回
|
||||
/// Err,调用方回退 CPU。
|
||||
fn render_gl_frame(
|
||||
inst: &Instance,
|
||||
job: &RenderJob,
|
||||
scale: RenderScale,
|
||||
window: OfxRectD,
|
||||
w: f64,
|
||||
h: f64,
|
||||
dst_params: &render::VideoParams,
|
||||
) -> crate::error::Result<std::sync::Arc<Image>> {
|
||||
use crate::error::Error;
|
||||
|
||||
// 离屏上下文(进程级共享;本线程 current 直到 guard drop)。
|
||||
let _guard = crate::gl_bridge::acquire()?;
|
||||
let (wpx, hpx) = (w as i32, h as i32);
|
||||
// 输出 GL 纹理 + FBO 挂载(GL_RGBA32F/GL_RGBA8 按目标帧格式)。
|
||||
let gl_tex = crate::gl_bridge::create_output_texture(wpx, hpx, dst_params)?;
|
||||
let fbo = crate::gl_bridge::create_fbo(gl_tex, wpx, hpx)?;
|
||||
crate::gl_bridge::bind_fbo(fbo);
|
||||
crate::gl_bridge::set_viewport(wpx, hpx);
|
||||
|
||||
// render action(OpenGLEnabled=1;输出纹理真实名经 GlCtx 注入)。
|
||||
let render_res = inst.render_gl(
|
||||
job.time,
|
||||
scale,
|
||||
window,
|
||||
job.renderer.clone().unwrap(),
|
||||
job.dst.clone(),
|
||||
Some(gl_tex),
|
||||
);
|
||||
|
||||
// 回读前防御性重绑 FBO + 视口(规范下插件不解除输出绑定,但个别
|
||||
// 插件可能改绑/改视口——重绑保证 glReadPixels 读的是输出纹理)。
|
||||
crate::gl_bridge::bind_fbo(fbo);
|
||||
crate::gl_bridge::set_viewport(wpx, hpx);
|
||||
|
||||
// 回读(FBO 仍绑定、输出 GL 纹理仍存活)。
|
||||
let readback = crate::gl_bridge::read_pixels_to_image(wpx, hpx, dst_params);
|
||||
|
||||
// 清理(guard drop 时清 current 并放锁)。
|
||||
crate::gl_bridge::delete_fbo(fbo);
|
||||
crate::gl_bridge::delete_gl_texture(gl_tex);
|
||||
|
||||
render_res?;
|
||||
readback.map_err(|e| Error::Failed(format!("GL 回读失败:{e}")))
|
||||
}
|
||||
|
||||
/// 把输入 clip 名与 RoI 列表配对(与 `clips` 顺序一致)。
|
||||
fn zip_rois(inst: &Instance, rois: &[OfxRectD]) -> Vec<(String, OfxRectD)> {
|
||||
inst.clips
|
||||
|
||||
@@ -0,0 +1,795 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! OfxDrawSuiteV1(OFX 1.5,vendored ofxDrawSuite.h):宿主绘制套件。
|
||||
//!
|
||||
//! 插件在 `kOfxInteractActionDraw` 内经 `kOfxInteractPropDrawContext`
|
||||
//! 取到绘制上下文(本模块的 [`DrawContext`]),再调 suite 函数画图:
|
||||
//! getColour / setColour / setLineWidth / setLineStipple / draw /
|
||||
//! drawText。
|
||||
//!
|
||||
//! ## 上下文模型
|
||||
//!
|
||||
//! - [`DrawContext`] 在每次 draw action 前由宿主创建、经存活表
|
||||
//! [`LIVE_CONTEXTS`] 注册(句柄 = 堆地址;对应 [`crate::suites::tag`]
|
||||
//! 的句柄纪律),draw 返回后摘除。插件只在 draw action 内持有上下文
|
||||
//! ——ofxDrawSuite.h 各函数文档注明 "failure, e.g. if function is
|
||||
//! called outside kOfxInteractActionDraw" → 存活表查不到即
|
||||
//! kOfxStatFailed。
|
||||
//! - 状态(colour / lineWidth / stipple / viewport / pixelScale)保存在
|
||||
//! [`DrawContext`],draw action 之间不共享。
|
||||
//! - GL 命令要求上下文 current:宿主在调 `kOfxInteractActionDraw` 前经
|
||||
//! [`crate::gl_bridge::acquire`] 保持 current(draw action 全期)。
|
||||
//!
|
||||
//! ## 绘制实现(真实 GL)
|
||||
//!
|
||||
//! gl_bridge 的 CGL 上下文是 **3.2 core profile**(无固定管线,
|
||||
//! glBegin 不可用)——本套件的 draw 用最小着色器 + VAO/VBO 真实渲染:
|
||||
//! 正交投影把 canonical 坐标映射到 NDC(canonical 宽 = viewport /
|
||||
//! pixelScale),非不透明色按 "over" 合成(ofxDrawSuite.h setColour
|
||||
//! 文档要求)。非 macOS 无 GL 桥 → kOfxStatFailed。
|
||||
//!
|
||||
//! ## drawText
|
||||
//!
|
||||
//! 本宿主无字体光栅化器(GL 内画字形需要字库/曲线上采样,超出 Phase
|
||||
//! 1 范围),如实返回 kOfxStatErrUnsupported,不假装画了字。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, c_float, c_int, c_void, CStr};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use crate::suites::status;
|
||||
|
||||
/// `OfxStandardColour`(ofxDrawSuite.h:37-46)枚举值。
|
||||
pub mod std_colour {
|
||||
/// kOfxStandardColourOverlayBackground。
|
||||
pub const BACKGROUND: i32 = 0;
|
||||
/// kOfxStandardColourOverlayActive。
|
||||
pub const ACTIVE: i32 = 1;
|
||||
/// kOfxStandardColourOverlaySelected。
|
||||
pub const SELECTED: i32 = 2;
|
||||
/// kOfxStandardColourOverlayDeselected。
|
||||
pub const DESELECTED: i32 = 3;
|
||||
/// kOfxStandardColourOverlayMarqueeFG。
|
||||
pub const MARQUEE_FG: i32 = 4;
|
||||
/// kOfxStandardColourOverlayMarqueeBG。
|
||||
pub const MARQUEE_BG: i32 = 5;
|
||||
/// kOfxStandardColourOverlayText。
|
||||
pub const TEXT: i32 = 6;
|
||||
}
|
||||
|
||||
/// `OfxDrawLineStipplePattern`(ofxDrawSuite.h:49-56)枚举值。
|
||||
pub mod stipple {
|
||||
/// kOfxDrawLineStipplePatternSolid。
|
||||
pub const SOLID: i32 = 0;
|
||||
/// kOfxDrawLineStipplePatternDot。
|
||||
pub const DOT: i32 = 1;
|
||||
/// kOfxDrawLineStipplePatternDash。
|
||||
pub const DASH: i32 = 2;
|
||||
/// kOfxDrawLineStipplePatternAltDash。
|
||||
pub const ALT_DASH: i32 = 3;
|
||||
/// kOfxDrawLineStipplePatternDotDash。
|
||||
pub const DOT_DASH: i32 = 4;
|
||||
}
|
||||
|
||||
/// `OfxDrawPrimitive`(ofxDrawSuite.h:60-68)枚举值。
|
||||
pub mod primitive {
|
||||
/// kOfxDrawPrimitiveLines(n 点画 n/2 条独立线段)。
|
||||
pub const LINES: i32 = 0;
|
||||
/// kOfxDrawPrimitiveLineStrip。
|
||||
pub const LINE_STRIP: i32 = 1;
|
||||
/// kOfxDrawPrimitiveLineLoop。
|
||||
pub const LINE_LOOP: i32 = 2;
|
||||
/// kOfxDrawPrimitiveRectangle(轴对齐实心矩形,2 对角点)。
|
||||
pub const RECTANGLE: i32 = 3;
|
||||
/// kOfxDrawPrimitivePolygon(实心 n 边形)。
|
||||
pub const POLYGON: i32 = 4;
|
||||
/// kOfxDrawPrimitiveEllipse(2 对角点包围盒内的轴对齐椭圆**线框**)。
|
||||
pub const ELLIPSE: i32 = 5;
|
||||
}
|
||||
|
||||
/// `OfxRGBAColourF`(ofxPixels.h):Draw suite 的颜色值类型。
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OfxRGBAColourF {
|
||||
/// 红。
|
||||
pub r: f32,
|
||||
/// 绿。
|
||||
pub g: f32,
|
||||
/// 蓝。
|
||||
pub b: f32,
|
||||
/// alpha。
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
/// `OfxPointD`(ofxCore.h:819):canonical 坐标点。
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OfxPointD {
|
||||
/// x。
|
||||
pub x: f64,
|
||||
/// y。
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
/// Draw suite 上下文:一次 draw action 期间插件的绘制状态。
|
||||
///
|
||||
/// 宿主在 [`crate::suites::interact::Interact::draw`] 内创建并注册,
|
||||
/// draw 返回后摘除。`viewport`/`pixel_scale` 在创建时快照(canonical
|
||||
/// → NDC 正交投影用);颜色/线宽/虚线样式由插件经 suite 函数写入。
|
||||
pub struct DrawContext {
|
||||
/// 当前绘制颜色(RGBA;setColour 写入,draw 时作为着色器 uniform)。
|
||||
pub colour: [f32; 4],
|
||||
/// 当前线宽(setLineWidth;GL 线绘制 glLineWidth)。
|
||||
pub line_width: f32,
|
||||
/// 当前虚线样式(setLineStipple;`stipple::*` 枚举)。
|
||||
pub stipple: i32,
|
||||
/// 视口尺寸(像素;创建时快照)。
|
||||
pub viewport: (f64, f64),
|
||||
/// canonical→屏幕像素比例(创建时快照)。
|
||||
pub pixel_scale: (f64, f64),
|
||||
}
|
||||
|
||||
impl DrawContext {
|
||||
/// 新上下文(默认颜色不透明黑、线宽 1、实线)。
|
||||
pub fn new(viewport: (f64, f64), pixel_scale: (f64, f64)) -> Self {
|
||||
Self {
|
||||
colour: [0.0, 0.0, 0.0, 1.0],
|
||||
line_width: 1.0,
|
||||
stipple: stipple::SOLID,
|
||||
viewport,
|
||||
pixel_scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 存活绘制上下文表:draw action 期间注册(地址 → 盒值)。
|
||||
/// 插件只在 draw 内持有句柄,draw 返回即摘除(同
|
||||
/// [`crate::suites::gl_render`] 的 LIVE_TEXTURES 模式)。
|
||||
static LIVE_CONTEXTS: LazyLock<Mutex<HashMap<usize, Box<DrawContext>>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
fn lock() -> std::sync::MutexGuard<'static, HashMap<usize, Box<DrawContext>>> {
|
||||
LIVE_CONTEXTS.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// 注册上下文并返回句柄(宿主 draw action 调用;句柄写入 inArgs 的
|
||||
/// kOfxInteractPropDrawContext)。
|
||||
pub(crate) fn make_context(viewport: (f64, f64), pixel_scale: (f64, f64)) -> *mut c_void {
|
||||
let ctx = Box::new(DrawContext::new(viewport, pixel_scale));
|
||||
let addr = &*ctx as *const DrawContext as usize;
|
||||
lock().insert(addr, ctx);
|
||||
addr as *mut c_void
|
||||
}
|
||||
|
||||
/// 摘除上下文(draw action 返回后调用;后续 suite 调用 → Failed)。
|
||||
pub(crate) fn drop_context(handle: *mut c_void) {
|
||||
lock().remove(&(handle as usize));
|
||||
}
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
|
||||
catch_unwind(AssertUnwindSafe(f)).map_or_else(
|
||||
|_| status::FAILED,
|
||||
|r| r.map_or_else(|c| c, |()| status::OK),
|
||||
)
|
||||
}
|
||||
|
||||
/// 上下文句柄解析(存活表反查;draw action 外 → Failed,ofxDrawSuite.h
|
||||
/// 的 "outside kOfxInteractActionDraw" 语义)。
|
||||
fn resolve(handle: *mut c_void) -> Result<&'static DrawContext, c_int> {
|
||||
if handle.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
let addr = handle as usize;
|
||||
lock()
|
||||
.get(&addr)
|
||||
.map(|b| unsafe { &*(&**b as *const DrawContext) })
|
||||
.ok_or(status::FAILED)
|
||||
}
|
||||
|
||||
/// 可变访问(resolve 的写路径;draw 期单线程,存活保证同 resolve)。
|
||||
fn resolve_mut(handle: *mut c_void) -> Result<(), c_int> {
|
||||
resolve(handle).map(|_| ())
|
||||
}
|
||||
|
||||
// ---- 宿主标准颜色板(getColour)----
|
||||
|
||||
/// 宿主标准颜色板:按 `OfxStandardColour` 枚举序(ofxDrawSuite.h:37-46)。
|
||||
const PALETTE: [[f32; 4]; 7] = [
|
||||
// BACKGROUND:宿主视口背景(本宿主 Phase 1 无合成背景,黑)。
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
// ACTIVE:进行中/激活的 overlay 元素。
|
||||
[1.0, 0.84, 0.0, 1.0],
|
||||
// SELECTED:选中元素。
|
||||
[0.0, 1.0, 0.0, 1.0],
|
||||
// DESELECTED:未选中元素。
|
||||
[1.0, 1.0, 1.0, 0.5],
|
||||
// MARQUEE_FG:框选前景。
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
// MARQUEE_BG:框选背景。
|
||||
[0.0, 0.0, 0.0, 0.5],
|
||||
// TEXT:overlay 文本。
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
];
|
||||
|
||||
unsafe extern "C" fn get_colour(
|
||||
ctx: *mut c_void,
|
||||
std_colour: c_int,
|
||||
out: *mut OfxRGBAColourF,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let _ = resolve(ctx)?;
|
||||
let c = PALETTE
|
||||
.get(std_colour as usize)
|
||||
.ok_or(status::ERR_VALUE)?;
|
||||
unsafe {
|
||||
*out = OfxRGBAColourF {
|
||||
r: c[0],
|
||||
g: c[1],
|
||||
b: c[2],
|
||||
a: c[3],
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn set_colour(ctx: *mut c_void, colour: *const OfxRGBAColourF) -> c_int {
|
||||
caught(|| {
|
||||
if colour.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let _ = resolve_mut(ctx)?;
|
||||
let v = unsafe { &*colour };
|
||||
let addr = ctx as usize;
|
||||
let mut live = lock();
|
||||
let slot = live.get_mut(&addr).expect("resolve 已保证存活");
|
||||
slot.colour = [v.r, v.g, v.b, v.a];
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn set_line_width(ctx: *mut c_void, width: c_float) -> c_int {
|
||||
caught(|| {
|
||||
let _ = resolve_mut(ctx)?;
|
||||
let addr = ctx as usize;
|
||||
let mut live = lock();
|
||||
let slot = live.get_mut(&addr).expect("resolve 已保证存活");
|
||||
slot.line_width = width;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn set_line_stipple(ctx: *mut c_void, pattern: c_int) -> c_int {
|
||||
caught(|| {
|
||||
if !(0..=stipple::DOT_DASH).contains(&pattern) {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let _ = resolve_mut(ctx)?;
|
||||
let addr = ctx as usize;
|
||||
let mut live = lock();
|
||||
let slot = live.get_mut(&addr).expect("resolve 已保证存活");
|
||||
slot.stipple = pattern;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn draw(
|
||||
ctx: *mut c_void,
|
||||
prim: c_int,
|
||||
points: *const OfxPointD,
|
||||
count: c_int,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if !(0..=primitive::ELLIPSE).contains(&prim) {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
if count < 0 || (count > 0 && points.is_null()) {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
// 每原语的合法点数(ofxDrawSuite.h draw 文档)。
|
||||
let valid = match prim {
|
||||
primitive::LINES => count >= 2 && count % 2 == 0,
|
||||
primitive::LINE_STRIP | primitive::LINE_LOOP => count >= 2,
|
||||
primitive::RECTANGLE | primitive::ELLIPSE => count == 2,
|
||||
primitive::POLYGON => count >= 3,
|
||||
_ => false,
|
||||
};
|
||||
if !valid {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let c = resolve(ctx)?;
|
||||
let pts: Vec<OfxPointD> = (0..count as usize)
|
||||
.map(|i| unsafe { *points.add(i) })
|
||||
.collect();
|
||||
// 真实 GL 渲染(macOS 3.2 core;非 macOS stub → Failed)。
|
||||
gl_imp::render(c, prim, &pts)
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn draw_text(
|
||||
ctx: *mut c_void,
|
||||
text: *const c_char,
|
||||
pos: *const OfxPointD,
|
||||
_alignment: c_int,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if text.is_null() || pos.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let _ = resolve(ctx)?;
|
||||
// 确认文本有效(UTF-8);随后如实拒绝(无字体光栅化器)。
|
||||
let _ = unsafe { CStr::from_ptr(text) }
|
||||
.to_str()
|
||||
.map_err(|_| status::ERR_VALUE)?;
|
||||
Err(status::ERR_UNSUPPORTED)
|
||||
})
|
||||
}
|
||||
|
||||
/// 函数表布局(与 SDK `OfxDrawSuiteV1` 逐字段一致;ofxDrawSuite.h:85-177)。
|
||||
#[repr(C)]
|
||||
pub struct DrawSuiteV1 {
|
||||
/// getColour:宿主标准颜色板取色。
|
||||
pub get_colour: unsafe extern "C" fn(*mut c_void, c_int, *mut OfxRGBAColourF) -> c_int,
|
||||
/// setColour:设置后续绘制颜色。
|
||||
pub set_colour: unsafe extern "C" fn(*mut c_void, *const OfxRGBAColourF) -> c_int,
|
||||
/// setLineWidth:设置后续线宽。
|
||||
pub set_line_width: unsafe extern "C" fn(*mut c_void, c_float) -> c_int,
|
||||
/// setLineStipple:设置后续虚线样式。
|
||||
pub set_line_stipple: unsafe extern "C" fn(*mut c_void, c_int) -> c_int,
|
||||
/// draw:绘制原语(点数组,canonical 坐标)。
|
||||
pub draw: unsafe extern "C" fn(*mut c_void, c_int, *const OfxPointD, c_int) -> c_int,
|
||||
/// drawText:绘制文本(Phase 1 返回 kOfxStatErrUnsupported)。
|
||||
pub draw_text: unsafe extern "C" fn(*mut c_void, *const c_char, *const OfxPointD, c_int) -> c_int,
|
||||
}
|
||||
|
||||
/// 函数表实例。
|
||||
pub fn suite_v1() -> &'static DrawSuiteV1 {
|
||||
static SUITE: std::sync::OnceLock<DrawSuiteV1> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| DrawSuiteV1 {
|
||||
get_colour,
|
||||
set_colour,
|
||||
set_line_width,
|
||||
set_line_stipple,
|
||||
draw,
|
||||
draw_text,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 标准颜色板:getColour 按枚举取宿主色板(越界 → ErrValue)。
|
||||
#[test]
|
||||
fn get_colour_palette() {
|
||||
let ctx = make_context((640.0, 480.0), (1.0, 1.0));
|
||||
let mut out = OfxRGBAColourF { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
|
||||
unsafe {
|
||||
assert_eq!(get_colour(ctx, std_colour::ACTIVE, &mut out), status::OK);
|
||||
}
|
||||
assert_eq!((out.r, out.g, out.b, out.a), (1.0, 0.84, 0.0, 1.0));
|
||||
// 越界枚举 → ErrValue。
|
||||
unsafe {
|
||||
assert_eq!(get_colour(ctx, 99, &mut out), status::ERR_VALUE);
|
||||
assert_eq!(get_colour(ctx, -1, &mut out), status::ERR_VALUE);
|
||||
// 空输出指针 → ErrValue。
|
||||
assert_eq!(
|
||||
get_colour(ctx, std_colour::BACKGROUND, std::ptr::null_mut()),
|
||||
status::ERR_VALUE
|
||||
);
|
||||
}
|
||||
drop_context(ctx);
|
||||
}
|
||||
|
||||
/// setColour 真实写入状态:再 setLineWidth/setLineStipple 后从存活
|
||||
/// 表读回(draw action 期插件视图)。
|
||||
#[test]
|
||||
fn set_colour_and_line_state() {
|
||||
let ctx = make_context((100.0, 100.0), (2.0, 2.0));
|
||||
let col = OfxRGBAColourF { r: 0.9, g: 0.1, b: 0.2, a: 1.0 };
|
||||
unsafe {
|
||||
assert_eq!(set_colour(ctx, &col), status::OK);
|
||||
assert_eq!(set_line_width(ctx, 3.5), status::OK);
|
||||
assert_eq!(set_line_stipple(ctx, stipple::DASH), status::OK);
|
||||
assert_eq!(set_line_stipple(ctx, 99), status::ERR_VALUE);
|
||||
}
|
||||
|
||||
let addr = ctx as usize;
|
||||
let live = lock();
|
||||
let c = live.get(&addr).unwrap();
|
||||
assert_eq!(c.colour, [0.9, 0.1, 0.2, 1.0]);
|
||||
assert_eq!(c.line_width, 3.5);
|
||||
assert_eq!(c.stipple, stipple::DASH);
|
||||
drop(live);
|
||||
drop_context(ctx);
|
||||
}
|
||||
|
||||
/// 摘除后调用 → Failed("outside kOfxInteractActionDraw" 语义)。
|
||||
#[test]
|
||||
fn draw_suite_outside_draw_fails() {
|
||||
let ctx = make_context((10.0, 10.0), (1.0, 1.0));
|
||||
drop_context(ctx);
|
||||
let col = OfxRGBAColourF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
|
||||
let mut out = OfxRGBAColourF { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
|
||||
let s = std::ffi::CString::new("x").unwrap();
|
||||
let pos = OfxPointD { x: 0.0, y: 0.0 };
|
||||
let pts = [OfxPointD { x: 0.0, y: 0.0 }, OfxPointD { x: 10.0, y: 10.0 }];
|
||||
unsafe {
|
||||
assert_eq!(set_colour(ctx, &col), status::FAILED);
|
||||
assert_eq!(get_colour(ctx, std_colour::TEXT, &mut out), status::FAILED);
|
||||
assert_eq!(set_line_width(ctx, 1.0), status::FAILED);
|
||||
assert_eq!(set_line_stipple(ctx, stipple::SOLID), status::FAILED);
|
||||
// 合法参数(避开空指针检查)→ 摘除后 resolve 失败 → Failed。
|
||||
assert_eq!(draw(ctx, primitive::RECTANGLE, pts.as_ptr(), 2), status::FAILED);
|
||||
// drawText 同样 Failed(先 resolve 失败)。
|
||||
assert_eq!(draw_text(ctx, s.as_ptr(), &pos, 0), status::FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/// draw 原语参数校验(不触 GL):非法原语/点数 → ErrValue。
|
||||
#[test]
|
||||
fn draw_primitive_argument_validation() {
|
||||
let ctx = make_context((10.0, 10.0), (1.0, 1.0));
|
||||
let pts = [OfxPointD { x: 0.0, y: 0.0 }, OfxPointD { x: 10.0, y: 10.0 }];
|
||||
unsafe {
|
||||
// 非法原语枚举。
|
||||
assert_eq!(draw(ctx, 99, pts.as_ptr(), 2), status::ERR_VALUE);
|
||||
assert_eq!(draw(ctx, -1, pts.as_ptr(), 2), status::ERR_VALUE);
|
||||
// 点数不合法:LINES 奇数点。
|
||||
assert_eq!(draw(ctx, primitive::LINES, pts.as_ptr(), 3), status::ERR_VALUE);
|
||||
// RECTANGLE 需恰好 2 点。
|
||||
assert_eq!(draw(ctx, primitive::RECTANGLE, pts.as_ptr(), 3), status::ERR_VALUE);
|
||||
// POLYGON 需 ≥3 点。
|
||||
assert_eq!(draw(ctx, primitive::POLYGON, pts.as_ptr(), 2), status::ERR_VALUE);
|
||||
// 空指针 + 正点数。
|
||||
assert_eq!(draw(ctx, primitive::LINES, std::ptr::null(), 2), status::ERR_VALUE);
|
||||
// 合法参数:环境相关的 GL 路径(无 current 上下文时宿主可能
|
||||
// 失败)——参数校验已过即可,具体状态不在此断言。
|
||||
let st = draw(ctx, primitive::RECTANGLE, pts.as_ptr(), 2);
|
||||
assert!(
|
||||
st == status::OK || st == status::FAILED,
|
||||
"合法参数应 OK(有 GL)或 Failed(无 GL 上下文),got {st}"
|
||||
);
|
||||
}
|
||||
drop_context(ctx);
|
||||
}
|
||||
|
||||
/// drawText:有效参数 → Unsupported(如实拒绝);空指针 → ErrValue。
|
||||
#[test]
|
||||
fn draw_text_is_honest_unsupported() {
|
||||
let ctx = make_context((10.0, 10.0), (1.0, 1.0));
|
||||
let s = std::ffi::CString::new("hello").unwrap();
|
||||
let pos = OfxPointD { x: 1.0, y: 2.0 };
|
||||
unsafe {
|
||||
assert_eq!(draw_text(ctx, s.as_ptr(), &pos, 0), status::ERR_UNSUPPORTED);
|
||||
assert_eq!(draw_text(ctx, std::ptr::null(), &pos, 0), status::ERR_VALUE);
|
||||
}
|
||||
drop_context(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- GL 绘制实现(平台分派)--------------------------------------------
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod gl_imp {
|
||||
use super::{DrawContext, OfxPointD};
|
||||
use crate::suites::status;
|
||||
use std::ffi::c_void;
|
||||
|
||||
// ---- GL 常量(GL 规范值)----
|
||||
const GL_FALSE: i32 = 0;
|
||||
const GL_FLOAT: u32 = 0x1406;
|
||||
const GL_ARRAY_BUFFER: u32 = 0x8892;
|
||||
const GL_STATIC_DRAW: u32 = 0x88E4;
|
||||
const GL_FRAGMENT_SHADER: u32 = 0x8B30;
|
||||
const GL_VERTEX_SHADER: u32 = 0x8B31;
|
||||
const GL_COMPILE_STATUS: u32 = 0x8B81;
|
||||
const GL_LINK_STATUS: u32 = 0x8B82;
|
||||
const GL_TRIANGLES: u32 = 0x0004;
|
||||
const GL_LINES: u32 = 0x0001;
|
||||
const GL_LINE_STRIP: u32 = 0x0003;
|
||||
const GL_LINE_LOOP: u32 = 0x0002;
|
||||
const GL_BLEND: u32 = 0x0BE2;
|
||||
const GL_SRC_ALPHA: u32 = 0x0302;
|
||||
const GL_ONE_MINUS_SRC_ALPHA: u32 = 0x0303;
|
||||
const GL_NO_ERROR: u32 = 0;
|
||||
|
||||
// # Safety: 全部是 OpenGL.framework 导出函数;参数语义见各函数
|
||||
// 注释。调用前提:GL 上下文 current(宿主 draw action 已 acquire)。
|
||||
#[link(name = "OpenGL", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
fn glGenVertexArrays(n: i32, arrays: *mut u32);
|
||||
fn glBindVertexArray(array: u32);
|
||||
fn glGenBuffers(n: i32, buffers: *mut u32);
|
||||
fn glBindBuffer(target: u32, buffer: u32);
|
||||
fn glBufferData(target: u32, size: isize, data: *const c_void, usage: u32);
|
||||
fn glCreateShader(type_: u32) -> u32;
|
||||
fn glShaderSource(shader: u32, count: i32, string: *const *const i8, length: *const i32);
|
||||
fn glCompileShader(shader: u32);
|
||||
fn glGetShaderiv(shader: u32, pname: u32, params: *mut i32);
|
||||
fn glDeleteShader(shader: u32);
|
||||
fn glCreateProgram() -> u32;
|
||||
fn glAttachShader(program: u32, shader: u32);
|
||||
fn glLinkProgram(program: u32);
|
||||
fn glGetProgramiv(program: u32, pname: u32, params: *mut i32);
|
||||
fn glUseProgram(program: u32);
|
||||
fn glGetAttribLocation(program: u32, name: *const i8) -> i32;
|
||||
fn glVertexAttribPointer(
|
||||
index: u32,
|
||||
size: i32,
|
||||
type_: u32,
|
||||
normalized: u8,
|
||||
stride: i32,
|
||||
pointer: *const c_void,
|
||||
);
|
||||
fn glEnableVertexAttribArray(index: u32);
|
||||
fn glGetUniformLocation(program: u32, name: *const i8) -> i32;
|
||||
fn glUniformMatrix4fv(location: i32, count: i32, transpose: u8, value: *const f32);
|
||||
fn glUniform4f(location: i32, x: f32, y: f32, z: f32, w: f32);
|
||||
fn glDrawArrays(mode: u32, first: i32, count: i32);
|
||||
fn glLineWidth(width: f32);
|
||||
fn glEnable(cap: u32);
|
||||
fn glBlendFunc(sfactor: u32, dfactor: u32);
|
||||
fn glGetError() -> u32;
|
||||
}
|
||||
|
||||
/// 顶点着色器(canonical 坐标 + 颜色 uniform;投影映射到 NDC)。
|
||||
const VERT_SRC: &[u8] = b"#version 150\n\
|
||||
in vec2 a_pos;\n\
|
||||
uniform mat4 u_proj;\n\
|
||||
uniform vec4 u_col;\n\
|
||||
out vec4 v_col;\n\
|
||||
void main() {\n\
|
||||
v_col = u_col;\n\
|
||||
gl_Position = u_proj * vec4(a_pos, 0.0, 1.0);\n\
|
||||
}\n\0";
|
||||
|
||||
const FRAG_SRC: &[u8] = b"#version 150\n\
|
||||
in vec4 v_col;\n\
|
||||
out vec4 frag_color;\n\
|
||||
void main() { frag_color = v_col; }\n\0";
|
||||
|
||||
fn compile_shader(kind: u32, src: &[u8]) -> Result<u32, i32> {
|
||||
// SAFETY: glCreateShader/glShaderSource/glCompileShader 均为当前
|
||||
// 上下文的合法调用;src 是 NUL 结尾串(b"...\0")。
|
||||
let sh = unsafe { glCreateShader(kind) };
|
||||
if sh == 0 {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
unsafe {
|
||||
let ptr = src.as_ptr() as *const i8;
|
||||
glShaderSource(sh, 1, &ptr, std::ptr::null());
|
||||
glCompileShader(sh);
|
||||
}
|
||||
let mut ok: i32 = 0;
|
||||
// SAFETY: glGetShaderiv 写 ok。
|
||||
unsafe { glGetShaderiv(sh, GL_COMPILE_STATUS, &mut ok) };
|
||||
if ok == GL_FALSE {
|
||||
// SAFETY: sh 是本函数 glCreateShader 的产物。
|
||||
unsafe { glDeleteShader(sh) };
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
Ok(sh)
|
||||
}
|
||||
|
||||
/// 惰性资源组:(program, vao, vbo)。进程级缓存;VAO/VBO 固定复用,
|
||||
/// 每次 draw 用 glBufferData 重灌顶点(避免逐次分配/泄漏)。
|
||||
fn resources() -> Result<(u32, u32, u32), i32> {
|
||||
static RES: std::sync::OnceLock<Result<(u32, u32, u32), i32>> = std::sync::OnceLock::new();
|
||||
*RES.get_or_init(|| {
|
||||
let vs = match compile_shader(GL_VERTEX_SHADER, VERT_SRC) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let fs = match compile_shader(GL_FRAGMENT_SHADER, FRAG_SRC) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
// SAFETY: vs 是本闭包编译成功的着色器。
|
||||
unsafe { glDeleteShader(vs) };
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let prog = unsafe { glCreateProgram() };
|
||||
if prog == 0 {
|
||||
// SAFETY: vs/fs 均有效。
|
||||
unsafe {
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
}
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
unsafe {
|
||||
glAttachShader(prog, vs);
|
||||
glAttachShader(prog, fs);
|
||||
glLinkProgram(prog);
|
||||
}
|
||||
let mut ok: i32 = 0;
|
||||
// SAFETY: glGetProgramiv 写 ok。
|
||||
unsafe { glGetProgramiv(prog, GL_LINK_STATUS, &mut ok) };
|
||||
// 链接后可删着色器对象(程序保留可执行版本)。
|
||||
unsafe {
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
}
|
||||
if ok == GL_FALSE {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
// SAFETY: 生成并绑定固定 VAO/VBO(3.2 core)。
|
||||
let mut vao: u32 = 0;
|
||||
let mut vbo: u32 = 0;
|
||||
unsafe {
|
||||
glGenVertexArrays(1, &mut vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &mut vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
}
|
||||
Ok((prog, vao, vbo))
|
||||
})
|
||||
}
|
||||
|
||||
/// canonical→NDC 正交投影(列主序 4x4)。
|
||||
fn ortho(cw: f64, ch: f64) -> [f32; 16] {
|
||||
let (cw, ch) = (cw.max(1.0) as f32, ch.max(1.0) as f32);
|
||||
[
|
||||
2.0 / cw, 0.0, 0.0, 0.0,
|
||||
0.0, 2.0 / ch, 0.0, 0.0,
|
||||
0.0, 0.0, -1.0, 0.0,
|
||||
-1.0, -1.0, 0.0, 1.0,
|
||||
]
|
||||
}
|
||||
|
||||
/// 按原语生成 GL 顶点(canonical 坐标;已按图元类型合法)。
|
||||
fn vertices(prim: i32, pts: &[OfxPointD]) -> (u32, Vec<f32>) {
|
||||
match prim {
|
||||
super::primitive::LINES => (
|
||||
GL_LINES,
|
||||
pts.iter().flat_map(|p| [p.x as f32, p.y as f32]).collect(),
|
||||
),
|
||||
super::primitive::LINE_STRIP => (
|
||||
GL_LINE_STRIP,
|
||||
pts.iter().flat_map(|p| [p.x as f32, p.y as f32]).collect(),
|
||||
),
|
||||
super::primitive::LINE_LOOP => (
|
||||
GL_LINE_LOOP,
|
||||
pts.iter().flat_map(|p| [p.x as f32, p.y as f32]).collect(),
|
||||
),
|
||||
super::primitive::RECTANGLE => {
|
||||
let (x1, y1) = (pts[0].x as f32, pts[0].y as f32);
|
||||
let (x2, y2) = (pts[1].x as f32, pts[1].y as f32);
|
||||
// 两个三角形组成实心矩形(对角点定义)。
|
||||
(
|
||||
GL_TRIANGLES,
|
||||
vec![
|
||||
x1, y1, x2, y1, x1, y2,
|
||||
x1, y2, x2, y1, x2, y2,
|
||||
],
|
||||
)
|
||||
}
|
||||
super::primitive::POLYGON => {
|
||||
// 三角扇:0,i,i+1(凸多边形约定)。
|
||||
let mut v = Vec::new();
|
||||
for i in 1..pts.len() - 1 {
|
||||
v.extend_from_slice(&[
|
||||
pts[0].x as f32, pts[0].y as f32,
|
||||
pts[i].x as f32, pts[i].y as f32,
|
||||
pts[i + 1].x as f32, pts[i + 1].y as f32,
|
||||
]);
|
||||
}
|
||||
(GL_TRIANGLES, v)
|
||||
}
|
||||
super::primitive::ELLIPSE => {
|
||||
// 椭圆**线框**(文档),包围盒内 64 段线环。
|
||||
let (cx, cy) = ((pts[0].x + pts[1].x) / 2.0, (pts[0].y + pts[1].y) / 2.0);
|
||||
let (rx, ry) = (
|
||||
(pts[1].x - pts[0].x).abs() / 2.0,
|
||||
(pts[1].y - pts[0].y).abs() / 2.0,
|
||||
);
|
||||
let n = 64;
|
||||
let mut v = Vec::with_capacity(n * 2);
|
||||
for i in 0..n {
|
||||
let a = std::f64::consts::TAU * i as f64 / n as f64;
|
||||
v.push((cx + rx * a.cos()) as f32);
|
||||
v.push((cy + ry * a.sin()) as f32);
|
||||
}
|
||||
(GL_LINE_LOOP, v)
|
||||
}
|
||||
_ => (GL_TRIANGLES, Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 真实渲染:绑定固定 VAO/VBO、重灌顶点、设置 uniform、绘制。
|
||||
/// 要求 GL 上下文 current(宿主 draw action 已 acquire);无 current
|
||||
/// 上下文时不发任何 GL 命令(macOS 无 current 调 GL 是未定义行为,
|
||||
/// 直接失败)。
|
||||
pub fn render(ctx: &DrawContext, prim: i32, pts: &[OfxPointD]) -> Result<(), i32> {
|
||||
if !crate::gl_bridge::is_current() {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
let (prog, vao, vbo) = resources().map_err(|_| status::FAILED)?;
|
||||
let (mode, verts) = vertices(prim, pts);
|
||||
if verts.is_empty() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
// 合成:非不透明色 "over"(ofxDrawSuite.h setColour 文档)。
|
||||
if ctx.colour[3] < 1.0 {
|
||||
// SAFETY: 开启混合并设因子(上下文 current)。
|
||||
unsafe {
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
}
|
||||
// SAFETY: 以下 GL 调用均要求上下文 current(宿主已保证);对象
|
||||
// 名来自 resources()(本进程唯一 GL 使用方)。
|
||||
unsafe {
|
||||
glUseProgram(prog);
|
||||
glBindVertexArray(vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(
|
||||
GL_ARRAY_BUFFER,
|
||||
(verts.len() * 4) as isize,
|
||||
verts.as_ptr() as *const c_void,
|
||||
GL_STATIC_DRAW,
|
||||
);
|
||||
|
||||
let a_pos = glGetAttribLocation(prog, b"a_pos\0".as_ptr() as *const i8);
|
||||
if a_pos < 0 {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
glVertexAttribPointer(a_pos as u32, 2, GL_FLOAT, 0, 0, std::ptr::null());
|
||||
glEnableVertexAttribArray(a_pos as u32);
|
||||
|
||||
let u_proj = glGetUniformLocation(prog, b"u_proj\0".as_ptr() as *const i8);
|
||||
let u_col = glGetUniformLocation(prog, b"u_col\0".as_ptr() as *const i8);
|
||||
let cw = ctx.viewport.0 / ctx.pixel_scale.0.max(1e-6);
|
||||
let ch = ctx.viewport.1 / ctx.pixel_scale.1.max(1e-6);
|
||||
let m = ortho(cw, ch);
|
||||
glUniformMatrix4fv(u_proj, 1, 0, m.as_ptr());
|
||||
glUniform4f(u_col, ctx.colour[0], ctx.colour[1], ctx.colour[2], ctx.colour[3]);
|
||||
|
||||
if ctx.line_width > 0.0 {
|
||||
glLineWidth(ctx.line_width);
|
||||
}
|
||||
glDrawArrays(mode, 0, (verts.len() / 2) as i32);
|
||||
|
||||
// 解除绑定(对象保留复用)。
|
||||
glBindVertexArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
let err = unsafe { glGetError() };
|
||||
if err != GL_NO_ERROR {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 非 macOS:Draw suite 的 GL 绘制 stub(无 GL 桥 → Failed)。
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod gl_imp {
|
||||
use super::{DrawContext, OfxPointD};
|
||||
use crate::suites::status;
|
||||
|
||||
pub fn render(_ctx: &DrawContext, _prim: i32, _pts: &[OfxPointD]) -> Result<(), i32> {
|
||||
Err(status::FAILED)
|
||||
}
|
||||
}
|
||||
@@ -27,29 +27,36 @@
|
||||
//! RenderScale/PixelAspectRatio/Bounds/RegionOfDefinition/RowBytes/
|
||||
//! Field/UniqueIdentifier);宿主侧存强引用([`LIVE_TEXTURES`]),
|
||||
//! clipFreeTexture 摘除即释放(对应 HS 的 get/release 配对)。
|
||||
//! **OpenGLTextureIndex 是真实 GL 纹理名**(GL 模式,见
|
||||
//! [`crate::gl_bridge`] 方案 B):Output clip 是宿主为输出帧建的
|
||||
//! GL 纹理(经 GlCtx 注入,render 驱动负责建/FBO 挂载/回读/删除);
|
||||
//! 输入 clip 是本 suite 在 GL 渲染期经桥上传图像得到的 GL 纹理
|
||||
//! (clipFreeTexture 删除)。0 = CPU 回退(无 GL 名,插件按规范回退)。
|
||||
//! - Output clip:`format` 忽略,宿主返回已附着的输出纹理句柄——
|
||||
//! 绑定渲染目标的动作(ofxGPURender.h "the host must bind the
|
||||
//! resulting texture as the current color buffer")由调用方约定:
|
||||
//! GL render 驱动的 C ABI 契约要求 oakrender 在进入 render_job 前
|
||||
//! 已把输出纹理附着为渲染器输出目标(等价 C++ 的
|
||||
//! `PluginRenderer::attach_output_texture`)。clipFreeTexture 对
|
||||
//! Output 只释放句柄、不删纹理(宿主还要读它)。
|
||||
//! - 输入纹理经 [`crate::render`] 在渲染器上创建(CPU 帧 →
|
||||
//! GL 上传)。纹理格式:全链路 F32 约束下,像素深度按 clip 协商
|
||||
//! 结果(恒 F32);`format` 参数(kOfxImageEffectGLFormat*)若
|
||||
//! 请求的分量与协商分量不符,Phase 2 不做转换 → Failed(规范要求
|
||||
//! "host ensures it gives the requested format"——宁可显式失败也
|
||||
//! 不静默给错格式)。ofxGPURender.h 注明"宿主无需按 Clip
|
||||
//! Preferences 把图像重映射到插件请求的位深",插件以纹理句柄的
|
||||
//! PixelDepth/Components 为准。
|
||||
//! GL render 驱动在进入 render_gl 前已把输出 GL 纹理挂进 FBO 并
|
||||
//! 绑定(等价 C++ 的 `PluginRenderer::attach_output_texture`)。
|
||||
//! clipFreeTexture 对 Output 只释放句柄、不删纹理(宿主还要回读它,
|
||||
//! 回读后由 render 驱动删除)。
|
||||
//! - 输入纹理在 GL 模式经 [`crate::gl_bridge::create_input_texture`]
|
||||
//! 上传(CPU 帧 → GL 纹理);CPU 模式(无 GL 上下文)维持
|
||||
//! [`crate::render`] 的 CPU 纹理。纹理格式:全链路 F32 约束下,
|
||||
//! 像素深度按 clip 协商结果(恒 F32);`format` 参数
|
||||
//! (kOfxImageEffectGLFormat*)若请求的分量与协商分量不符,Phase 2
|
||||
//! 不做转换 → Failed(规范要求 "host ensures it gives the requested
|
||||
//! format"——宁可显式失败也不静默给错格式)。ofxGPURender.h 注明
|
||||
//! "宿主无需按 Clip Preferences 把图像重映射到插件请求的位深",
|
||||
//! 插件以纹理句柄的 PixelDepth/Components 为准。
|
||||
//! - `flushResources`:宿主在 render 之间不缓存 GPU 资源(纹理随
|
||||
//! clipFreeTexture 立即释放)→ 无可释放 → kOfxStatReplyDefault
|
||||
//! (规范:"nothing the host could do")。
|
||||
//! - GL 上下文规则(ofxGPURender.h "OpenGL Current Context"):宿主
|
||||
//! 只在 Render/BeginSequenceRender/EndSequenceRender/Attach/Detach
|
||||
//! 期间要求上下文 current;本实现的约定是调用方(oakrender
|
||||
//! PluginJob 路径)在调用前置好上下文,本 suite 经
|
||||
//! [`crate::suites::gl_ctx`] TLS 取渲染器句柄。
|
||||
//! 期间要求上下文 current;本实现的约定是 render 驱动一次 acquire
|
||||
//! 整个 GL render action([`crate::gl_bridge::acquire`]),本 suite
|
||||
//! 回调期间上下文恒 current,经 [`crate::suites::gl_ctx`] TLS 取
|
||||
//! 渲染器句柄。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, c_double, c_int, c_void, CStr, CString};
|
||||
@@ -164,37 +171,64 @@ mod tests {
|
||||
// ---- 存活纹理表 -----------------------------------------------------------
|
||||
|
||||
/// 存活 GL 纹理表:clipLoadTexture 产出(props 地址 → 属性集 +
|
||||
/// 纹理值 + 是否输出 clip);clipFreeTexture 摘除即释放——对应
|
||||
/// HS 的 get/release 配对(HS: ofxhImageEffect.cpp:2336-2351)。
|
||||
/// 纹理是 oakrender 值(drop 自动释放后端 token;原
|
||||
/// `texture_free` 调用面随值模型删除)。
|
||||
/// 纹理值 + 是否输出 clip + 真实 GL 纹理名[可选]);clipFreeTexture
|
||||
/// 摘除即释放——对应 HS 的 get/release 配对
|
||||
/// (HS: ofxhImageEffect.cpp:2336-2351)。纹理是 oakrender 值(drop
|
||||
/// 自动释放后端 token;原 `texture_free` 调用面随值模型删除)。
|
||||
///
|
||||
/// `gl_texture`:GL 模式下输入 clip 经 [`crate::gl_bridge`] 建的真实
|
||||
/// GL 纹理名(输出 clip 恒 None——宿主自建并负责生命周期,见
|
||||
/// [`crate::gl_bridge`] 模块文档);摘除时经
|
||||
/// [`delete_gl_texture_if_gl`] 删除。
|
||||
///
|
||||
/// 属性集必须**装箱**(Box 稳定堆地址):纹理句柄指向它,函数返回后
|
||||
/// 必须仍存活;栈上临时变量会悬垂(phase-2 实现初版的 bug)。
|
||||
static LIVE_TEXTURES: std::sync::LazyLock<
|
||||
Mutex<HashMap<usize, (Box<PropertySet>, crate::render::Texture, bool)>>,
|
||||
Mutex<HashMap<usize, (Box<PropertySet>, crate::render::Texture, bool, Option<i32>)>>,
|
||||
> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// 登记纹理(clipLoadTexture 内部;`props` 装箱后取地址为句柄基址)。
|
||||
/// `gl_texture`:真实 GL 纹理名(输入 clip GL 模式;无则 None)。
|
||||
fn register(
|
||||
props: Box<PropertySet>,
|
||||
texture: crate::render::Texture,
|
||||
is_output: bool,
|
||||
gl_texture: Option<i32>,
|
||||
) -> usize {
|
||||
let addr = &*props as *const PropertySet as usize;
|
||||
LIVE_TEXTURES
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(addr, (props, texture, is_output));
|
||||
.insert(addr, (props, texture, is_output, gl_texture));
|
||||
addr
|
||||
}
|
||||
|
||||
/// 删除 GL 纹理(上下文 current 时;否则留给下次 GL 渲染前的兜底——
|
||||
/// 正常路径 clipFreeTexture/purge 都在 GL render 期,上下文必 current)。
|
||||
fn delete_gl_texture_if_gl(name: i32) {
|
||||
if crate::suites::gl_ctx().is_some() {
|
||||
crate::gl_bridge::delete_gl_texture(name);
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放全部残留输入纹理(GL render action 返回后的安全网:规范要求
|
||||
/// 插件在 action 返回前 clipFreeTexture 全部句柄;遗漏的输入纹理在
|
||||
/// 此释放,输出纹理保留——宿主还要读它)。
|
||||
/// 此释放,输出纹理保留——宿主还要读它)。GL 输入纹理同步删除
|
||||
/// (上下文 current;render_gl 在清 GlCtx TLS 前调用本函数)。
|
||||
pub(crate) fn purge_leftovers() {
|
||||
let mut live = LIVE_TEXTURES.lock().unwrap_or_else(|e| e.into_inner());
|
||||
live.retain(|_, (_, _, is_output)| *is_output);
|
||||
let dropped: Vec<Option<i32>> = live
|
||||
.iter()
|
||||
.filter(|(_, (_, _, is_output, _))| !*is_output)
|
||||
.map(|(_, (_, _, _, gl))| *gl)
|
||||
.collect();
|
||||
live.retain(|_, (_, _, is_output, _)| *is_output);
|
||||
drop(live);
|
||||
for gl in dropped {
|
||||
if let Some(gl) = gl {
|
||||
delete_gl_texture_if_gl(gl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
@@ -240,12 +274,11 @@ fn clip_string(clip: &ClipInstance, name: &str, default: &str) -> String {
|
||||
}
|
||||
|
||||
/// 构造纹理属性集(ofxGPURender.h 规定的属性;输入与输出共用,
|
||||
/// 只是数据来源不同)。`OpenGLTextureIndex` 经
|
||||
/// [`crate::render::texture_id`](桩恒 0——wgpu 后端无 GL 命名空间;
|
||||
/// CPU 回退下插件按规范回退,纹理内容仍可经 clipGetImage 取用)。
|
||||
/// 只是数据来源不同)。`OpenGLTextureIndex` 为真实 GL 纹理名
|
||||
/// (`texture_index`;0 = 无 GL 名——CPU 回退下插件按规范回退,纹理
|
||||
/// 内容仍可经 clipGetImage 取用)。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn make_texture_props(
|
||||
texture: &crate::render::Texture,
|
||||
width: f64,
|
||||
height: f64,
|
||||
components: crate::image::Components,
|
||||
@@ -254,12 +287,10 @@ fn make_texture_props(
|
||||
par: f64,
|
||||
scale: crate::instance::RenderScale,
|
||||
row_bytes: i32,
|
||||
texture_index: i32,
|
||||
) -> PropertySet {
|
||||
let props = PropertySet::new();
|
||||
props.set_one(
|
||||
GL_TEXTURE_INDEX,
|
||||
Value::Int(crate::render::texture_id(texture)),
|
||||
);
|
||||
props.set_one(GL_TEXTURE_INDEX, Value::Int(texture_index));
|
||||
props.set_one(GL_TEXTURE_TARGET, Value::Int(GL_TEXTURE_2D));
|
||||
props.set_one(
|
||||
crate::image::K_IMAGE_EFFECT_PROP_PIXEL_DEPTH,
|
||||
@@ -343,8 +374,10 @@ unsafe extern "C" fn clip_load_texture(
|
||||
if w <= 0.0 || h <= 0.0 {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
// OpenGLTextureIndex = 宿主为输出帧建的真实 GL 纹理名
|
||||
// (render 驱动 GL 分支经 GlCtx 注入;0 = CPU 回退)。
|
||||
let index = gl.output_gl_texture.unwrap_or(0);
|
||||
let props = make_texture_props(
|
||||
&tex,
|
||||
w,
|
||||
h,
|
||||
crate::image::Components::Rgba,
|
||||
@@ -353,8 +386,9 @@ unsafe extern "C" fn clip_load_texture(
|
||||
clip_par(c),
|
||||
scale,
|
||||
(w as i32) * 4 * 4,
|
||||
index,
|
||||
);
|
||||
let addr = register(Box::new(props), tex, true);
|
||||
let addr = register(Box::new(props), tex, true, None);
|
||||
unsafe { *out = tag::make(addr as *const PropertySet, tag::PROPERTY_SET) };
|
||||
return Ok(());
|
||||
}
|
||||
@@ -386,6 +420,17 @@ unsafe extern "C" fn clip_load_texture(
|
||||
if w <= 0.0 || h <= 0.0 {
|
||||
return Err(status::FAILED);
|
||||
}
|
||||
// GL 模式:把输入图像上传成真实 GL 纹理(RGBA32F;上下文
|
||||
// current——render 驱动已 acquire),OpenGLTextureIndex 返回
|
||||
// 真实名;GL 上传失败 → 显式 Failed(插件按规范继续)。非 GL
|
||||
// 上下文(本函数仅在 GL 渲染期可达,此分支是 CPU 纹理兜底):
|
||||
// 仍建 CPU 纹理、索引 0。
|
||||
let gl_tex = crate::gl_bridge::create_input_texture(
|
||||
w as i32,
|
||||
h as i32,
|
||||
image.pixels(),
|
||||
)
|
||||
.ok();
|
||||
let params = crate::render::VideoParams {
|
||||
width: w as i32,
|
||||
height: h as i32,
|
||||
@@ -395,7 +440,6 @@ unsafe extern "C" fn clip_load_texture(
|
||||
let tex = crate::render::texture_create(¶ms, image.pixels(), image.row_bytes() as i32)
|
||||
.map_err(|_| status::ERR_MEMORY)?;
|
||||
let props = make_texture_props(
|
||||
&tex,
|
||||
w,
|
||||
h,
|
||||
components,
|
||||
@@ -404,8 +448,9 @@ unsafe extern "C" fn clip_load_texture(
|
||||
clip_par(c),
|
||||
scale,
|
||||
image.row_bytes() as i32,
|
||||
gl_tex.unwrap_or(0),
|
||||
);
|
||||
let addr = register(Box::new(props), tex, false);
|
||||
let addr = register(Box::new(props), tex, false, gl_tex);
|
||||
unsafe { *out = tag::make(addr as *const PropertySet, tag::PROPERTY_SET) };
|
||||
Ok(())
|
||||
})
|
||||
@@ -440,9 +485,9 @@ fn clip_par(c: &ClipInstance) -> f64 {
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
/// clipFreeTexture:释放纹理(输入 clip 删除纹理值;Output 只释放
|
||||
/// 句柄不删纹理——宿主还要读它)。纹理是值:摘除表条目即 drop
|
||||
/// (原 `texture_free` 调用面随值模型删除)。
|
||||
/// clipFreeTexture:释放纹理(输入 clip 删除纹理值并释放真实 GL
|
||||
/// 纹理;Output 只释放句柄不删纹理——宿主还要读它)。纹理是值:
|
||||
/// 摘除表条目即 drop(原 `texture_free` 调用面随值模型删除)。
|
||||
unsafe extern "C" fn clip_free_texture(texture_handle: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
if texture_handle.is_null() {
|
||||
@@ -454,7 +499,13 @@ unsafe extern "C" fn clip_free_texture(texture_handle: *mut c_void) -> c_int {
|
||||
live.remove(&addr)
|
||||
};
|
||||
match entry {
|
||||
Some((_props, _texture, _is_output)) => Ok(()),
|
||||
Some((_props, _texture, _is_output, gl_tex)) => {
|
||||
// 输入 clip 的真实 GL 纹理随释放删除(上下文 current)。
|
||||
if let Some(gl) = gl_tex {
|
||||
delete_gl_texture_if_gl(gl);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
None => Err(status::ERR_BAD_HANDLE),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -579,6 +579,7 @@ mod tests {
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
interact: std::sync::Mutex::new(None),
|
||||
});
|
||||
let ih = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
|
||||
unsafe {
|
||||
|
||||
@@ -0,0 +1,814 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! OfxInteractSuiteV1 + [`Interact`] 宿主对象。
|
||||
//!
|
||||
//! Interact 是插件自定义 UI 的宿主侧对象(主进程 UI 事件宿主):与
|
||||
//! worker 进程里的渲染实例并存(OFX 允许同一插件多实例),经
|
||||
//! [`crate::instance::Instance::new_interact`] 创建、绑定到效果实例。
|
||||
//!
|
||||
//! ## 生命周期
|
||||
//!
|
||||
//! ```text
|
||||
//! new_interact → kOfxActionNewInteract(创建;插件拒绝 → None)
|
||||
//! describe → kOfxActionDescribe(kOfxActionDescribeInteract)
|
||||
//! create_instance→ kOfxActionCreateInstance(kOfxActionCreateInstanceInteract)
|
||||
//! draw/pen/key/idle → kOfxInteractAction*(宿主→插件)
|
||||
//! destroy → kOfxActionDestroyInstance(kOfxActionDestroyInstanceInteract;
|
||||
//! Instance 销毁时自动连带)
|
||||
//! ```
|
||||
//!
|
||||
//! 所有 action 走 `Interact::call` → 插件入口(overlay interact V2/V1
|
||||
//! 入口,未声明则插件 main entry;任务契约:new_interact 向插件入口
|
||||
//! 发 kOfxActionNewInteract)。返回值(OfxStatus)如实透传。
|
||||
//!
|
||||
//! ## 语义(ofxInteract.h)
|
||||
//!
|
||||
//! - pen/key action 的返回:kOfxStatOK = 插件已处理该事件,宿主不应再
|
||||
//! 把事件传给视图中其他交互对象;kOfxStatReplyDefault = 插件未处理,
|
||||
//! 宿主可自行处置。
|
||||
//! - pen 坐标:调用方给 viewport 像素;inArgs 另带 canonical 坐标
|
||||
//! (kOfxInteractPropPenPosition,= viewport 像素 / pixelScale)与
|
||||
//! pixelScale、pressure(两态笔按 ofxInteract.h 映射 0/1)。
|
||||
//! - key:kOfxPropKeySym(ofxKeySyms.h 关键码)+ kOfxPropKeyString
|
||||
//! (UTF-8;无 UTF8 编码的键为空串)。
|
||||
//! - draw:宿主在调用前经 [`crate::gl_bridge::acquire`] 保持 GL current
|
||||
//! 整个 action(插件发原生 GL 命令或经 Draw suite,ofxInteract.h
|
||||
//! "the openGL context for this interact has been set");inArgs 带
|
||||
//! viewport/pixelScale/time/backgroundImage/backgroundColour/
|
||||
//! drawContext。
|
||||
|
||||
use std::ffi::{c_int, c_void, CString};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::host::{EntryPoint, Plugin};
|
||||
use crate::property::{PropertySet, Value};
|
||||
use crate::suites::{status, tag};
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
/// Interact 实例。`Arc<Interact>` 管理生命周期(app 侧持句柄 +
|
||||
/// [`crate::instance::Instance`] 的 interact 字段)。
|
||||
///
|
||||
/// `#[repr(C)]` + props 在偏移 0(句柄约定:interact handle =
|
||||
/// `&props` | INTERACT 标签;property suite 剥标签后直读属性集)。
|
||||
#[repr(C)]
|
||||
pub struct Interact {
|
||||
/// interact 实例级属性集(偏移 0,句柄约定)。
|
||||
pub props: PropertySet,
|
||||
/// 所属插件。
|
||||
pub plugin: Arc<Plugin>,
|
||||
/// interact 的入口(overlay interact V2/V1 入口,未声明则插件
|
||||
/// main entry)。interact 的所有 action 都发到这里。
|
||||
pub entry: EntryPoint,
|
||||
/// 效果实例句柄(tagged INSTANCE;interact action inArgs 的
|
||||
/// kOfxPropEffectInstance)。不透明令牌,不解引用。
|
||||
pub effect_handle: *mut c_void,
|
||||
/// 插件经 interactSwapBuffers 请求的缓冲交换(app 侧轮询后清零)。
|
||||
pub swap_requested: AtomicBool,
|
||||
/// 插件经 interactRedraw 请求的重绘(app 侧轮询后清零)。
|
||||
pub redraw_requested: AtomicBool,
|
||||
/// destroy 是否已通知(幂等门;Instance 销毁连带触发)。
|
||||
pub destroyed: AtomicBool,
|
||||
}
|
||||
|
||||
// SAFETY: effect_handle 是不透明令牌(同 [`crate::property::Value::Pointer`],
|
||||
// 从不在此对象上解引用);entry 是函数指针,调用永远发生在宿主控制的
|
||||
// action 分发点;其余字段(Arc/Mutex/AtomicBool/PropertySet)均线程安全。
|
||||
// Interact 经 Instance.interact 字段跨线程可达,因此 Send/Sync 成立。
|
||||
unsafe impl Send for Interact {}
|
||||
unsafe impl Sync for Interact {}
|
||||
|
||||
impl Interact {
|
||||
/// 构造(宿主内部;[`crate::instance::Instance::new_interact`] 调)。
|
||||
/// `effect_handle` 为 tagged INSTANCE 句柄(不透明令牌)。
|
||||
pub(crate) fn new(
|
||||
plugin: Arc<Plugin>,
|
||||
entry: EntryPoint,
|
||||
effect_handle: *mut c_void,
|
||||
) -> Arc<Interact> {
|
||||
let i = Arc::new(Interact {
|
||||
props: PropertySet::new(),
|
||||
plugin,
|
||||
entry,
|
||||
effect_handle,
|
||||
swap_requested: AtomicBool::new(false),
|
||||
redraw_requested: AtomicBool::new(false),
|
||||
destroyed: AtomicBool::new(false),
|
||||
});
|
||||
init_interact_props(&i.props);
|
||||
i
|
||||
}
|
||||
|
||||
/// interact handle(tagged INTERACT)。
|
||||
pub(crate) fn handle(&self) -> *mut c_void {
|
||||
tag::make(&self.props as *const PropertySet, tag::INTERACT)
|
||||
}
|
||||
|
||||
/// 向插件入口发一个 interact action。
|
||||
///
|
||||
/// # Safety
|
||||
/// handle 与 action 匹配(Interact 生命周期由宿主控制;destroy 后
|
||||
/// 不再调用)。
|
||||
pub(crate) fn call(&self, action: &str, in_args: &PropertySet, out_args: &PropertySet) -> i32 {
|
||||
unsafe {
|
||||
self.plugin.call_entry(self.entry, action, self.handle(), in_args, out_args)
|
||||
}
|
||||
}
|
||||
|
||||
/// 取 effect 实例句柄(inArgs 的 kOfxPropEffectInstance)。
|
||||
fn effect_instance_prop(&self) -> Value {
|
||||
Value::Pointer(self.effect_handle)
|
||||
}
|
||||
|
||||
/// 当前 pixelScale(interact props,默认 1,1)。
|
||||
fn pixel_scale(&self) -> (f64, f64) {
|
||||
read_double2(&self.props, crate::host::PROP_INTERACT_PIXEL_SCALE)
|
||||
.unwrap_or((1.0, 1.0))
|
||||
}
|
||||
|
||||
/// describe(kOfxActionDescribe == kOfxActionDescribeInteract,
|
||||
/// ofxInteract.h:171)。inArgs/outArgs 冗余为 NULL。
|
||||
pub fn describe(&self) -> i32 {
|
||||
let empty = PropertySet::new();
|
||||
self.call(crate::host::ACTION_DESCRIBE, &empty, &empty)
|
||||
}
|
||||
|
||||
/// createInstance(kOfxActionCreateInstance ==
|
||||
/// kOfxActionCreateInstanceInteract,ofxInteract.h:198)。
|
||||
/// draw/pen/key 的前置(ofxInteract.h 各 action 的 \pre)。
|
||||
pub fn create_instance(&self) -> i32 {
|
||||
let empty = PropertySet::new();
|
||||
self.call(crate::host::ACTION_CREATE_INSTANCE, &empty, &empty)
|
||||
}
|
||||
|
||||
/// draw action(ofxInteract.h:265)。宿主在调用前
|
||||
/// [`crate::gl_bridge::acquire`] 保持 GL current 整个 action;inArgs
|
||||
/// 带 kOfxInteractPropPixelScale/ViewportSize/BackgroundColour/
|
||||
/// BackgroundImage/Time/RenderScale/DrawContext + kOfxPropEffectInstance。
|
||||
///
|
||||
/// `background_image`:宿主持有的背景图像句柄(本宿主 Phase 1 无
|
||||
/// 合成背景,传 None 即空——任务契约"可空")。
|
||||
///
|
||||
/// 返回值:插件的 OfxStatus 如实透传;GL 上下文不可用(非 macOS /
|
||||
/// 无上下文)时返回 kOfxStatErrMissingHostFeature(宿主侧失败,未
|
||||
/// 触达插件)。
|
||||
pub fn draw(
|
||||
&self,
|
||||
viewport_size: (f64, f64),
|
||||
pixel_scale: (f64, f64),
|
||||
time: f64,
|
||||
background_image: Option<*mut c_void>,
|
||||
) -> i32 {
|
||||
// 1. GL current 整个 draw action(插件发原生 GL 命令或经 Draw
|
||||
// suite;失败即无法绘制 → 不触达插件)。
|
||||
let _guard = match crate::gl_bridge::acquire() {
|
||||
Ok(g) => g,
|
||||
Err(_) => return status::ERR_MISSING_HOST_FEATURE,
|
||||
};
|
||||
|
||||
// 2. Draw suite 上下文(存活注册;draw 返回后摘除)。
|
||||
let draw_ctx = crate::suites::draw::make_context(viewport_size, pixel_scale);
|
||||
|
||||
let in_args = PropertySet::new();
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_PIXEL_SCALE,
|
||||
vec![Value::Double(pixel_scale.0), Value::Double(pixel_scale.1)],
|
||||
);
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_VIEWPORT_SIZE,
|
||||
vec![Value::Double(viewport_size.0), Value::Double(viewport_size.1)],
|
||||
);
|
||||
in_args.set_one(
|
||||
crate::host::PROP_INTERACT_BACKGROUND_IMAGE,
|
||||
Value::Pointer(background_image.unwrap_or(std::ptr::null_mut())),
|
||||
);
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_BACKGROUND_COLOUR,
|
||||
vec![Value::Double(0.0), Value::Double(0.0), Value::Double(0.0)],
|
||||
);
|
||||
in_args.set_one(crate::host::PROP_TIME, Value::Double(time));
|
||||
in_args.define(
|
||||
crate::host::PROP_RENDER_SCALE,
|
||||
vec![Value::Double(1.0), Value::Double(1.0)],
|
||||
);
|
||||
in_args.set_one(crate::host::PROP_INTERACT_DRAW_CONTEXT, Value::Pointer(draw_ctx));
|
||||
in_args.set_one(crate::host::PROP_EFFECT_INSTANCE, self.effect_instance_prop());
|
||||
|
||||
// 3. interact 实例属性同步(kOfxInteractPropPixelScale 等是实例
|
||||
// 属性集上只读的"当前状态",ofxInteract.h:58)。
|
||||
self.props.define(
|
||||
crate::host::PROP_INTERACT_PIXEL_SCALE,
|
||||
vec![Value::Double(pixel_scale.0), Value::Double(pixel_scale.1)],
|
||||
);
|
||||
self.props.define(
|
||||
crate::host::PROP_INTERACT_VIEWPORT_SIZE,
|
||||
vec![Value::Double(viewport_size.0), Value::Double(viewport_size.1)],
|
||||
);
|
||||
self.props.set_one(
|
||||
crate::host::PROP_INTERACT_BACKGROUND_IMAGE,
|
||||
Value::Pointer(background_image.unwrap_or(std::ptr::null_mut())),
|
||||
);
|
||||
|
||||
let out = PropertySet::new();
|
||||
let st = self.call(crate::host::ACTION_INTERACT_DRAW, &in_args, &out);
|
||||
crate::suites::draw::drop_context(draw_ctx);
|
||||
st
|
||||
}
|
||||
|
||||
/// 装配 pen 类 action 的 inArgs(PenMotion/PenDown/PenUp 共用,
|
||||
/// ofxInteract.h:274-321)。
|
||||
fn pen_in_args(&self, pen_viewport: (f64, f64), pressure: f64, time: f64) -> PropertySet {
|
||||
let (ps_x, ps_y) = self.pixel_scale();
|
||||
// canonical = viewport 像素 / pixelScale(pixelScale 是
|
||||
// canonical→屏幕像素的换算比例)。
|
||||
let in_args = PropertySet::new();
|
||||
in_args.set_one(crate::host::PROP_EFFECT_INSTANCE, self.effect_instance_prop());
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_PIXEL_SCALE,
|
||||
vec![Value::Double(ps_x), Value::Double(ps_y)],
|
||||
);
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_BACKGROUND_COLOUR,
|
||||
vec![Value::Double(0.0), Value::Double(0.0), Value::Double(0.0)],
|
||||
);
|
||||
in_args.set_one(crate::host::PROP_TIME, Value::Double(time));
|
||||
in_args.define(
|
||||
crate::host::PROP_RENDER_SCALE,
|
||||
vec![Value::Double(1.0), Value::Double(1.0)],
|
||||
);
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_PEN_POSITION,
|
||||
vec![
|
||||
Value::Double(pen_viewport.0 / ps_x.max(1e-9)),
|
||||
Value::Double(pen_viewport.1 / ps_y.max(1e-9)),
|
||||
],
|
||||
);
|
||||
in_args.define(
|
||||
crate::host::PROP_INTERACT_PEN_VIEWPORT_POSITION,
|
||||
vec![
|
||||
Value::Int(pen_viewport.0.round() as i32),
|
||||
Value::Int(pen_viewport.1.round() as i32),
|
||||
],
|
||||
);
|
||||
in_args.set_one(crate::host::PROP_INTERACT_PEN_PRESSURE, Value::Double(pressure));
|
||||
in_args
|
||||
}
|
||||
|
||||
/// pen_motion(ofxInteract.h:302)。`pen_viewport` 为视口像素坐标;
|
||||
/// `pen_down` 表示笔是否按下(两态笔按 ofxInteract.h:114 映射压力
|
||||
/// 1.0/0.0)。
|
||||
pub fn pen_motion(&self, pen_viewport: (f64, f64), pen_down: bool, time: f64) -> i32 {
|
||||
let pressure = if pen_down { 1.0 } else { 0.0 };
|
||||
let in_args = self.pen_in_args(pen_viewport, pressure, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_PEN_MOTION, &in_args, &out)
|
||||
}
|
||||
|
||||
/// pen_down(ofxInteract.h:340)。压力 1.0。
|
||||
pub fn pen_down(&self, pen_viewport: (f64, f64), time: f64) -> i32 {
|
||||
let in_args = self.pen_in_args(pen_viewport, 1.0, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_PEN_DOWN, &in_args, &out)
|
||||
}
|
||||
|
||||
/// pen_up(ofxInteract.h:376)。压力 0.0。
|
||||
pub fn pen_up(&self, pen_viewport: (f64, f64), time: f64) -> i32 {
|
||||
let in_args = self.pen_in_args(pen_viewport, 0.0, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_PEN_UP, &in_args, &out)
|
||||
}
|
||||
|
||||
/// 装配 key 类 action 的 inArgs(KeyDown/KeyUp 共用,
|
||||
/// ofxInteract.h:384-442)。
|
||||
fn key_in_args(&self, key_sym: i32, key_string: &str, time: f64) -> PropertySet {
|
||||
let in_args = PropertySet::new();
|
||||
in_args.set_one(crate::host::PROP_EFFECT_INSTANCE, self.effect_instance_prop());
|
||||
in_args.set_one(crate::host::PROP_KEY_SYM, Value::Int(key_sym));
|
||||
in_args.set_one(crate::host::PROP_KEY_STRING, Value::String(cs(key_string)));
|
||||
in_args.set_one(crate::host::PROP_TIME, Value::Double(time));
|
||||
in_args.define(
|
||||
crate::host::PROP_RENDER_SCALE,
|
||||
vec![Value::Double(1.0), Value::Double(1.0)],
|
||||
);
|
||||
in_args
|
||||
}
|
||||
|
||||
/// key_down(ofxInteract.h:410)。`key_sym` 为 ofxKeySyms.h 关键码;
|
||||
/// `key_string` 为 UTF-8 字符(无 UTF8 编码的键为空串)。
|
||||
pub fn key_down(&self, key_sym: i32, key_string: &str, time: f64) -> i32 {
|
||||
let in_args = self.key_in_args(key_sym, key_string, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_KEY_DOWN, &in_args, &out)
|
||||
}
|
||||
|
||||
/// key_up(ofxInteract.h:443)。
|
||||
pub fn key_up(&self, key_sym: i32, key_string: &str, time: f64) -> i32 {
|
||||
let in_args = self.key_in_args(key_sym, key_string, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_KEY_UP, &in_args, &out)
|
||||
}
|
||||
|
||||
/// idle(宿主空闲泵;任务契约的 kOfxInteractActionIdle,属本宿主
|
||||
/// 扩展)。插件未实现时返回 kOfxStatReplyDefault。
|
||||
pub fn idle(&self) -> i32 {
|
||||
let empty = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_IDLE, &empty, &empty)
|
||||
}
|
||||
|
||||
/// gain_focus(ofxInteract.h:501):pen/key 事件的前置
|
||||
/// (ofxInteract.h 各 action 的 \pre 要求 interact 已获焦点)。
|
||||
pub fn gain_focus(&self, time: f64) -> i32 {
|
||||
let in_args = self.pen_in_args((0.0, 0.0), 0.0, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_GAIN_FOCUS, &in_args, &out)
|
||||
}
|
||||
|
||||
/// lose_focus(ofxInteract.h:526)。
|
||||
pub fn lose_focus(&self, time: f64) -> i32 {
|
||||
let in_args = self.pen_in_args((0.0, 0.0), 0.0, time);
|
||||
let out = PropertySet::new();
|
||||
self.call(crate::host::ACTION_INTERACT_LOSE_FOCUS, &in_args, &out)
|
||||
}
|
||||
|
||||
/// destroy(kOfxActionDestroyInstance == kOfxActionDestroyInstanceInteract,
|
||||
/// ofxInteract.h:227)。幂等:只通知一次。返回状态忽略(析构不可
|
||||
/// 回滚,ofxInteract.h "what is returned is moot")。
|
||||
pub fn destroy(&self) {
|
||||
if self
|
||||
.destroyed
|
||||
.swap(true, Ordering::Relaxed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let empty = PropertySet::new();
|
||||
self.call(crate::host::ACTION_DESTROY_INSTANCE, &empty, &empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// interact 实例属性表(ofxInteract.h 的 PropertiesInteract 子集)。
|
||||
fn init_interact_props(props: &PropertySet) {
|
||||
props.define(
|
||||
crate::host::PROP_INTERACT_PIXEL_SCALE,
|
||||
vec![Value::Double(1.0), Value::Double(1.0)],
|
||||
);
|
||||
props.define(
|
||||
crate::host::PROP_INTERACT_VIEWPORT_SIZE,
|
||||
vec![Value::Double(0.0), Value::Double(0.0)],
|
||||
);
|
||||
props.set_one(
|
||||
crate::host::PROP_INTERACT_BACKGROUND_IMAGE,
|
||||
Value::Pointer(std::ptr::null_mut()),
|
||||
);
|
||||
props.define(
|
||||
crate::host::PROP_INTERACT_SUGGESTED_COLOUR,
|
||||
vec![Value::Double(1.0), Value::Double(1.0), Value::Double(1.0)],
|
||||
);
|
||||
props.define(crate::host::PROP_INTERACT_SLAVE_TO_PARAM, vec![]);
|
||||
props.define(
|
||||
crate::host::PROP_INTERACT_BACKGROUND_COLOUR,
|
||||
vec![Value::Double(0.0), Value::Double(0.0), Value::Double(0.0)],
|
||||
);
|
||||
// 帧缓冲位深:离屏 FBO 是 RGBA32F → 每分量 32 bit,含 alpha。
|
||||
props.set_one(crate::host::PROP_INTERACT_BIT_DEPTH, Value::Int(32));
|
||||
props.set_one(crate::host::PROP_INTERACT_HAS_ALPHA, Value::Int(1));
|
||||
}
|
||||
|
||||
/// 从属性集读 Double×2(缺失/类型不符 → None)。
|
||||
fn read_double2(props: &PropertySet, name: &str) -> Option<(f64, f64)> {
|
||||
let x = match props.get(name, 0)? {
|
||||
Value::Double(d) => d,
|
||||
_ => return None,
|
||||
};
|
||||
let y = match props.get(name, 1)? {
|
||||
Value::Double(d) => d,
|
||||
_ => return None,
|
||||
};
|
||||
Some((x, y))
|
||||
}
|
||||
|
||||
// ---- OfxInteractSuiteV1(ofxInteract.h:534-544)------------------------
|
||||
|
||||
/// interact 句柄解析(tag INTERACT)。
|
||||
fn resolve_interact(handle: *mut c_void) -> Result<&'static Interact, c_int> {
|
||||
if handle.is_null() {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
unsafe {
|
||||
if tag::kind(handle) != tag::INTERACT {
|
||||
return Err(status::ERR_BAD_HANDLE);
|
||||
}
|
||||
// Interact 的 props 在偏移 0:剥标签后的地址即 &Interact。
|
||||
Ok(&*(tag::strip(handle) as *const Interact))
|
||||
}
|
||||
}
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_or_else(
|
||||
|_| status::FAILED,
|
||||
|r| r.map_or_else(|c| c, |()| status::OK),
|
||||
)
|
||||
}
|
||||
|
||||
/// interactSwapBuffers:插件请求宿主交换 GL 缓冲(宿主 UI 视图中)。
|
||||
/// 本宿主无 UI 视图,把请求记入 `swap_requested`(app 侧轮询后清零)并
|
||||
/// 返回 OK——宿主服务真实生效(请求被记录、可被 UI 消费)。
|
||||
unsafe extern "C" fn interact_swap_buffers(handle: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
let i = resolve_interact(handle)?;
|
||||
i.swap_requested.store(true, Ordering::Relaxed);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// interactRedraw:插件请求宿主重绘 interact 视图。同上记入
|
||||
/// `redraw_requested`(app 侧轮询)。
|
||||
unsafe extern "C" fn interact_redraw(handle: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
let i = resolve_interact(handle)?;
|
||||
i.redraw_requested.store(true, Ordering::Relaxed);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// interactGetPropertySet:返回 interact 的属性集句柄(tagged INTERACT)。
|
||||
unsafe extern "C" fn interact_get_property_set(
|
||||
handle: *mut c_void,
|
||||
property: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
caught(|| {
|
||||
if property.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
let i = resolve_interact(handle)?;
|
||||
unsafe { *property = tag::make(&i.props as *const PropertySet, tag::INTERACT) };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// 函数表布局(与 SDK `OfxInteractSuiteV1` 逐字段一致;ofxInteract.h:534)。
|
||||
#[repr(C)]
|
||||
pub struct InteractSuiteV1 {
|
||||
/// interactSwapBuffers。
|
||||
pub interact_swap_buffers: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
/// interactRedraw。
|
||||
pub interact_redraw: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
/// interactGetPropertySet。
|
||||
pub interact_get_property_set: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> c_int,
|
||||
}
|
||||
|
||||
/// 函数表实例。
|
||||
pub fn suite_v1() -> &'static InteractSuiteV1 {
|
||||
static SUITE: std::sync::OnceLock<InteractSuiteV1> = std::sync::OnceLock::new();
|
||||
SUITE.get_or_init(|| InteractSuiteV1 {
|
||||
interact_swap_buffers,
|
||||
interact_redraw,
|
||||
interact_get_property_set,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::ffi::{c_char, CStr};
|
||||
use std::sync::Mutex;
|
||||
|
||||
fn cs(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
/// interact 属性表:PixelScale/ViewportSize/SuggestedColour/
|
||||
/// SlaveToParam/BitDepth/HasAlpha 等按头文件预置,经 property suite
|
||||
/// 可读写。
|
||||
#[test]
|
||||
fn interact_props_initialized() {
|
||||
let props = PropertySet::new();
|
||||
init_interact_props(&props);
|
||||
// Value 无 PartialEq(指针/CString 成员),用 matches! 断言。
|
||||
assert!(
|
||||
matches!(
|
||||
props.get(crate::host::PROP_INTERACT_PIXEL_SCALE, 0),
|
||||
Some(Value::Double(1.0))
|
||||
),
|
||||
"PixelScale[0] 应为 1.0"
|
||||
);
|
||||
assert_eq!(props.dimension(crate::host::PROP_INTERACT_PIXEL_SCALE), 2);
|
||||
assert!(
|
||||
matches!(
|
||||
props.get(crate::host::PROP_INTERACT_SUGGESTED_COLOUR, 2),
|
||||
Some(Value::Double(1.0))
|
||||
),
|
||||
"SuggestedColour[2] 应为 1.0"
|
||||
);
|
||||
assert_eq!(props.dimension(crate::host::PROP_INTERACT_SLAVE_TO_PARAM), 0);
|
||||
assert!(
|
||||
matches!(
|
||||
props.get(crate::host::PROP_INTERACT_BIT_DEPTH, 0),
|
||||
Some(Value::Int(32))
|
||||
),
|
||||
"BitDepth 应为 32"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
props.get(crate::host::PROP_INTERACT_HAS_ALPHA, 0),
|
||||
Some(Value::Int(1))
|
||||
),
|
||||
"HasAlpha 应为 1"
|
||||
);
|
||||
}
|
||||
|
||||
/// read_double2 帮助函数。
|
||||
#[test]
|
||||
fn read_double2_helper() {
|
||||
let props = PropertySet::new();
|
||||
props.define("P", vec![Value::Double(2.0), Value::Double(3.0)]);
|
||||
assert_eq!(read_double2(&props, "P"), Some((2.0, 3.0)));
|
||||
assert_eq!(read_double2(&props, "Q"), None);
|
||||
}
|
||||
|
||||
/// 空指针/错标签 → ErrBadHandle(interact suite 的句柄校验)。
|
||||
#[test]
|
||||
fn interact_suite_handle_validation() {
|
||||
unsafe {
|
||||
assert_eq!(interact_swap_buffers(std::ptr::null_mut()), status::ERR_BAD_HANDLE);
|
||||
assert_eq!(interact_redraw(std::ptr::null_mut()), status::ERR_BAD_HANDLE);
|
||||
let mut out: *mut c_void = std::ptr::null_mut();
|
||||
assert_eq!(
|
||||
interact_get_property_set(std::ptr::null_mut(), &mut out),
|
||||
status::ERR_BAD_HANDLE
|
||||
);
|
||||
// 错标签:裸 PropertySet(标签 0)当 interact 用。
|
||||
let props = PropertySet::new();
|
||||
let raw = &props as *const PropertySet as *mut c_void;
|
||||
assert_eq!(interact_swap_buffers(raw), status::ERR_BAD_HANDLE);
|
||||
}
|
||||
}
|
||||
|
||||
/// interact suite 服务端:swap/redraw 请求被记录(app 侧轮询面),
|
||||
/// getPropertySet 返回 interact 属性集句柄。
|
||||
#[test]
|
||||
fn interact_suite_requests_are_recorded() {
|
||||
// 用假插件构造 Interact(不进插件入口——只测 suite 服务端)。
|
||||
let plugin = crate::host::Plugin {
|
||||
identifier: "fake".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::from("fake"),
|
||||
contexts: Vec::new(),
|
||||
descriptor: crate::descriptor::EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: dummy_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
};
|
||||
let interact = Interact::new(Arc::new(plugin), dummy_entry, std::ptr::null_mut());
|
||||
let handle = interact.handle();
|
||||
|
||||
unsafe {
|
||||
assert_eq!(interact_swap_buffers(handle), status::OK);
|
||||
assert_eq!(interact_redraw(handle), status::OK);
|
||||
}
|
||||
assert!(interact.swap_requested.load(Ordering::Relaxed));
|
||||
assert!(interact.redraw_requested.load(Ordering::Relaxed));
|
||||
|
||||
let mut props_handle: *mut c_void = std::ptr::null_mut();
|
||||
unsafe {
|
||||
assert_eq!(interact_get_property_set(handle, &mut props_handle), status::OK);
|
||||
}
|
||||
assert_eq!(tag::kind(props_handle), tag::INTERACT);
|
||||
assert_eq!(
|
||||
tag::strip(props_handle) as usize,
|
||||
&interact.props as *const PropertySet as usize
|
||||
);
|
||||
|
||||
// property suite 可直读 interact 属性集(句柄约定打通)。
|
||||
let addr = tag::strip(props_handle);
|
||||
let set: &PropertySet = unsafe { &*addr };
|
||||
assert!(
|
||||
matches!(
|
||||
set.get(crate::host::PROP_INTERACT_BIT_DEPTH, 0),
|
||||
Some(Value::Int(32))
|
||||
),
|
||||
"经 interact 句柄的 property suite 应读到 BitDepth=32"
|
||||
);
|
||||
|
||||
// 摘除引用计数:Arc 释放(interact 已无引用)。
|
||||
drop(interact);
|
||||
}
|
||||
|
||||
/// 假入口:所有 action 返回 ReplyDefault(不触达真实插件)。
|
||||
unsafe extern "C" fn dummy_entry(
|
||||
_action: *const c_char,
|
||||
_handle: *const c_void,
|
||||
_in: *mut c_void,
|
||||
_out: *mut c_void,
|
||||
) -> c_int {
|
||||
status::REPLY_DEFAULT
|
||||
}
|
||||
|
||||
/// call 面透传:entry 收到 action 与 tagged handle(假插件记录)。
|
||||
#[test]
|
||||
fn interact_call_dispatches_to_entry() {
|
||||
static CALLED_ACTION: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
unsafe extern "C" fn recording_entry(
|
||||
action: *const c_char,
|
||||
handle: *const c_void,
|
||||
_in: *mut c_void,
|
||||
_out: *mut c_void,
|
||||
) -> c_int {
|
||||
CALLED_ACTION
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(unsafe { CStr::from_ptr(action) }.to_string_lossy().into_owned());
|
||||
// handle 必须是 tagged INTERACT。
|
||||
assert_eq!(tag::kind(handle as *mut c_void), tag::INTERACT);
|
||||
status::OK
|
||||
}
|
||||
let plugin = crate::host::Plugin {
|
||||
identifier: "fake".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::from("fake"),
|
||||
contexts: Vec::new(),
|
||||
descriptor: crate::descriptor::EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: recording_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
};
|
||||
let interact = Interact::new(Arc::new(plugin), recording_entry, std::ptr::null_mut());
|
||||
|
||||
assert_eq!(interact.describe(), status::OK);
|
||||
assert_eq!(interact.create_instance(), status::OK);
|
||||
assert_eq!(interact.pen_motion((10.0, 20.0), true, 5.0), status::OK);
|
||||
assert_eq!(interact.pen_down((10.0, 20.0), 5.0), status::OK);
|
||||
assert_eq!(interact.pen_up((10.0, 20.0), 5.0), status::OK);
|
||||
assert_eq!(interact.key_down(crate::host::KEY_A, "a", 5.0), status::OK);
|
||||
assert_eq!(interact.key_up(crate::host::KEY_A, "a", 5.0), status::OK);
|
||||
assert_eq!(interact.idle(), status::OK);
|
||||
interact.destroy();
|
||||
|
||||
let calls = CALLED_ACTION.lock().unwrap().clone();
|
||||
let want = [
|
||||
crate::host::ACTION_DESCRIBE,
|
||||
crate::host::ACTION_CREATE_INSTANCE,
|
||||
crate::host::ACTION_INTERACT_PEN_MOTION,
|
||||
crate::host::ACTION_INTERACT_PEN_DOWN,
|
||||
crate::host::ACTION_INTERACT_PEN_UP,
|
||||
crate::host::ACTION_INTERACT_KEY_DOWN,
|
||||
crate::host::ACTION_INTERACT_KEY_UP,
|
||||
crate::host::ACTION_INTERACT_IDLE,
|
||||
crate::host::ACTION_DESTROY_INSTANCE,
|
||||
];
|
||||
assert_eq!(calls, want, "interact action 序列应按序透传到入口");
|
||||
}
|
||||
|
||||
/// pen 事件的真实参数:inArgs 的 canonical 位置/视口位置/压力按
|
||||
/// pixelScale 换算(假插件读取记录)。
|
||||
#[test]
|
||||
fn pen_in_args_canonical_conversion() {
|
||||
// 记录 pen_motion 的 inArgs 关键属性。
|
||||
static CAPTURED: Mutex<Option<(f64, f64, f64)>> = Mutex::new(None);
|
||||
unsafe extern "C" fn capture_entry(
|
||||
_action: *const c_char,
|
||||
_handle: *const c_void,
|
||||
in_args: *mut c_void,
|
||||
_out: *mut c_void,
|
||||
) -> c_int {
|
||||
if in_args.is_null() {
|
||||
return status::OK;
|
||||
}
|
||||
let set = unsafe { &*(in_args as *const PropertySet) };
|
||||
// 视口位置(Int×2)与 canonical 位置(Double×2)与压力。
|
||||
let vx = match set.get(crate::host::PROP_INTERACT_PEN_VIEWPORT_POSITION, 0) {
|
||||
Some(Value::Int(i)) => i,
|
||||
_ => 0,
|
||||
};
|
||||
let px = match set.get(crate::host::PROP_INTERACT_PEN_POSITION, 0) {
|
||||
Some(Value::Double(d)) => d,
|
||||
_ => 0.0,
|
||||
};
|
||||
let pressure = match set.get(crate::host::PROP_INTERACT_PEN_PRESSURE, 0) {
|
||||
Some(Value::Double(d)) => d,
|
||||
_ => 0.0,
|
||||
};
|
||||
*CAPTURED.lock().unwrap() = Some((vx as f64, px, pressure));
|
||||
status::OK
|
||||
}
|
||||
let plugin = crate::host::Plugin {
|
||||
identifier: "fake".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::from("fake"),
|
||||
contexts: Vec::new(),
|
||||
descriptor: crate::descriptor::EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: capture_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
};
|
||||
let interact = Interact::new(Arc::new(plugin), capture_entry, std::ptr::null_mut());
|
||||
// 设 pixelScale=2 → canonical = viewport/2。
|
||||
interact.props.define(
|
||||
crate::host::PROP_INTERACT_PIXEL_SCALE,
|
||||
vec![Value::Double(2.0), Value::Double(2.0)],
|
||||
);
|
||||
|
||||
assert_eq!(interact.pen_motion((10.0, 20.0), true, 0.0), status::OK);
|
||||
let (vx, px, pressure) = CAPTURED.lock().unwrap().take().unwrap();
|
||||
assert_eq!(vx, 10.0);
|
||||
assert_eq!(px, 5.0);
|
||||
assert_eq!(pressure, 1.0);
|
||||
|
||||
// pen_up 压力 0。
|
||||
assert_eq!(interact.pen_up((10.0, 20.0), 0.0), status::OK);
|
||||
let (_, _, pressure) = CAPTURED.lock().unwrap().take().unwrap();
|
||||
assert_eq!(pressure, 0.0);
|
||||
}
|
||||
|
||||
/// key 事件真实参数:keySym/keyString 进 inArgs。
|
||||
#[test]
|
||||
fn key_in_args_carry_sym_and_string() {
|
||||
static CAPTURED: Mutex<Option<(i32, String)>> = Mutex::new(None);
|
||||
unsafe extern "C" fn capture_entry(
|
||||
_action: *const c_char,
|
||||
_handle: *const c_void,
|
||||
in_args: *mut c_void,
|
||||
_out: *mut c_void,
|
||||
) -> c_int {
|
||||
let set = unsafe { &*(in_args as *const PropertySet) };
|
||||
let sym = match set.get(crate::host::PROP_KEY_SYM, 0) {
|
||||
Some(Value::Int(i)) => i,
|
||||
_ => 0,
|
||||
};
|
||||
let s = match set.get(crate::host::PROP_KEY_STRING, 0) {
|
||||
Some(Value::String(c)) => c.to_string_lossy().into_owned(),
|
||||
_ => String::new(),
|
||||
};
|
||||
*CAPTURED.lock().unwrap() = Some((sym, s));
|
||||
status::OK
|
||||
}
|
||||
let plugin = crate::host::Plugin {
|
||||
identifier: "fake".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::from("fake"),
|
||||
contexts: Vec::new(),
|
||||
descriptor: crate::descriptor::EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: capture_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
};
|
||||
let interact = Interact::new(Arc::new(plugin), capture_entry, std::ptr::null_mut());
|
||||
assert_eq!(interact.key_down(crate::host::KEY_RETURN, "", 1.0), status::OK);
|
||||
let (sym, s) = CAPTURED.lock().unwrap().take().unwrap();
|
||||
assert_eq!(sym, crate::host::KEY_RETURN);
|
||||
assert_eq!(s, "");
|
||||
assert_eq!(interact.key_up(crate::host::KEY_A, "a", 1.0), status::OK);
|
||||
let (sym, s) = CAPTURED.lock().unwrap().take().unwrap();
|
||||
assert_eq!(sym, crate::host::KEY_A);
|
||||
assert_eq!(s, "a");
|
||||
}
|
||||
|
||||
/// destroy 幂等:只发一次。
|
||||
#[test]
|
||||
fn destroy_is_idempotent() {
|
||||
static COUNT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
|
||||
unsafe extern "C" fn counting_entry(
|
||||
action: *const c_char,
|
||||
_handle: *const c_void,
|
||||
_in: *mut c_void,
|
||||
_out: *mut c_void,
|
||||
) -> c_int {
|
||||
let a = unsafe { CStr::from_ptr(action) }.to_string_lossy();
|
||||
if a == crate::host::ACTION_DESTROY_INSTANCE {
|
||||
COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
status::OK
|
||||
}
|
||||
let plugin = crate::host::Plugin {
|
||||
identifier: "fake".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::from("fake"),
|
||||
contexts: Vec::new(),
|
||||
descriptor: crate::descriptor::EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: counting_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
};
|
||||
let interact = Interact::new(Arc::new(plugin), counting_entry, std::ptr::null_mut());
|
||||
interact.destroy();
|
||||
interact.destroy();
|
||||
assert_eq!(COUNT.load(std::sync::atomic::Ordering::Relaxed), 1);
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,10 @@
|
||||
//!
|
||||
//! 参照:HS: ofxhImageEffect.cpp:2776(fetchSuite 分发表与版本协商)。
|
||||
|
||||
pub mod draw;
|
||||
pub mod gl_render;
|
||||
pub mod image_effect;
|
||||
pub mod interact;
|
||||
pub mod memory;
|
||||
pub mod message;
|
||||
pub mod multithread;
|
||||
@@ -103,6 +105,9 @@ pub mod tag {
|
||||
pub const CLIP: usize = 5;
|
||||
/// [`crate::image::Image`](clipGetImage 的产物)。
|
||||
pub const IMAGE: usize = 6;
|
||||
/// [`crate::suites::interact::Interact`](interact 实例 handle;
|
||||
/// 实例期 effect/param-set 之外的独立交互对象)。
|
||||
pub const INTERACT: usize = 7;
|
||||
|
||||
/// 打标签(宿主创建对象句柄用;公开供宿主/测试构造句柄)。
|
||||
pub fn make(props: *const crate::property::PropertySet, t: usize) -> *mut std::ffi::c_void {
|
||||
@@ -183,6 +188,11 @@ pub struct GlCtx {
|
||||
/// 全链路 F32,由 render_gl 按插件 kOfxOpenGLPropPixelDepth 协商
|
||||
/// 后填入——纹理句柄的 kOfxImageEffectPropPixelDepth 以它为准)。
|
||||
pub gl_pixel_depth: &'static str,
|
||||
/// 已附着的**真实 GL 输出纹理名**(GL 模式下宿主为输出帧建的
|
||||
/// GL 纹理 + FBO 颜色附件;clipLoadTexture(Output) 的
|
||||
/// OpenGLTextureIndex 以它为准——ofxGPURender.h 要求宿主把输出
|
||||
/// 纹理绑定为当前颜色缓冲)。None = 无真实 GL 名(CPU 回退)。
|
||||
pub output_gl_texture: Option<i32>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -213,6 +223,7 @@ pub(crate) const OFX_API_VERSION: i32 = 105;
|
||||
/// OfxMultiThreadSuite v1。
|
||||
/// 第 2 期追加:OfxImageEffectOpenGLRenderSuite v1(GL 路径);
|
||||
/// ofxColour 无 suite 表(纯属性 + GetOutputColourspace action)。
|
||||
/// 第 3 期追加:OfxInteractSuite v1、OfxDrawSuite v1(interact 宿主)。
|
||||
pub fn fetch_suite(name: &str, version: i32) -> Option<*const std::ffi::c_void> {
|
||||
let suite: *const std::ffi::c_void = match (name, version) {
|
||||
("OfxPropertySuite", 1) => ptr(property::suite_v1()),
|
||||
@@ -226,6 +237,8 @@ pub fn fetch_suite(name: &str, version: i32) -> Option<*const std::ffi::c_void>
|
||||
("OfxTimeLineSuite", 1) => ptr(timeline::suite_v1()),
|
||||
("OfxMultiThreadSuite", 1) => ptr(multithread::suite_v1()),
|
||||
("OfxImageEffectOpenGLRenderSuite", 1) => ptr(gl_render::suite_v1()),
|
||||
("OfxInteractSuite", 1) => ptr(interact::suite_v1()),
|
||||
("OfxDrawSuite", 1) => ptr(draw::suite_v1()),
|
||||
_ => return None,
|
||||
};
|
||||
Some(suite)
|
||||
@@ -240,7 +253,7 @@ fn ptr<T>(p: &'static T) -> *const std::ffi::c_void {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 八张 suite 的分发表:版本精确匹配、未知版本/名字 → None。
|
||||
/// 十张 suite 的分发表:版本精确匹配、未知版本/名字 → None。
|
||||
#[test]
|
||||
fn fetch_suite_dispatch() {
|
||||
assert!(fetch_suite("OfxPropertySuite", 1).is_some());
|
||||
@@ -254,10 +267,14 @@ mod tests {
|
||||
assert!(fetch_suite("OfxTimeLineSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxMultiThreadSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxImageEffectOpenGLRenderSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxInteractSuite", 1).is_some());
|
||||
assert!(fetch_suite("OfxDrawSuite", 1).is_some());
|
||||
|
||||
assert!(fetch_suite("OfxPropertySuite", 2).is_none());
|
||||
assert!(fetch_suite("OfxMessageSuite", 3).is_none());
|
||||
assert!(fetch_suite("OfxImageEffectOpenGLRenderSuite", 2).is_none());
|
||||
assert!(fetch_suite("OfxInteractSuite", 2).is_none());
|
||||
assert!(fetch_suite("OfxDrawSuite", 2).is_none());
|
||||
assert!(fetch_suite("OfxBogusSuite", 1).is_none());
|
||||
assert!(fetch_suite("", 1).is_none());
|
||||
}
|
||||
@@ -276,6 +293,7 @@ mod tests {
|
||||
tag::PARAM_INSTANCE,
|
||||
tag::CLIP,
|
||||
tag::IMAGE,
|
||||
tag::INTERACT,
|
||||
] {
|
||||
let h = tag::make(props, t);
|
||||
assert_eq!(tag::kind(h), t);
|
||||
@@ -340,6 +358,7 @@ mod tests {
|
||||
renderer,
|
||||
output_texture: tex.clone(),
|
||||
gl_pixel_depth: "OfxBitDepthFloat",
|
||||
output_gl_texture: Some(42),
|
||||
}));
|
||||
let got = gl_ctx().unwrap();
|
||||
assert_eq!(
|
||||
@@ -348,6 +367,7 @@ mod tests {
|
||||
);
|
||||
assert!(got.output_texture.is_dummy());
|
||||
assert_eq!(got.gl_pixel_depth, "OfxBitDepthFloat");
|
||||
assert_eq!(got.output_gl_texture, Some(42));
|
||||
set_gl_ctx(None);
|
||||
assert!(gl_ctx().is_none());
|
||||
}
|
||||
|
||||
@@ -816,6 +816,7 @@ mod tests {
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
interact: std::sync::Mutex::new(None),
|
||||
});
|
||||
(inst.clone(), instance_handle(&inst))
|
||||
}
|
||||
|
||||
@@ -114,7 +114,14 @@ unsafe extern "C" fn progress_update_v1(_handle: *mut c_void, progress: c_double
|
||||
}
|
||||
|
||||
unsafe extern "C" fn progress_end_v1(_handle: *mut c_void) -> c_int {
|
||||
caught(|| status::OK)
|
||||
caught(|| {
|
||||
CURRENT.with(|c| {
|
||||
if let Some(r) = c.borrow().as_ref() {
|
||||
r.end_ui();
|
||||
}
|
||||
});
|
||||
status::OK
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn progress_start_v2(
|
||||
|
||||
Reference in New Issue
Block a user