CI / Build & test (Windows) (push) Failing after 7s
All crates take the oak-* kebab-case naming (oak-audio, oak-codec, oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin, oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: -> oak_core::, ...) across all 226 referencing files. The GUI application moves from the workspace root into crates/oak-app/: src/, build.rs (paths fixed for the new location) and tests/ travel with it, the root Cargo.toml becomes workspace-only ([workspace] + workspace.package + profiles), and the app package inherits the workspace version. The screenshots example becomes a standalone crate examples/simple_player/ with its own Cargo.toml. Every crate now inherits the single workspace version (version.workspace = true), and the workflows' crate paths and the build docs follow the renames. Validated with a clean cargo check --workspace.
683 lines
24 KiB
Rust
683 lines
24 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/>.
|
|
|
|
//! OCIO LUT file node (C++ `src/node/src/color/ociolut/ociolut.{h,cpp}`,
|
|
//! `olive::OCIOLutNode`).
|
|
//!
|
|
//! Note: OpenColorIO itself is never linked here; it is reached through
|
|
//! the color manager (`crate::colormanager`) and the oakrender bridge
|
|
//! (oakrender, opaque handles), like the C++ node's
|
|
//! `oakrender_color_processor_create_lut` / `oakrender_lut_*` calls.
|
|
|
|
use std::sync::Mutex;
|
|
|
|
use crate::factory::NodeMeta;
|
|
use crate::node::{Category, NodeBehavior, NodeCore};
|
|
|
|
use crate::nodes::ociobase::OcioBase;
|
|
|
|
/// LUT file input id (C++ `k_file_input`). Type: file; default: empty
|
|
/// string; flags: not-keyframable, not-connectable; properties:
|
|
/// `filter = "LUT Files (*.<ext> ...);;All Files (*)"` (extensions from
|
|
/// `oakrender_lut_supported_extension_at`, `*.*` fallback),
|
|
/// `placeholder = "Select a LUT file"`, `lut_library = true`.
|
|
pub const FILE_INPUT: &str = "lut_file_in";
|
|
|
|
/// Direction combo input id (C++ `k_direction_input`). Type: combo;
|
|
/// default `0` (forward); flags: not-keyframable, not-connectable.
|
|
/// Combo strings (set in `retranslate`): "Forward", "Inverse".
|
|
pub const DIRECTION_INPUT: &str = "lut_dir_in";
|
|
|
|
/// Deferred processor-generation state (C++ `mutable` members
|
|
/// `gen_mutex_`, `processor_dirty_`, `last_path_`, `last_direction_`,
|
|
/// `last_processor_`, `last_error_`). Grouped behind one mutex: the C++
|
|
/// mutable-in-const-method pattern maps to interior mutability here,
|
|
/// and the C++ code already serializes all of these under `gen_mutex_`.
|
|
struct ProcessorState {
|
|
/// Regeneration pending (C++ `processor_dirty_`, starts `true`).
|
|
dirty: bool,
|
|
/// Path of the LUT the cached processor was built from (C++
|
|
/// `last_path_`).
|
|
last_path: String,
|
|
/// Direction the cached processor was built for (C++
|
|
/// `last_direction_`, starts `-1`).
|
|
last_direction: i64,
|
|
/// Cached processor for change detection (C++ `last_processor_`);
|
|
/// released with the node.
|
|
last_processor: Option<crate::handle::CHandle>,
|
|
/// Human-readable reason no LUT processor is active (C++
|
|
/// `last_error_`); empty when a valid processor is in use or no LUT
|
|
/// file has been selected yet.
|
|
last_error: String,
|
|
}
|
|
|
|
impl Default for ProcessorState {
|
|
/// Fresh state: dirty, empty path, no cached processor or error.
|
|
fn default() -> Self {
|
|
ProcessorState {
|
|
dirty: true,
|
|
last_path: String::new(),
|
|
last_direction: -1,
|
|
last_processor: None,
|
|
last_error: String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// The cached processor handle wraps a refcounted C object that is only
|
|
// dereferenced from the render path; see `OcioBase` for the rationale.
|
|
unsafe impl Send for ProcessorState {}
|
|
|
|
/// OCIO LUT node. Applies a LUT file through OpenColorIO.
|
|
pub struct OCIOLutNode {
|
|
/// Shared OCIO base state (C++ base class `OCIOBaseNode`).
|
|
base: OcioBase,
|
|
/// Processor generation cache/lock (C++ `gen_mutex_` + the mutable
|
|
/// `last_*` members).
|
|
state: Mutex<ProcessorState>,
|
|
}
|
|
|
|
impl OCIOLutNode {
|
|
/// Human-readable description of why no LUT processor is active
|
|
/// (C++ `last_error()`); empty when a valid LUT processor is in use
|
|
/// or no LUT file has been selected yet.
|
|
pub fn last_error(&self) -> String {
|
|
self.state.lock().unwrap().last_error.clone()
|
|
}
|
|
|
|
/// Record a new error string (C++ `set_last_error()`); no-op when
|
|
/// unchanged. The Qt version surfaced the error on the main-window
|
|
/// status bar; here it is only recorded and read back via
|
|
/// [`Self::last_error`].
|
|
fn set_last_error(&self, error: &str) {
|
|
let mut state = self.state.lock().unwrap();
|
|
if state.last_error == error {
|
|
return;
|
|
}
|
|
state.last_error = error.to_string();
|
|
}
|
|
|
|
/// Direction combo value with legacy fallback (C++ file-static
|
|
/// `read_direction_input()`): integer combo value, or — for old
|
|
/// serializers that stored the combo as a string — case-insensitive
|
|
/// "forward"/"0" -> 0 and "inverse"/"1" -> 1, defaulting to 0 with a
|
|
/// stderr warning for anything else.
|
|
fn read_direction_input(core: &NodeCore) -> i64 {
|
|
match &core.standard_value(DIRECTION_INPUT, -1) {
|
|
crate::value::NodeValue::Combo(i) => *i,
|
|
crate::value::NodeValue::Int(i) => *i,
|
|
crate::value::NodeValue::Text(s) => {
|
|
let lower = s.to_lowercase();
|
|
if lower == "forward" || lower == "0" {
|
|
0
|
|
} else if lower == "inverse" || lower == "1" {
|
|
1
|
|
} else {
|
|
eprintln!("OCIOLutNode: unexpected direction value {}", s);
|
|
0
|
|
}
|
|
}
|
|
other => {
|
|
eprintln!(
|
|
"OCIOLutNode: unexpected direction value {}",
|
|
other.to_double()
|
|
);
|
|
0
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Whether this is the main GUI process (C++ file-static
|
|
/// `is_main_process()`): true when a render manager exists (the
|
|
/// render worker never creates one).
|
|
fn is_main_process() -> bool {
|
|
// The C++ probes `oakrender_manager_available()`, which the
|
|
// oakrender bridge does not expose. Without a render manager (the
|
|
// oakrender-free test environment) the probe would report the
|
|
// worker process, so the worker branch — defer processor
|
|
// generation to value() time — always applies here.
|
|
// `// CPP-PARITY: ociolut.cpp` is_main_process.
|
|
false
|
|
}
|
|
|
|
/// (Re)generate the processor now (C++ `generate_processor()`):
|
|
/// ensures the processor is current, then — in the main process
|
|
/// only — invalidates the texture-input cache and cancels background
|
|
/// video cache tasks so in-flight renders cannot write stale frames.
|
|
fn generate_processor(&mut self, core: &mut NodeCore) {
|
|
self.ensure_processor(core);
|
|
// The C++ main-process half (`invalidate_all(k_texture_input)`
|
|
// + `oakrender_cancel_video_tasks(0)`) refreshes the viewer after
|
|
// a processor change; it needs the render manager seam that the
|
|
// Rust model does not expose, and `is_main_process()` is false
|
|
// here, so it is skipped (`// CPP-PARITY: ociolut.cpp`
|
|
// generate_processor).
|
|
}
|
|
|
|
/// Ensure the processor matches the current inputs (C++
|
|
/// `ensure_processor()`): under the generation mutex, returns early
|
|
/// when not dirty, a cached processor exists, and path/direction are
|
|
/// unchanged; otherwise rebuilds from the inputs.
|
|
fn ensure_processor(&self, core: &NodeCore) {
|
|
{
|
|
let state = self.state.lock().unwrap();
|
|
if !state.dirty
|
|
&& state.last_processor.as_ref().is_some_and(|p| !p.is_null())
|
|
&& Self::file_path(core) == state.last_path
|
|
&& Self::read_direction_input(core) == state.last_direction
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
self.create_processor_from_inputs(core);
|
|
}
|
|
|
|
/// Rebuild the processor from the LUT path and direction (C++
|
|
/// `create_processor_from_inputs()`): no manager, empty path,
|
|
/// non-regular file, or unsupported extension -> clear both
|
|
/// processors, reset the cache markers, record the error, and return
|
|
/// false; unchanged path+direction with a live cached processor ->
|
|
/// clear the dirty flag and return false (reuse); otherwise create
|
|
/// the LUT processor via `oakrender_color_processor_create_lut`
|
|
/// (direction 0 = forward), update the cache markers and both
|
|
/// processor slots, and return true.
|
|
fn create_processor_from_inputs(&self, core: &NodeCore) -> bool {
|
|
let _ = core;
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
// C++ branch 1: no color manager. The Rust model reaches the
|
|
// manager through the oakrender bridge (absent here), so this
|
|
// branch is always taken: reset the cache markers and report
|
|
// false. The C++ additionally clears the standard processor and
|
|
// frees `last_processor_` — the Rust base processor is only
|
|
// reachable through `&mut self` and can never hold a processor
|
|
// without the render bridge, so those clears are no-ops here.
|
|
// The empty-path/non-regular-file/unsupported-extension error
|
|
// branches are unreachable without a manager and are not
|
|
// representable. `// CPP-PARITY: ociolut.cpp`
|
|
// create_processor_from_inputs.
|
|
state.last_processor = None;
|
|
state.last_path.clear();
|
|
state.last_direction = -1;
|
|
state.dirty = false;
|
|
false
|
|
}
|
|
|
|
/// OCIO config change hook (C++ `config_changed()` override):
|
|
/// regenerates immediately in the main process, or just marks the
|
|
/// processor dirty in the render worker (deferred to render time).
|
|
fn config_changed(&mut self, core: &mut NodeCore) {
|
|
let _ = core;
|
|
// C++: main process -> generate_processor(); render worker ->
|
|
// mark dirty. `is_main_process()` is false here, so the worker
|
|
// branch applies and the processor is rebuilt at value() time.
|
|
self.state.lock().unwrap().dirty = true;
|
|
}
|
|
|
|
/// The standard value of [`FILE_INPUT`] as a path string.
|
|
fn file_path(core: &NodeCore) -> String {
|
|
match &core.standard_value(FILE_INPUT, -1) {
|
|
crate::value::NodeValue::Text(s) => s.clone(),
|
|
_ => String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl NodeBehavior for OCIOLutNode {
|
|
/// Human-readable name (C++ `name()`).
|
|
fn name(&self) -> &str {
|
|
"OCIO LUT"
|
|
}
|
|
|
|
/// Stable type id (C++ `id()`).
|
|
fn type_id(&self) -> &str {
|
|
"org.olivevideoeditor.Olive.ociolut"
|
|
}
|
|
|
|
/// Categories (C++ `category()`).
|
|
fn categories(&self) -> &[Category] {
|
|
&[Category::Color]
|
|
}
|
|
|
|
/// Description (C++ `description()`).
|
|
fn description(&self) -> &str {
|
|
"Applies a LUT file through OpenColorIO."
|
|
}
|
|
|
|
/// Localized input names (C++ `retranslate()`): `tex_in` -> "Input",
|
|
/// `lut_file_in` -> "LUT File", `lut_dir_in` -> "Direction" (also
|
|
/// sets the direction combo strings "Forward"/"Inverse").
|
|
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
|
match id {
|
|
crate::nodes::ociobase::TEXTURE_INPUT => "Input",
|
|
FILE_INPUT => "LUT File",
|
|
DIRECTION_INPUT => "Direction",
|
|
_ => id,
|
|
}
|
|
}
|
|
|
|
/// Combo input option labels (C++ `retranslate()` /
|
|
/// `set_combo_box_strings`): `lut_dir_in` -> "Forward", "Inverse".
|
|
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
|
|
match id {
|
|
DIRECTION_INPUT => vec!["Forward", "Inverse"],
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Input value changed (C++ `InputValueChangedEvent`): for
|
|
/// `lut_file_in` or `lut_dir_in`, regenerates the processor
|
|
/// immediately in the main process; in the render worker (where
|
|
/// generation can be slow and the main process may be blocked
|
|
/// waiting on LoadGraph) only marks the processor dirty so it is
|
|
/// rebuilt at render time.
|
|
fn input_value_changed(&mut self, core: &mut NodeCore, input: &str, element: i32) {
|
|
let _ = element;
|
|
if input == FILE_INPUT || input == DIRECTION_INPUT {
|
|
// C++: main process -> generate_processor(); render worker ->
|
|
// mark dirty. `is_main_process()` is false here (no render
|
|
// manager), so the worker branch applies.
|
|
self.state.lock().unwrap().dirty = true;
|
|
let _ = core;
|
|
}
|
|
}
|
|
|
|
/// Evaluate outputs (C++ `value()`): first `ensure_processor()` so
|
|
/// the processor is up to date before the base class emits the color
|
|
/// transform job (essential in the render worker, where creation is
|
|
/// deferred until the first render), then delegates to
|
|
/// [`OcioBase::value`].
|
|
fn value(
|
|
&self,
|
|
core: &NodeCore,
|
|
inputs: &crate::value::NodeValueRow,
|
|
time: oak_core::Rational,
|
|
table: &mut crate::value::NodeValueTable,
|
|
) {
|
|
self.ensure_processor(core);
|
|
self.base.value(core, inputs, time, table);
|
|
}
|
|
|
|
/// Added to a graph (C++ base `AddedToGraphEvent`): captures the
|
|
/// project's color manager and runs `config_changed()` via
|
|
/// [`OcioBase::added_to_graph`].
|
|
fn added_to_graph(&mut self, core: &mut NodeCore) {
|
|
self.base.added_to_graph(core);
|
|
self.config_changed(core);
|
|
}
|
|
|
|
/// Removed from a graph (C++ base `RemovedFromGraphEvent`): clears
|
|
/// the color manager pointer via [`OcioBase::removed_from_graph`].
|
|
fn removed_from_graph(&mut self, core: &mut NodeCore) {
|
|
self.base.removed_from_graph(core);
|
|
}
|
|
|
|
/// Deep copy (C++ `copy()` via `NODE_COPY_FUNCTION`; the destructor
|
|
/// additionally disconnects all signals and frees the cached
|
|
/// processor).
|
|
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
|
// The C++ copy constructor carries over the processor-cache
|
|
// members; the refcounted `last_processor_` cannot be shared
|
|
// safely by value (the destructor frees it), so a fresh
|
|
// processor state (dirty, no cache) plus a fresh empty OcioBase
|
|
// is the safe port — the processor is never populated without
|
|
// the render bridge anyway.
|
|
Some(Box::new(OCIOLutNode {
|
|
base: OcioBase::new(),
|
|
state: Mutex::new(ProcessorState::default()),
|
|
}))
|
|
}
|
|
}
|
|
|
|
/// Constructor (C++ `OCIOLutNode::OCIOLutNode()`): builds the base
|
|
/// (`tex_in` texture input, effect input, video-effect flag) and adds
|
|
/// `lut_file_in` (with the LUT filter/placeholder/lut-library
|
|
/// properties) and `lut_dir_in` with the defaults, flags and properties
|
|
/// documented on the constants; the processor state starts dirty.
|
|
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
|
let mut core = NodeCore::new();
|
|
|
|
// OCIOBaseNode base constructor.
|
|
let mut tex = crate::input::Input::new(
|
|
crate::nodes::ociobase::TEXTURE_INPUT,
|
|
crate::value::ValueType::Texture,
|
|
crate::value::NodeValue::None,
|
|
);
|
|
tex.flags |= crate::input::flags::NOT_KEYFRAMABLE;
|
|
core.add_input(tex);
|
|
core.effect_input = crate::nodes::ociobase::TEXTURE_INPUT.to_string();
|
|
core.flags |= crate::node::flags::VIDEO_EFFECT;
|
|
|
|
// The file input is a string-carried type in Rust (no file value
|
|
// type); the C++ k_file type maps to the same string marshalling.
|
|
let mut file = crate::input::Input::new(
|
|
FILE_INPUT,
|
|
crate::value::ValueType::Text,
|
|
crate::value::NodeValue::Text(String::new()),
|
|
);
|
|
file.flags |= crate::input::flags::NOT_KEYFRAMABLE | crate::input::flags::NOT_CONNECTABLE;
|
|
file.properties = vec![
|
|
// The C++ collects the supported LUT extensions from
|
|
// `oakrender_lut_supported_extension_at`; without the render
|
|
// bridge the list is empty and the `*.*` fallback applies
|
|
// (`// CPP-PARITY: ociolut.cpp` constructor).
|
|
(
|
|
"filter".to_string(),
|
|
crate::value::NodeValue::Text("LUT Files (*.*);;All Files (*)".into()),
|
|
),
|
|
(
|
|
"placeholder".to_string(),
|
|
crate::value::NodeValue::Text("Select a LUT file".into()),
|
|
),
|
|
(
|
|
"lut_library".to_string(),
|
|
crate::value::NodeValue::Boolean(true),
|
|
),
|
|
];
|
|
core.add_input(file);
|
|
|
|
let mut direction = crate::input::Input::new(
|
|
DIRECTION_INPUT,
|
|
crate::value::ValueType::Combo,
|
|
crate::value::NodeValue::Combo(0),
|
|
);
|
|
direction.flags |= crate::input::flags::NOT_KEYFRAMABLE | crate::input::flags::NOT_CONNECTABLE;
|
|
core.add_input(direction);
|
|
|
|
(
|
|
core,
|
|
Box::new(OCIOLutNode {
|
|
base: OcioBase::new(),
|
|
state: Mutex::new(ProcessorState::default()),
|
|
}),
|
|
)
|
|
}
|
|
|
|
/// Register this node type (C++ factory entry for
|
|
/// `org.olivevideoeditor.Olive.ociolut`).
|
|
pub fn register(meta: &mut Vec<NodeMeta>) {
|
|
meta.push(NodeMeta {
|
|
type_id: "org.olivevideoeditor.Olive.ociolut",
|
|
name: "OCIO LUT",
|
|
categories: &[Category::Color],
|
|
create,
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::value::{NodeValue, NodeValueTable, ValueType};
|
|
use oak_core::Rational;
|
|
|
|
fn node() -> OCIOLutNode {
|
|
OCIOLutNode {
|
|
base: OcioBase::new(),
|
|
state: Mutex::new(ProcessorState::default()),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn input_names() {
|
|
let n = node();
|
|
assert_eq!(n.input_name(crate::nodes::ociobase::TEXTURE_INPUT), "Input");
|
|
assert_eq!(n.input_name(FILE_INPUT), "LUT File");
|
|
assert_eq!(n.input_name(DIRECTION_INPUT), "Direction");
|
|
assert_eq!(n.input_name("other_in"), "other_in");
|
|
}
|
|
|
|
#[test]
|
|
fn create_wires_inputs_flags_and_properties() {
|
|
let (core, behavior) = create();
|
|
assert_eq!(behavior.type_id(), "org.olivevideoeditor.Olive.ociolut");
|
|
assert_ne!(
|
|
core.get_input(crate::nodes::ociobase::TEXTURE_INPUT)
|
|
.unwrap()
|
|
.flags & crate::input::flags::NOT_KEYFRAMABLE,
|
|
0
|
|
);
|
|
let file = core.get_input(FILE_INPUT).unwrap();
|
|
assert_eq!(file.default, NodeValue::Text(String::new()));
|
|
assert_ne!(file.flags & crate::input::flags::NOT_KEYFRAMABLE, 0);
|
|
assert_ne!(file.flags & crate::input::flags::NOT_CONNECTABLE, 0);
|
|
// `*.*` filter fallback without the render bridge.
|
|
assert!(file.properties.iter().any(|(k, v)| k == "filter"
|
|
&& *v == NodeValue::Text("LUT Files (*.*);;All Files (*)".into())));
|
|
assert!(file
|
|
.properties
|
|
.iter()
|
|
.any(|(k, v)| k == "placeholder" && *v == NodeValue::Text("Select a LUT file".into())));
|
|
assert!(file
|
|
.properties
|
|
.iter()
|
|
.any(|(k, v)| k == "lut_library" && *v == NodeValue::Boolean(true)));
|
|
let dir = core.get_input(DIRECTION_INPUT).unwrap();
|
|
assert_eq!(dir.default, NodeValue::Combo(0));
|
|
assert_ne!(dir.flags & crate::input::flags::NOT_KEYFRAMABLE, 0);
|
|
assert_ne!(dir.flags & crate::input::flags::NOT_CONNECTABLE, 0);
|
|
assert_eq!(core.effect_input, crate::nodes::ociobase::TEXTURE_INPUT);
|
|
assert_ne!(core.flags & crate::node::flags::VIDEO_EFFECT, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn read_direction_input_parses_combo_and_legacy_strings() {
|
|
let mut core = NodeCore::new();
|
|
core.add_input(crate::input::Input::new(
|
|
DIRECTION_INPUT,
|
|
crate::value::ValueType::Combo,
|
|
crate::value::NodeValue::Combo(0),
|
|
));
|
|
assert_eq!(OCIOLutNode::read_direction_input(&core), 0);
|
|
core.set_standard_value(DIRECTION_INPUT, -1, NodeValue::Combo(1));
|
|
assert_eq!(OCIOLutNode::read_direction_input(&core), 1);
|
|
// Legacy string storage (old serializers).
|
|
core.set_standard_value(DIRECTION_INPUT, -1, NodeValue::Text("forward".into()));
|
|
assert_eq!(OCIOLutNode::read_direction_input(&core), 0);
|
|
core.set_standard_value(DIRECTION_INPUT, -1, NodeValue::Text("Inverse".into()));
|
|
assert_eq!(OCIOLutNode::read_direction_input(&core), 1);
|
|
core.set_standard_value(DIRECTION_INPUT, -1, NodeValue::Text("0".into()));
|
|
assert_eq!(OCIOLutNode::read_direction_input(&core), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn last_error_roundtrip_and_set_once() {
|
|
let n = node();
|
|
assert_eq!(n.last_error(), "");
|
|
n.set_last_error("OCIO LUT: file does not exist: /nope.cube");
|
|
assert_eq!(n.last_error(), "OCIO LUT: file does not exist: /nope.cube");
|
|
// No-op when unchanged.
|
|
n.set_last_error("OCIO LUT: file does not exist: /nope.cube");
|
|
assert_eq!(n.last_error(), "OCIO LUT: file does not exist: /nope.cube");
|
|
n.set_last_error("");
|
|
assert_eq!(n.last_error(), "");
|
|
}
|
|
|
|
#[test]
|
|
fn create_processor_from_inputs_resets_markers_without_manager() {
|
|
let n = node();
|
|
let core = NodeCore::new();
|
|
{
|
|
let mut state = n.state.lock().unwrap();
|
|
state.dirty = true;
|
|
state.last_path = "/tmp/foo.cube".to_string();
|
|
state.last_direction = 1;
|
|
state.last_processor = Some(crate::handle::CHandle::null());
|
|
state.last_error = "stale error".to_string();
|
|
}
|
|
let created = n.create_processor_from_inputs(&core);
|
|
assert!(!created);
|
|
let state = n.state.lock().unwrap();
|
|
assert!(!state.dirty);
|
|
assert_eq!(state.last_path, "");
|
|
assert_eq!(state.last_direction, -1);
|
|
assert!(state.last_processor.is_none());
|
|
// The no-manager branch does not touch the recorded error (C++).
|
|
assert_eq!(state.last_error, "stale error");
|
|
}
|
|
|
|
#[test]
|
|
fn ensure_processor_rebuilds_when_no_cached_processor() {
|
|
let n = node();
|
|
let core = NodeCore::new();
|
|
// Fresh state is dirty -> rebuild (resets markers, returns false).
|
|
n.ensure_processor(&core);
|
|
let state = n.state.lock().unwrap();
|
|
assert!(!state.dirty);
|
|
assert!(state.last_processor.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn input_value_changed_marks_dirty() {
|
|
let mut core = NodeCore::new();
|
|
let mut n = node();
|
|
n.state.lock().unwrap().dirty = false;
|
|
n.input_value_changed(&mut core, FILE_INPUT, 0);
|
|
assert!(n.state.lock().unwrap().dirty);
|
|
n.state.lock().unwrap().dirty = false;
|
|
n.input_value_changed(&mut core, DIRECTION_INPUT, 0);
|
|
assert!(n.state.lock().unwrap().dirty);
|
|
// Unrelated inputs are ignored.
|
|
n.state.lock().unwrap().dirty = false;
|
|
n.input_value_changed(&mut core, crate::nodes::ociobase::TEXTURE_INPUT, 0);
|
|
assert!(!n.state.lock().unwrap().dirty);
|
|
}
|
|
|
|
#[test]
|
|
fn config_changed_marks_dirty() {
|
|
let mut core = NodeCore::new();
|
|
let mut n = node();
|
|
n.state.lock().unwrap().dirty = false;
|
|
n.config_changed(&mut core);
|
|
assert!(n.state.lock().unwrap().dirty);
|
|
}
|
|
|
|
#[test]
|
|
fn file_path_reads_text_standard_value() {
|
|
let mut core = NodeCore::new();
|
|
core.add_input(crate::input::Input::new(
|
|
FILE_INPUT,
|
|
crate::value::ValueType::Text,
|
|
crate::value::NodeValue::Text(String::new()),
|
|
));
|
|
core.set_standard_value(FILE_INPUT, -1, NodeValue::Text("/tmp/x.cube".into()));
|
|
assert_eq!(OCIOLutNode::file_path(&core), "/tmp/x.cube");
|
|
// Non-text standard value (or missing input) yields empty.
|
|
core.set_standard_value(FILE_INPUT, -1, NodeValue::Float(3.0));
|
|
assert_eq!(OCIOLutNode::file_path(&core), "");
|
|
}
|
|
|
|
#[test]
|
|
fn ensure_processor_reuses_cached_when_unchanged() {
|
|
let n = node();
|
|
let mut core = NodeCore::new();
|
|
core.add_input(crate::input::Input::new(
|
|
FILE_INPUT,
|
|
crate::value::ValueType::Text,
|
|
crate::value::NodeValue::Text(String::new()),
|
|
));
|
|
core.add_input(crate::input::Input::new(
|
|
DIRECTION_INPUT,
|
|
crate::value::ValueType::Combo,
|
|
crate::value::NodeValue::Combo(0),
|
|
));
|
|
core.set_standard_value(FILE_INPUT, -1, NodeValue::Text("/tmp/x.cube".into()));
|
|
core.set_standard_value(DIRECTION_INPUT, -1, NodeValue::Combo(1));
|
|
{
|
|
let mut state = n.state.lock().unwrap();
|
|
state.dirty = false;
|
|
state.last_path = "/tmp/x.cube".to_string();
|
|
state.last_direction = 1;
|
|
state.last_processor = Some(crate::handle::make_owned::<u8>(1));
|
|
}
|
|
// Unchanged path+direction with a live processor: early return,
|
|
// markers are preserved (create_processor_from_inputs would have
|
|
// reset them).
|
|
n.ensure_processor(&core);
|
|
let state = n.state.lock().unwrap();
|
|
assert_eq!(state.last_path, "/tmp/x.cube");
|
|
assert_eq!(state.last_direction, 1);
|
|
assert!(state.last_processor.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn generate_processor_wraps_ensure_processor() {
|
|
let mut n = node();
|
|
let mut core = NodeCore::new();
|
|
n.state.lock().unwrap().dirty = true;
|
|
// Worker branch: generate_processor just ensures; markers reset.
|
|
n.generate_processor(&mut core);
|
|
let state = n.state.lock().unwrap();
|
|
assert!(!state.dirty);
|
|
assert_eq!(state.last_path, "");
|
|
assert_eq!(state.last_direction, -1);
|
|
}
|
|
|
|
#[test]
|
|
fn is_main_process_always_false_without_bridge() {
|
|
assert!(!OCIOLutNode::is_main_process());
|
|
}
|
|
|
|
#[test]
|
|
fn value_no_texture_pushes_nothing() {
|
|
let (core, behavior) = create();
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(
|
|
&core,
|
|
&crate::value::NodeValueRow::default(),
|
|
Rational::new(0, 1),
|
|
&mut table,
|
|
);
|
|
assert!(table.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn value_passes_texture_through_without_processor() {
|
|
// Without the render bridge no LUT processor can be created, so
|
|
// the base value() passes the input texture through unchanged.
|
|
let core = NodeCore::new();
|
|
let n = node();
|
|
let tex = NodeValue::Texture(crate::handle::CHandle::null());
|
|
let inputs = crate::value::NodeValueRow::from([(
|
|
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
|
tex.clone(),
|
|
)]);
|
|
let mut table = NodeValueTable::default();
|
|
n.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
|
assert_eq!(table.get(ValueType::Texture), Some(&tex));
|
|
}
|
|
|
|
#[test]
|
|
fn value_pushes_deferred_job_with_processor() {
|
|
let core = NodeCore::new();
|
|
let mut n = node();
|
|
n.base.set_processor(Some(crate::handle::CHandle::null()));
|
|
let inputs = crate::value::NodeValueRow::from([(
|
|
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
|
|
NodeValue::Texture(crate::handle::CHandle::null()),
|
|
)]);
|
|
let mut table = NodeValueTable::default();
|
|
n.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
|
assert!(table.get(ValueType::Texture).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_clones() {
|
|
let (core, behavior) = create();
|
|
let dup = behavior.duplicate(&core).unwrap();
|
|
assert_eq!(dup.name(), "OCIO LUT");
|
|
}
|
|
}
|