- 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.
214 lines
6.7 KiB
Rust
214 lines
6.7 KiB
Rust
// 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/>.
|
|
|
|
//! The evaluation engine — the C++ `NodeTraverser` restructured.
|
|
//!
|
|
//! Key change from C++: no inheritance. C++ `RenderProcessor :
|
|
//! NodeTraverser` overrode virtuals to plug rendering in; here the
|
|
//! traverser is a free engine and oakrender supplies [`RenderHooks`].
|
|
//! The graph is walked iteratively in topological order with an
|
|
//! explicit value stack (the C++ recursive path could blow the stack
|
|
//! on deep graphs — same order, no recursion).
|
|
//! `// CPP-PARITY: src/node/src/traverser.cpp`.
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
use oakcore_rs::{Rational, TimeRange};
|
|
|
|
use crate::graph::Graph;
|
|
use crate::id::NodeId;
|
|
use crate::value::{NodeValue, NodeValueRow, NodeValueTable, ValueType};
|
|
|
|
/// Backend hooks supplied by the consumer (oakrender). Default no-ops
|
|
/// give the C++ "offline evaluation" behavior.
|
|
pub trait RenderHooks {
|
|
/// Whether cached textures may be used (C++ `use_cache()`).
|
|
fn use_cache(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
/// Convert a finished value row into a backend job/texture
|
|
/// (C++ `resolve_jobs` / `process_*_job` family).
|
|
fn resolve(&mut self, node: NodeId, row: &NodeValueRow, table: &mut NodeValueTable) {
|
|
let _ = (node, row, table);
|
|
}
|
|
|
|
/// Cancel-check polled between nodes (C++ `IsCancelled`).
|
|
fn is_cancelled(&self) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Evaluation request.
|
|
pub struct EvalRequest {
|
|
/// Root node to evaluate.
|
|
pub root: NodeId,
|
|
/// Time.
|
|
pub time: Rational,
|
|
/// Optional range (for audio pulls).
|
|
pub range: Option<TimeRange>,
|
|
}
|
|
|
|
impl EvalRequest {
|
|
/// New request.
|
|
pub fn new(root: NodeId, time: Rational) -> EvalRequest {
|
|
EvalRequest {
|
|
root,
|
|
time,
|
|
range: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The traversal engine.
|
|
pub struct Traverser {
|
|
/// Value stack / per-node row cache for this pass.
|
|
stack: Vec<(NodeId, NodeValueTable)>,
|
|
/// Nodes touched by the last [`Traverser::invalidate_downstream`]
|
|
/// walk (observable for tests; the C++ fan-out has no return value).
|
|
last_invalidation: Vec<NodeId>,
|
|
}
|
|
|
|
impl Traverser {
|
|
/// New empty engine (reusable across evaluations).
|
|
pub fn new() -> Self {
|
|
Traverser {
|
|
stack: Vec::new(),
|
|
last_invalidation: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Nodes marked by the last invalidation walk.
|
|
pub fn last_invalidation(&self) -> &[NodeId] {
|
|
&self.last_invalidation
|
|
}
|
|
|
|
/// Evaluate `request` against `graph`, calling `hooks` at the
|
|
/// backend seams. Returns the root's output table.
|
|
///
|
|
/// Errors: `State` on cancellation, `Failed` on node evaluation
|
|
/// errors (C++ returned empty tables; we surface the error —
|
|
/// `// CPP-PARITY: traverser.cpp` behavior notes inline).
|
|
pub fn evaluate(
|
|
&mut self,
|
|
graph: &Graph,
|
|
request: &EvalRequest,
|
|
hooks: &mut dyn RenderHooks,
|
|
) -> crate::error::Result<NodeValueTable> {
|
|
use crate::error::Error;
|
|
if !graph.is_valid(request.root) {
|
|
return Err(Error::NotFound);
|
|
}
|
|
|
|
self.stack.clear();
|
|
let order = graph.topological_order();
|
|
|
|
// Per-node output tables for this pass (memoization: a shared
|
|
// upstream evaluates once — `// CPP-PARITY: traverser.cpp`
|
|
// process_node_children).
|
|
let mut tables: HashMap<NodeId, NodeValueTable> = HashMap::new();
|
|
|
|
for node in order {
|
|
if hooks.is_cancelled() {
|
|
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. 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 = 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();
|
|
// The behavior writes outputs; the default no-op leaves the
|
|
// table empty (C++ `Node::value` default).
|
|
entry
|
|
.behavior
|
|
.value(&entry.core, &row, request.time, &mut table);
|
|
hooks.resolve(node, &row, &mut table);
|
|
|
|
tables.insert(node, table);
|
|
}
|
|
|
|
Ok(tables.remove(&request.root).unwrap_or_default())
|
|
}
|
|
|
|
/// Invalidate walk: mark downstream caches dirty after an input
|
|
/// change (C++ `invalidate_cache` fan-out, signal-free). Records the
|
|
/// walked set in [`Traverser::last_invalidation`].
|
|
pub fn invalidate_downstream(&mut self, graph: &Graph, from: NodeId, range: TimeRange) {
|
|
let _ = range;
|
|
self.last_invalidation.clear();
|
|
let mut seen: HashSet<NodeId> = HashSet::new();
|
|
let mut queue: Vec<NodeId> = vec![from];
|
|
while let Some(n) = queue.pop() {
|
|
if !seen.insert(n) {
|
|
continue;
|
|
}
|
|
self.last_invalidation.push(n);
|
|
queue.extend(graph.downstream(n));
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Traverser {
|
|
fn default() -> Self {
|
|
Traverser::new()
|
|
}
|
|
}
|
|
|
|
/// A value database: per-node input rows over a time range (C++
|
|
/// `NodeValueDatabase`), exposed by the traverser ffi family.
|
|
pub struct ValueDatabase {
|
|
/// Rows keyed by node input id.
|
|
pub rows: Vec<(String, Vec<(ValueType, NodeValue)>)>,
|
|
}
|