feat(oakplugin): GL render bridge, color picker, push-button action, worker progress, Interact host

- gl_bridge: macOS CGL offscreen context (process-wide singleton,
  serialized GlGuard), real GL output textures/FBOs, glReadPixels
  readback with vertical flip and format conversion; use_opengl now
  really engages for OpenGLRenderSupported plugins (verified with real
  GL rendering: C smoke 11/11, unit tests, GL e2e).
- OfxColor: color params get a swatch button plus a real picker popup
  (RGBA sliders, live preview, hex input, undoable commit) replacing
  the four spinboxes.
- Push buttons route kOfxActionInstanceChanged (UserEdited) per the
  OFX contract; test plugin asserts the callback.
- Worker-side plugin progress flows to the main-process progress
  dialog over the NDJSON control channel, with cancel propagation.
- OFX Interact host: NewInteract/Describe lifecycle, Draw/Pen/Key/Idle
  action surface with proper in-args, DrawSuite v1 host implementation
  sharing the gl_bridge context; interact test plugin verifies the
  event stream and real GL drawing.
This commit is contained in:
2026-08-19 22:14:54 +08:00
parent 29696479b5
commit b4ceaa9cab
28 changed files with 5481 additions and 161 deletions
+795
View File
@@ -0,0 +1,795 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! 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)
}
}
+90 -39
View File
@@ -27,29 +27,36 @@
//! RenderScale/PixelAspectRatio/Bounds/RegionOfDefinition/RowBytes/
//! Field/UniqueIdentifier);宿主侧存强引用([`LIVE_TEXTURES`]),
//! clipFreeTexture 摘除即释放(对应 HS 的 get/release 配对)。
//! **OpenGLTextureIndex 是真实 GL 纹理名**(GL 模式,见
//! [`crate::gl_bridge`] 方案 B):Output clip 是宿主为输出帧建的
//! GL 纹理(经 GlCtx 注入,render 驱动负责建/FBO 挂载/回读/删除);
//! 输入 clip 是本 suite 在 GL 渲染期经桥上传图像得到的 GL 纹理
//! (clipFreeTexture 删除)。0 = CPU 回退(无 GL 名,插件按规范回退)。
//! - Output clip:`format` 忽略,宿主返回已附着的输出纹理句柄——
//! 绑定渲染目标的动作(ofxGPURender.h "the host must bind the
//! resulting texture as the current color buffer")由调用方约定:
//! GL render 驱动的 C ABI 契约要求 oakrender 在进入 render_job 前
//! 已把输出纹理附着为渲染器输出目标(等价 C++ 的
//! `PluginRenderer::attach_output_texture`)。clipFreeTexture 对
//! Output 只释放句柄、不删纹理(宿主还要读它)。
//! - 输入纹理经 [`crate::render`] 在渲染器上创建(CPU 帧 →
//! GL 上传)。纹理格式:全链路 F32 约束下,像素深度按 clip 协商
//! 结果(恒 F32);`format` 参数(kOfxImageEffectGLFormat*)若
//! 请求的分量与协商分量不符,Phase 2 不做转换 → Failed(规范要求
//! "host ensures it gives the requested format"——宁可显式失败也
//! 不静默给错格式)。ofxGPURender.h 注明"宿主无需按 Clip
//! Preferences 把图像重映射到插件请求的位深",插件以纹理句柄的
//! PixelDepth/Components 为准。
//! GL render 驱动在进入 render_gl 前已把输出 GL 纹理挂进 FBO 并
//! 绑定(等价 C++ 的 `PluginRenderer::attach_output_texture`)。
//! clipFreeTexture 对 Output 只释放句柄、不删纹理(宿主还要回读它,
//! 回读后由 render 驱动删除)。
//! - 输入纹理在 GL 模式经 [`crate::gl_bridge::create_input_texture`]
//! 上传(CPU 帧 → GL 纹理);CPU 模式(无 GL 上下文)维持
//! [`crate::render`] 的 CPU 纹理。纹理格式:全链路 F32 约束下,
//! 像素深度按 clip 协商结果(恒 F32);`format` 参数
//! (kOfxImageEffectGLFormat*)若请求的分量与协商分量不符,Phase 2
//! 不做转换 → Failed(规范要求 "host ensures it gives the requested
//! format"——宁可显式失败也不静默给错格式)。ofxGPURender.h 注明
//! "宿主无需按 Clip Preferences 把图像重映射到插件请求的位深",
//! 插件以纹理句柄的 PixelDepth/Components 为准。
//! - `flushResources`:宿主在 render 之间不缓存 GPU 资源(纹理随
//! clipFreeTexture 立即释放)→ 无可释放 → kOfxStatReplyDefault
//! (规范:"nothing the host could do")。
//! - GL 上下文规则(ofxGPURender.h "OpenGL Current Context"):宿主
//! 只在 Render/BeginSequenceRender/EndSequenceRender/Attach/Detach
//! 期间要求上下文 current;本实现的约定是调用方(oakrender
//! PluginJob 路径)在调用前置好上下文,本 suite 经
//! [`crate::suites::gl_ctx`] TLS 取渲染器句柄。
//! 期间要求上下文 current;本实现的约定是 render 驱动一次 acquire
//! 整个 GL render action([`crate::gl_bridge::acquire`]),本 suite
//! 回调期间上下文恒 current,经 [`crate::suites::gl_ctx`] TLS 取
//! 渲染器句柄。
use std::collections::HashMap;
use std::ffi::{c_char, c_double, c_int, c_void, CStr, CString};
@@ -164,37 +171,64 @@ mod tests {
// ---- 存活纹理表 -----------------------------------------------------------
/// 存活 GL 纹理表:clipLoadTexture 产出(props 地址 → 属性集 +
/// 纹理值 + 是否输出 clip);clipFreeTexture 摘除即释放——对应
/// HS 的 get/release 配对(HS: ofxhImageEffect.cpp:2336-2351)。
/// 纹理是 oakrender 值(drop 自动释放后端 token;原
/// `texture_free` 调用面随值模型删除)。
/// 纹理值 + 是否输出 clip + 真实 GL 纹理名[可选]);clipFreeTexture
/// 摘除即释放——对应 HS 的 get/release 配对
/// (HS: ofxhImageEffect.cpp:2336-2351)。纹理是 oakrender 值(drop
/// 自动释放后端 token;原 `texture_free` 调用面随值模型删除)。
///
/// `gl_texture`:GL 模式下输入 clip 经 [`crate::gl_bridge`] 建的真实
/// GL 纹理名(输出 clip 恒 None——宿主自建并负责生命周期,见
/// [`crate::gl_bridge`] 模块文档);摘除时经
/// [`delete_gl_texture_if_gl`] 删除。
///
/// 属性集必须**装箱**(Box 稳定堆地址):纹理句柄指向它,函数返回后
/// 必须仍存活;栈上临时变量会悬垂(phase-2 实现初版的 bug)。
static LIVE_TEXTURES: std::sync::LazyLock<
Mutex<HashMap<usize, (Box<PropertySet>, crate::render::Texture, bool)>>,
Mutex<HashMap<usize, (Box<PropertySet>, crate::render::Texture, bool, Option<i32>)>>,
> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
/// 登记纹理(clipLoadTexture 内部;`props` 装箱后取地址为句柄基址)。
/// `gl_texture`:真实 GL 纹理名(输入 clip GL 模式;无则 None)。
fn register(
props: Box<PropertySet>,
texture: crate::render::Texture,
is_output: bool,
gl_texture: Option<i32>,
) -> usize {
let addr = &*props as *const PropertySet as usize;
LIVE_TEXTURES
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(addr, (props, texture, is_output));
.insert(addr, (props, texture, is_output, gl_texture));
addr
}
/// 删除 GL 纹理(上下文 current 时;否则留给下次 GL 渲染前的兜底——
/// 正常路径 clipFreeTexture/purge 都在 GL render 期,上下文必 current)。
fn delete_gl_texture_if_gl(name: i32) {
if crate::suites::gl_ctx().is_some() {
crate::gl_bridge::delete_gl_texture(name);
}
}
/// 释放全部残留输入纹理(GL render action 返回后的安全网:规范要求
/// 插件在 action 返回前 clipFreeTexture 全部句柄;遗漏的输入纹理在
/// 此释放,输出纹理保留——宿主还要读它)。
/// 此释放,输出纹理保留——宿主还要读它)。GL 输入纹理同步删除
/// (上下文 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(&params, image.pixels(), image.row_bytes() as i32)
.map_err(|_| status::ERR_MEMORY)?;
let props = make_texture_props(
&tex,
w,
h,
components,
@@ -404,8 +448,9 @@ unsafe extern "C" fn clip_load_texture(
clip_par(c),
scale,
image.row_bytes() as i32,
gl_tex.unwrap_or(0),
);
let addr = register(Box::new(props), tex, false);
let addr = register(Box::new(props), tex, false, gl_tex);
unsafe { *out = tag::make(addr as *const PropertySet, tag::PROPERTY_SET) };
Ok(())
})
@@ -440,9 +485,9 @@ fn clip_par(c: &ClipInstance) -> f64 {
.unwrap_or(1.0)
}
/// clipFreeTexture:释放纹理(输入 clip 删除纹理值;Output 只释放
/// 句柄不删纹理——宿主还要读它)。纹理是值:摘除表条目即 drop
/// (原 `texture_free` 调用面随值模型删除)。
/// clipFreeTexture:释放纹理(输入 clip 删除纹理值并释放真实 GL
/// 纹理;Output 只释放句柄不删纹理——宿主还要读它)。纹理是值:
/// 摘除表条目即 drop(原 `texture_free` 调用面随值模型删除)。
unsafe extern "C" fn clip_free_texture(texture_handle: *mut c_void) -> c_int {
caught(|| {
if texture_handle.is_null() {
@@ -454,7 +499,13 @@ unsafe extern "C" fn clip_free_texture(texture_handle: *mut c_void) -> c_int {
live.remove(&addr)
};
match entry {
Some((_props, _texture, _is_output)) => Ok(()),
Some((_props, _texture, _is_output, gl_tex)) => {
// 输入 clip 的真实 GL 纹理随释放删除(上下文 current)。
if let Some(gl) = gl_tex {
delete_gl_texture_if_gl(gl);
}
Ok(())
}
None => Err(status::ERR_BAD_HANDLE),
}
})
@@ -579,6 +579,7 @@ mod tests {
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
interact: std::sync::Mutex::new(None),
});
let ih = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
unsafe {
+814
View File
@@ -0,0 +1,814 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! OfxInteractSuiteV1 + [`Interact`] 宿主对象。
//!
//! Interact 是插件自定义 UI 的宿主侧对象(主进程 UI 事件宿主):与
//! worker 进程里的渲染实例并存(OFX 允许同一插件多实例),经
//! [`crate::instance::Instance::new_interact`] 创建、绑定到效果实例。
//!
//! ## 生命周期
//!
//! ```text
//! new_interact → kOfxActionNewInteract(创建;插件拒绝 → None)
//! describe → 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);
}
}
+21 -1
View File
@@ -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());
}
+1
View File
@@ -816,6 +816,7 @@ mod tests {
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
interact: std::sync::Mutex::new(None),
});
(inst.clone(), instance_handle(&inst))
}
+8 -1
View File
@@ -114,7 +114,14 @@ unsafe extern "C" fn progress_update_v1(_handle: *mut c_void, progress: c_double
}
unsafe extern "C" fn progress_end_v1(_handle: *mut c_void) -> c_int {
caught(|| status::OK)
caught(|| {
CURRENT.with(|c| {
if let Some(r) = c.borrow().as_ref() {
r.end_ui();
}
});
status::OK
})
}
unsafe extern "C" fn progress_start_v2(