Files
oak-editor/crates/oak-node/src/nodes/merge.rs
T
Mike-Solar 3a48dd4991 render: Job enum in the tables, single-loop match resolve, real CacheJob
M0a of the render-pipeline plan (docs/zh/plans/render-pipeline-threads.md):

- oak-node: every payload push site (58 across footage.rs, plugin.rs
  and the nodes/* effects) now boxes the Job enum instead of the raw
  payload. The enum gains CacheJob with a CacheJobPayload (path +
  time + fallback value, the C++ cachejob.h shape), plus safe as_*
  accessors and unsafe probe helpers beside job_ref.
- oak-render: RenderEvalHooks::resolve is one loop over the table —
  a single get_checked::<Job> probe per texture value, a match
  dispatch to process_footage/shader/plugin/color_transform/cache,
  and recursive resolution of the job boxes embedded in a payload's
  inputs (depth-capped, cycle-guarded) — replacing the four
  sequential full-table scans (resolve_*_jobs, deleted).
- The disk frame cache is real: frameio.rs implements a minimal
  self-describing F32 container (magic/version/dims/format/timestamp
  + payload, tmp-write + atomic rename, full header validation on
  load) because the OIIO bridge is a stub and EXR is unavailable in
  this build; process_cache_job genuinely reads the file before
  falling back to the job's (already resolved) fallback value.
- Tests: CacheJob roundtrip (save -> resolve -> pixel equality),
  missing-file fallback, nested cache-job-through-shader resolution,
  plus four frameio container tests. 2330 passed, 0 failed across
  the workspace.
2026-09-11 10:38:12 +08:00

328 lines
9.9 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/>.
//! Merge node (C++ `src/node/src/math/merge/merge.{h,cpp}`,
//! `olive::MergeNode`): alpha-over composites two textures.
use crate::factory::NodeMeta;
use crate::jobs::{Job, ShaderJobPayload};
use crate::node::{Category, NodeBehavior, NodeCore};
/// Base (background) texture input id (C++ `k_base_in`). Type:
/// texture; flags: not-keyframable.
pub const BASE_INPUT: &str = "base_in";
/// Blend (foreground) texture input id (C++ `k_blend_in`). Type:
/// texture; flags: not-keyframable.
pub const BLEND_INPUT: &str = "blend_in";
/// Merge node. Unit-like: the C++ class's only members are
/// `base_in_`/`blend_in_` `NodeInput*` back-pointers, which live in
/// [`NodeCore`] here, so there is nothing to duplicate.
pub struct MergeNode;
/// Fragment shader (C++ `get_shader_code()` loads the
/// `:/shaders/alphaover.frag` resource). Text copied verbatim from
/// `engine/shaders/alphaover.frag`.
const SHADER_FRAG: &str = r#"uniform sampler2D base_in;
uniform sampler2D blend_in;
uniform bool base_in_enabled;
uniform bool blend_in_enabled;
in vec2 ove_texcoord;
out vec4 frag_color;
void main(void) {
vec4 base_col = texture(base_in, ove_texcoord);
vec4 blend_col = texture(blend_in, ove_texcoord);
if (!base_in_enabled && !blend_in_enabled) {
frag_color = vec4(0.0);
return;
}
if (!base_in_enabled) {
frag_color = blend_col;
return;
}
if (!blend_in_enabled) {
frag_color = base_col;
return;
}
base_col *= 1.0 - blend_col.a;
base_col += blend_col;
frag_color = base_col;
}
"#;
impl MergeNode {
/// Fragment shader (C++ `get_shader_code()`; the request id is
/// ignored — the same alpha-over shader serves every request).
fn shader_frag() -> &'static str {
SHADER_FRAG
}
}
impl NodeBehavior for MergeNode {
/// Human-readable name (C++ `name()`).
fn name(&self) -> &str {
"Merge"
}
/// Stable type id (C++ `id()`).
fn type_id(&self) -> &str {
"org.olivevideoeditor.Olive.merge"
}
/// Categories (C++ `category()`; filed under math even though it
/// composites textures).
fn categories(&self) -> &[Category] {
&[Category::Math]
}
/// Description (C++ `description()`).
fn description(&self) -> &str {
"Merge two textures together."
}
/// Localized input names (C++ `retranslate()`): `base_in` ->
/// "Base", `blend_in` -> "Blend".
fn input_name<'a>(&self, id: &'a str) -> &'a str {
match id {
BASE_INPUT => "Base",
BLEND_INPUT => "Blend",
_ => id,
}
}
/// Evaluate outputs (C++ `value()`): if only the blend texture is
/// present, or the blend texture has fewer than RGBA channels (no
/// alpha to over with), push the blend input as-is; if only the
/// base texture is present, push the base input as-is; if both are
/// present, push a shader job over the base texture with the whole
/// input row as job values; if neither, push nothing.
///
/// The both-present case boxes a [`ShaderJobPayload`] that the
/// renderer's resolve hook executes and replaces with the result
/// texture; the params row carries both input textures, keyed by
/// their input ids. The C++ `MergeNode` constructor never sets an
/// effect input, so `effect_input` is empty and the runner binds
/// `base_in`/`blend_in` explicitly by name (the pass size follows the
/// first bound texture — the base). The "blend has fewer than 4
/// channels" check
/// needs the texture's channel count, which the Rust texture handle
/// does not carry, so the alpha-less blend case is only
/// distinguishable by presence here (`// CPP-PARITY: merge.cpp`
/// `value()`).
fn value(
&self,
core: &NodeCore,
inputs: &crate::value::NodeValueRow,
time: oak_core::Rational,
table: &mut crate::value::NodeValueTable,
) {
let base = inputs.get(BASE_INPUT);
let blend = inputs.get(BLEND_INPUT);
match (base, blend) {
(
Some(b @ crate::value::NodeValue::Texture(_)),
Some(bl @ crate::value::NodeValue::Texture(_)),
) => {
// Both present: alpha-over shader job (C++
// `base_tex->toJob(ShaderJob(value))`). The C++ checks the
// blend channel count here (RGBA required for an alpha to over
// with) and pushes the blend as-is when it has no alpha
// channel — not representable without the texture params
// (`// CPP-PARITY: merge.cpp`).
let _ = (b, bl);
table.push(
crate::value::ValueType::Texture,
crate::value::NodeValue::Texture(crate::handle::make_owned(Job::ShaderJob(ShaderJobPayload {
node_id: crate::id::NodeId::INVALID,
time,
iterations: 1,
type_id: self.type_id().to_string(),
shader_id: String::new(),
effect_input: core.effect_input.clone(),
params: inputs.clone(),
iterative_input: String::new(),
}))),
None,
);
}
(Some(b @ crate::value::NodeValue::Texture(_)), None) => {
table.push(crate::value::ValueType::Texture, b.clone(), None);
}
(None, Some(bl @ crate::value::NodeValue::Texture(_))) => {
table.push(crate::value::ValueType::Texture, bl.clone(), None);
}
_ => {}
}
}
/// Shader code request (C++ `get_shader_code()`): always returns
/// the alpha-over fragment shader regardless of the request id.
fn shader_code(&self, _request: &str) -> Option<String> {
Some(SHADER_FRAG.to_string())
}
/// Deep copy (C++ `copy()`).
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
Some(Box::new(MergeNode))
}
}
/// Constructor (C++ `MergeNode::MergeNode()`): adds `base_in` and
/// `blend_in` as not-keyframable texture inputs and sets the
/// dont-show-in-param-view flag.
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
let mut core = NodeCore::new();
let mut base = crate::input::Input::new(
BASE_INPUT,
crate::value::ValueType::Texture,
crate::value::NodeValue::None,
);
base.flags |= crate::input::flags::NOT_KEYFRAMABLE;
core.add_input(base);
let mut blend = crate::input::Input::new(
BLEND_INPUT,
crate::value::ValueType::Texture,
crate::value::NodeValue::None,
);
blend.flags |= crate::input::flags::NOT_KEYFRAMABLE;
core.add_input(blend);
core.flags |= crate::node::flags::DONT_SHOW_IN_PARAM_VIEW;
(core, Box::new(MergeNode))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node::NodeBehavior;
use crate::value::{NodeValue, NodeValueTable, ValueType};
use oak_core::Rational;
fn tex() -> NodeValue {
NodeValue::Texture(crate::handle::CHandle::null())
}
#[test]
fn input_names() {
let n = MergeNode;
assert_eq!(n.input_name(BASE_INPUT), "Base");
assert_eq!(n.input_name(BLEND_INPUT), "Blend");
}
#[test]
fn create_wires_inputs_and_flag() {
let (core, behavior) = create();
assert_eq!(behavior.type_id(), "org.olivevideoeditor.Olive.merge");
assert!(core.get_input(BASE_INPUT).is_some());
assert!(core.get_input(BLEND_INPUT).is_some());
assert_ne!(core.flags & crate::node::flags::DONT_SHOW_IN_PARAM_VIEW, 0);
}
#[test]
fn value_neither_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_base_only_pushes_base() {
let (core, behavior) = create();
let base = tex();
let inputs = crate::value::NodeValueRow::from([(BASE_INPUT.to_string(), base.clone())]);
let mut table = NodeValueTable::default();
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
assert_eq!(table.get(ValueType::Texture), Some(&base));
}
#[test]
fn value_blend_only_pushes_blend() {
let (core, behavior) = create();
let blend = tex();
let inputs = crate::value::NodeValueRow::from([(BLEND_INPUT.to_string(), blend.clone())]);
let mut table = NodeValueTable::default();
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
assert_eq!(table.get(ValueType::Texture), Some(&blend));
}
#[test]
fn value_both_pushes_job_payload() {
let (core, behavior) = create();
let inputs = crate::value::NodeValueRow::from([
(BASE_INPUT.to_string(), tex()),
(BLEND_INPUT.to_string(), tex()),
]);
let mut table = NodeValueTable::default();
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
match table.get(ValueType::Texture) {
Some(NodeValue::Texture(h)) => {
let payload = unsafe { crate::jobs::shader_job(h) }
.expect("shader job payload boxed");
assert_eq!(payload.type_id, "org.olivevideoeditor.Olive.merge");
assert_eq!(payload.shader_id, "");
assert_eq!(payload.iterations, 1);
assert_eq!(payload.iterative_input, "");
assert!(payload.params.contains_key(BASE_INPUT));
assert!(payload.params.contains_key(BLEND_INPUT));
}
_ => panic!("texture expected"),
}
}
#[test]
fn shader_code_always_alphaover() {
let n = MergeNode;
let code = n.shader_code("anything").unwrap();
assert!(code.contains("base_col *= 1.0 - blend_col.a;"));
}
#[test]
fn duplicate_clones() {
let (core, behavior) = create();
let dup = behavior.duplicate(&core).unwrap();
assert_eq!(dup.name(), "Merge");
}
}
/// Register this node type (C++ `k_merge_node` in
/// `factory.cpp::create_from_factory_index`).
pub fn register(meta: &mut Vec<NodeMeta>) {
meta.push(NodeMeta {
type_id: "org.olivevideoeditor.Olive.merge",
name: "Merge",
categories: &[Category::Math],
create,
});
}