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
+42
View File
@@ -37,13 +37,32 @@ use std::sync::atomic::{AtomicU32, Ordering};
pub const OAKNODE_ABI_VERSION: u32 = 1;
/// Heap box behind a handle's `ctx`.
///
/// `repr(C)`: the field order is the stable prefix layout
/// [`RefBoxHeader`] relies on for type discrimination across boxes of
/// different payloads sharing one handle channel (the render seam
/// downcasts plugin-job boxes vs texture boxes).
#[repr(C)]
pub struct RefBox<T: ?Sized> {
/// Atomic reference count.
pub refs: AtomicU32,
/// Type identity of the boxed value (stamped by [`make_owned`];
/// [`get_checked`] compares it before reading [`RefBox::value`]).
pub type_id: std::any::TypeId,
/// Boxed value.
pub value: T,
}
/// The fixed-size prefix of every [`RefBox`] (layout-stable across
/// payload types because [`RefBox`] is `repr(C)`).
#[repr(C)]
struct RefBoxHeader {
/// Mirror of [`RefBox::refs`] (present for the layout prefix;
/// never read here).
_refs: AtomicU32,
type_id: std::any::TypeId,
}
/// The shared ABI value-handle type (single-lib unification, see
/// `docs/zh/plans/riir/single-lib.md`): one canonical
/// `{ctx, addref, release, abi_version}` type in `oakcore-rs`, re-exported
@@ -79,6 +98,7 @@ unsafe extern "C" fn refbox_release_owned<T: Any + Send>(ctx: *mut std::ffi::c_v
pub fn make_owned<T: Any + Send>(value: T) -> CHandle {
let rb = Box::into_raw(Box::new(RefBox {
refs: AtomicU32::new(1),
type_id: std::any::TypeId::of::<T>(),
value,
}));
CHandle {
@@ -98,6 +118,7 @@ pub fn make_owned_with<T: Any + Send>(
) -> CHandle {
let rb = Box::into_raw(Box::new(RefBox {
refs: AtomicU32::new(1),
type_id: std::any::TypeId::of::<T>(),
value,
}));
CHandle {
@@ -118,3 +139,24 @@ pub unsafe fn get<T: Any>(h: &CHandle) -> Option<&T> {
}
unsafe { Some(&(*(h.ctx as *const RefBox<T>)).value) }
}
/// Typed view with type discrimination: `None` when the handle is
/// empty **or** boxes a different payload type (the render seam probes
/// texture-channel handles for plugin-job payloads this way without
/// knowing the producer).
///
/// # Safety
/// `h` must be either empty or a live handle created by
/// [`make_owned`]/[`make_owned_with`] for the duration of the call.
pub unsafe fn get_checked<T: Any>(h: &CHandle) -> Option<&T> {
if h.ctx.is_null() {
return None;
}
// SAFETY: every live box starts with the repr(C) RefBox prefix;
// the caller guarantees the handle is alive.
let header = unsafe { &*(h.ctx as *const RefBoxHeader) };
if header.type_id != std::any::TypeId::of::<T>() {
return None;
}
unsafe { Some(&(*(h.ctx as *const RefBox<T>)).value) }
}