feat(oakplugin): wire OpenFX plugins into the node graph and renderer

- oaknode: dynamic node factory registration, PluginNode value model
  pushing PluginJobPayload, traverser texture passthrough for texture
  inputs, type-stamped RefBox::get_checked.
- oakrender: PluginExecutor dependency-inversion slot; eval resolves
  and executes plugin jobs, purple frame on failure.
- oakplugin: node_factory with full OFX param -> node input
  translation (15 types, color semantics heuristic, combo ordering,
  secret/ui_group/ui_page, clip inputs), plugin instance registry,
  render executor + duplicator installation, progress reporter and
  active-viewer provider injection points, U8/U16/F16 input
  conversion with NaN scrubbing, in-place output frame writeback fix.
- gl_bridge.rs documents the wgpu<->GL interop spike: Metal-first on
  macOS rules out wgpu-hal GL interop; offscreen GL context deferred.

End-to-end tests cover registration, param translation, CPU render
pixel assertions, identity passthrough and NaN fallback.
This commit is contained in:
2026-08-18 17:15:13 +08:00
parent 9daa266189
commit 2db1615453
21 changed files with 2595 additions and 189 deletions
+35 -7
View File
@@ -84,10 +84,29 @@ pub struct ProgressSuiteV2 {
pub end: unsafe extern "C" fn(*mut c_void) -> c_int,
}
/// progressStart:括号起点,OK(label/message 留作未来 UI 展示,
/// 第 1 期不建模)。
unsafe extern "C" fn progress_start_v1(_handle: *mut c_void, _label: *const c_char) -> c_int {
caught(|| status::OK)
/// C 字符串解码(空指针/非法 UTF-8 → 空串)。
unsafe fn decode<'a>(p: *const c_char) -> &'a str {
if p.is_null() {
return "";
}
unsafe { std::ffi::CStr::from_ptr(p) }
.to_str()
.unwrap_or("")
}
/// progressStart:括号起点;携 label(v2 另携 message)经
/// [`crate::progress`] 工厂现造 UI 报告器(无工厂/已有报告器时
/// no-op),状态恒 OK。
unsafe extern "C" fn progress_start_v1(_handle: *mut c_void, label: *const c_char) -> c_int {
caught(|| {
let label = unsafe { decode(label) }.to_string();
CURRENT.with(|c| {
if let Some(r) = c.borrow().as_ref() {
r.install_ui(&label, "");
}
});
status::OK
})
}
unsafe extern "C" fn progress_update_v1(_handle: *mut c_void, progress: c_double) -> c_int {
@@ -100,10 +119,19 @@ unsafe extern "C" fn progress_end_v1(_handle: *mut c_void) -> c_int {
unsafe extern "C" fn progress_start_v2(
_handle: *mut c_void,
_label: *const c_char,
_message: *const c_char,
label: *const c_char,
message: *const c_char,
) -> c_int {
caught(|| status::OK)
caught(|| {
let label = unsafe { decode(label) }.to_string();
let message = unsafe { decode(message) }.to_string();
CURRENT.with(|c| {
if let Some(r) = c.borrow().as_ref() {
r.install_ui(&label, &message);
}
});
status::OK
})
}
unsafe extern "C" fn progress_update_v2(_handle: *mut c_void, progress: c_double) -> c_int {
+53 -6
View File
@@ -18,14 +18,54 @@
//! ([`crate::suites::RenderCtx`],frame range 来自 clip 桥)。
//! 参照 HS: ofxhImageEffect.cpp gTimelineSuite。
//!
//! 无渲染上下文(渲染外调用)→ 时间 0 / 时间域 (0,0) 的 headless
//! 默认;gotoTime 第 1 期无时间线驱动 → OK no-op(渲染时间由驱动
//! 固定,见 [`crate::suites::set_render_ctx`])。
//! 无渲染上下文(渲染外调用)→ 回退 app 注入的活动 viewer 时间源
//! ([`set_active_viewer_provider`],阶段 6a 注入点);也未注入则
//! 时间 0 / 时间域 (0,0) 的 headless 默认。gotoTime 第 1 期无时间线
//! 驱动 → OK no-op(渲染时间由驱动固定,见
//! [`crate::suites::set_render_ctx`])。
use std::ffi::{c_double, c_int, c_void};
use std::sync::{Arc, Mutex, OnceLock};
use crate::suites::{render_ctx, status};
// ---------------------------------------------------------------------------
// app 注入点:活动 viewer 时间源
// ---------------------------------------------------------------------------
/// 活动 viewer 的时间信息(timeline suite 渲染外回退源的最小集)。
#[derive(Clone, Copy, Debug)]
pub struct ViewerTimeInfo {
/// viewer 当前时间(秒)。
pub time: f64,
/// 时间域下界(秒)。
pub range_min: f64,
/// 时间域上界(秒)。
pub range_max: f64,
}
/// 活动 viewer 提供器:返回 None 表示当前无活动 viewer。
pub type ActiveViewerProvider = Arc<dyn Fn() -> Option<ViewerTimeInfo> + Send + Sync>;
static ACTIVE_VIEWER: OnceLock<Mutex<Option<ActiveViewerProvider>>> = OnceLock::new();
fn viewer_slot() -> &'static Mutex<Option<ActiveViewerProvider>> {
ACTIVE_VIEWER.get_or_init(|| Mutex::new(None))
}
/// 注册/清除活动 viewer 提供器(app 接线点;覆盖式)。
pub fn set_active_viewer_provider(provider: Option<ActiveViewerProvider>) {
*viewer_slot().lock().unwrap_or_else(|e| e.into_inner()) = provider;
}
fn active_viewer() -> Option<ViewerTimeInfo> {
let provider = viewer_slot()
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()?;
provider()
}
/// 函数表布局(OfxTimeLineSuiteV1)。
#[repr(C)]
pub struct TimeLineSuiteV1 {
@@ -54,8 +94,11 @@ unsafe extern "C" fn timeline_get_time(handle: *mut c_void, time: *mut c_double)
if time.is_null() {
return status::ERR_VALUE;
}
// 渲染上下文缺省 → 0(headless 默认)。
*time = render_ctx().map_or(0.0, |c| c.time);
// 渲染上下文 → 活动 viewer → 0(headless 默认)。
*time = render_ctx()
.map(|c| c.time)
.or_else(|| active_viewer().map(|v| v.time))
.unwrap_or(0.0);
status::OK
})
}
@@ -77,7 +120,11 @@ unsafe extern "C" fn timeline_get_time_bounds(
if min.is_null() || max.is_null() {
return status::ERR_VALUE;
}
let r = render_ctx().map_or((0.0, 0.0), |c| (c.range.min, c.range.max));
// 渲染上下文 → 活动 viewer → (0,0)(headless 默认)。
let r = render_ctx()
.map(|c| (c.range.min, c.range.max))
.or_else(|| active_viewer().map(|v| (v.range_min, v.range_max)))
.unwrap_or((0.0, 0.0));
*min = r.0;
*max = r.1;
status::OK