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
+82 -5
View File
@@ -17,13 +17,57 @@
//! 进度上报(Progress suite 的宿主侧)。
//!
//! 对应 C++ 的 `PluginProgressReporter`。进度/取消经 facade 注册的
//! 回调出 crateM9 的 facade 回调模式,不设全局状态)。
//! 取消是粘滞的:一旦回调答 false,本报告器的 [`is_cancelled`]
//! image effect suite 的 abort 查询)持续为真。
//! 回调出 crateM9 的 facade 回调模式)。取消是粘滞的:一旦回调
//! 答 false,本报告器的 [`is_cancelled`]image effect suite 的
//! abort 查询)持续为真。
//!
//! ## app 注入点(阶段 6a
//!
//! facade C 回调之外,app 可经 [`set_reporter_factory`] 注册一个
//! 工厂:渲染未装 C 回调时,Progress suite 的 progressStart 携
//! (label, message) 从工厂现造一个 [`UiProgressReporter`](如进度
//! 对话框),progressUpdate 转发给它,返回 false 即取消。对应 C++
//! `PluginProgressDialogReporter` 的创建路径。
use std::ffi::c_int;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
/// 进度 UI 报告器(app 实现:对话框、状态栏等)。`update` 返回
/// false 表示用户请求取消(映射 kOfxStatReplyNo)。
pub trait UiProgressReporter: Send {
/// 上报进度(0.0..=1.0);false = 取消。
fn update(&mut self, progress: f64) -> bool;
}
/// 报告器工厂:progressStart 携 (label, message) 调用,现造一个
/// [`UiProgressReporter`]。
pub type ReporterFactory =
Arc<dyn Fn(&str, &str) -> Box<dyn UiProgressReporter> + Send + Sync>;
static REPORTER_FACTORY: OnceLock<Mutex<Option<ReporterFactory>>> = OnceLock::new();
fn factory_slot() -> &'static Mutex<Option<ReporterFactory>> {
REPORTER_FACTORY.get_or_init(|| Mutex::new(None))
}
/// 注册/清除进度 UI 工厂(app 接线点;覆盖式)。
pub fn set_reporter_factory(factory: Option<ReporterFactory>) {
*factory_slot().lock().unwrap_or_else(|e| e.into_inner()) = factory;
}
pub(crate) fn reporter_factory() -> Option<ReporterFactory> {
factory_slot()
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
/// 是否已注册 UI 工厂(render 路径据此决定是否装静默报告器)。
pub fn has_reporter_factory() -> bool {
reporter_factory().is_some()
}
/// 进度回调:签名与 `include/plugin/instance.h` 的
/// `oakplugin_progress_fn` 逐字一致——`(progress, userdata)`
@@ -36,16 +80,21 @@ pub type ProgressFn = unsafe extern "C" fn(progress: f64, userdata: *mut std::ff
pub struct ProgressReporter {
callback: Option<ProgressFn>,
userdata: usize,
/// UI 报告器(progressStart 经 [`reporter_factory`] 现造;
/// C 回调优先,无回调时才用)。
ui: Mutex<Option<Box<dyn UiProgressReporter>>>,
/// 取消标志(粘滞;update 返回 false 时置位)。
cancelled: AtomicBool,
}
impl ProgressReporter {
/// 无回调(渲染静默进行)。
/// 无回调(渲染静默进行progressStart 仍可能经工厂装上 UI
/// 报告器)。
pub fn silent() -> Self {
Self {
callback: None,
userdata: 0,
ui: Mutex::new(None),
cancelled: AtomicBool::new(false),
}
}
@@ -59,10 +108,26 @@ impl ProgressReporter {
callback: Some(callback),
// usize 存(裸指针破坏 Send 推导;值语义不变)。
userdata: userdata as usize,
ui: Mutex::new(None),
cancelled: AtomicBool::new(false),
}
}
/// progressStart 钩子:无 C 回调且工厂已注册时,现造 UI 报告器
/// (已装过则不重复造——progressStart 可嵌套括号)。
pub(crate) fn install_ui(&self, label: &str, message: &str) {
if self.callback.is_some() {
return;
}
let mut slot = self.ui.lock().unwrap_or_else(|e| e.into_inner());
if slot.is_some() {
return;
}
if let Some(factory) = reporter_factory() {
*slot = Some(factory(label, message));
}
}
/// 报告进度;返回 false 表示应取消(映射
/// kOfxStatReplyNo/action 失败由调用点决定)。
pub fn update(&self, progress: f64) -> bool {
@@ -75,7 +140,19 @@ impl ProgressReporter {
}
!abort
}
None => true,
None => {
let mut slot = self.ui.lock().unwrap_or_else(|e| e.into_inner());
match slot.as_mut() {
Some(ui) => {
let keep_going = ui.update(progress);
if !keep_going {
self.cancelled.store(true, Ordering::Relaxed);
}
keep_going
}
None => true,
}
}
}
}