multicam: wizard + AFV + node-graph preview + performance
CI / Build & test (Linux) (push) Failing after 24s
CI / Build & test (Windows) (push) Failing after 3m38s

- wizard: angle multi-select, sync modes, auto-align, create sequence;
  keeps the host sequence current (host clip = the multicam clip)
- build_multicam_sequence: per-angle clip -> SOURCES_INPUT[element],
  array slot growth, audio angle tracks for AFV
- AFV: host linked audio clip follows the switched source (one undo),
  muted host audio track disables it
- node-graph preview: inline producer renders through the traverser
  (viewer/project-matched graph frames); sequence viewer uses the graph
- multicam node value(): element-tagged row keys (sources_in[i]),
  reads the current source; build_row keys array inputs by element
- angle grid: viewer=0 (single-track montage, not whole-graph)
- project explorer: rename (dialog) + delete (undoable) real items
- performance: decoded-frame LRU, per-process NVDEC quota (1 session,
  evict before open), GPU composite fail-once fallback, snapshot
  upload debounced on the engine tick, worker vram budget headroom
- timeline clip: multicam overlay via ClipDecorator
- wizard menu item moved to Sequence menu
This commit is contained in:
2026-09-01 20:27:59 +08:00
parent 8de9af705e
commit 1b0a15192e
25 changed files with 3528 additions and 65 deletions
+20 -9
View File
@@ -18,6 +18,7 @@
//! `src/node/src/input/multicam/multicamnode.{h,cpp}`,
//! `olive::MultiCamNode`).
use std::any::Any;
use crate::factory::NodeMeta;
use crate::id::NodeId;
use crate::node::{Category, NodeBehavior, NodeCore};
@@ -256,14 +257,12 @@ impl NodeBehavior for MultiCamNode {
}
}
/// Evaluate outputs (C++ `value()`): pushes the first value of the
/// `sources_in` value array (which, per
/// `active_elements_at_time`, is the currently selected source);
/// pushes nothing when the array is empty.
///
/// The Rust row carries no array payload (the `sources_in` value is
/// the single active element), so the connected value is pushed
/// through as-is; when the input is absent nothing is pushed.
/// Evaluate outputs (C++ `value()`): pushes the ARRAY element of
/// `sources_in` selected by `current_in` (the current source) — the
/// traverser stores array elements under `{input}[{element}]` keys
/// ([`crate::traverser`]), so the selected source's texture is the
/// value at `sources_in[source]`. A missing element (or an absent
/// array) pushes nothing.
fn value(
&self,
core: &NodeCore,
@@ -271,7 +270,19 @@ impl NodeBehavior for MultiCamNode {
time: Rational,
table: &mut NodeValueTable,
) {
let _ = (core, time);
let _ = time;
let source = core.standard_value(CURRENT_INPUT, -1).to_double() as i32;
let key = if source >= 0 {
format!("{SOURCES_INPUT}[{source}]")
} else {
SOURCES_INPUT.to_string()
};
if let Some(v) = inputs.get(&key) {
table.push(v.value_type(), v.clone(), None);
return;
}
// Fallback: an unindexed source value (single-source wiring from
// before the element-tagged rows).
if let Some(v) = inputs.get(SOURCES_INPUT) {
table.push(v.value_type(), v.clone(), None);
}
+11 -1
View File
@@ -254,6 +254,11 @@ fn build_row(
// which elements are live at this time (C++
// `GetActiveElementsAtTime` — a track pulls only the blocks
// covering the frame). An empty answer means "no restriction".
// Element-tagged keys: an array input's per-element values coexist
// in the row under `{input}[{element}]` (C++ `GetValueAtTime`
// indexes the array; the multi-cam node reads exactly the element
// of its current source — a plain `id` key would collapse the
// array to its last element).
if conns.iter().any(|(_, e)| *e >= 0) {
let active = entry.behavior.active_elements_at_time(id, time);
if !active.is_empty() {
@@ -271,7 +276,12 @@ fn build_row(
.get(&(from, upstream_time))
.map(|t| pick_value(t, entry.core.input_data_type(id)))
.unwrap_or(NodeValue::None);
row.insert(id.to_string(), value);
let key = if element >= 0 {
format!("{id}[{element}]")
} else {
id.to_string()
};
row.insert(key, value);
}
}
row