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
+30 -16
View File
@@ -126,34 +126,48 @@ impl Traverser {
return Err(Error::State);
}
let entry = graph.get(node).ok_or(Error::NotFound)?;
// Build this node's input row from its upstream outputs. The
// C++ picks the last value of the matching type per input;
// the Rust model keys rows by input id.
// the Rust model keys rows by input id. Inputs declared as
// texture take the upstream texture directly (the scalar
// chain below would otherwise hand a plugin node's tagged
// param passthrough to a downstream clip input).
let mut row: NodeValueRow = std::collections::BTreeMap::new();
for (from, input_id, element) in graph.input_connections(node) {
let _ = element;
if let Some(from_table) = tables.get(&from) {
let value = from_table
.get(ValueType::Float)
.or_else(|| from_table.get(ValueType::Int))
.or_else(|| from_table.get(ValueType::Color))
.or_else(|| from_table.get(ValueType::Vec2))
.or_else(|| from_table.get(ValueType::Vec3))
.or_else(|| from_table.get(ValueType::Vec4))
.or_else(|| from_table.get(ValueType::Boolean))
.or_else(|| from_table.get(ValueType::Rational))
.or_else(|| from_table.get(ValueType::Text))
.or_else(|| from_table.get(ValueType::Combo))
.or_else(|| from_table.get(ValueType::StrCombo))
.cloned()
.unwrap_or(NodeValue::None);
let value = if entry.core.input_data_type(&input_id)
== Some(ValueType::Texture)
{
from_table
.get(ValueType::Texture)
.cloned()
.unwrap_or(NodeValue::None)
} else {
from_table
.get(ValueType::Float)
.or_else(|| from_table.get(ValueType::Int))
.or_else(|| from_table.get(ValueType::Color))
.or_else(|| from_table.get(ValueType::Vec2))
.or_else(|| from_table.get(ValueType::Vec3))
.or_else(|| from_table.get(ValueType::Vec4))
.or_else(|| from_table.get(ValueType::Boolean))
.or_else(|| from_table.get(ValueType::Rational))
.or_else(|| from_table.get(ValueType::Text))
.or_else(|| from_table.get(ValueType::Combo))
.or_else(|| from_table.get(ValueType::StrCombo))
.or_else(|| from_table.get(ValueType::Texture))
.cloned()
.unwrap_or(NodeValue::None)
};
row.insert(input_id, value);
}
}
// Evaluate the node's behavior into its output table.
let mut table = NodeValueTable::default();
let entry = graph.get(node).ok_or(Error::NotFound)?;
// The behavior writes outputs; the default no-op leaves the
// table empty (C++ `Node::value` default).
entry