render: apply clip effect stacks in the montage path
CI / Build & test (Windows) (push) Failing after 16m26s
CI / Build & test (Linux) (push) Successful in 19m12s

Adding an effect to a clip did nothing: the sequence render is
flattened into a montage (decode + composite), and MontageClip carried
no effect data at all.

- MontageClip gains an ordered effect stack (type id / enabled /
  effect input / parameter values); protocol v2 carries it as an
  additive wire field (older peers default to an empty stack).
- renderops::video_montage fills the stack from the effect chain
  (the footage source node — the chain end without an effect input —
  is dropped; the montage decodes the footage itself). Export
  (oak-task) and the multicam single-track montage fill it too.
- The worker applies the stack between decode and composite: built-in
  Opacity gets a CPU evaluator (C++ opacity.frag parity — whole vec4,
  alpha included, unity pass-through); everything else dispatches as an
  OFX plugin job through a new instance-factory slot (oak-plugin
  lazily creates + caches one instance per identifier per render
  process) with the montage's parameters injected. Disabled effects
  bypass (the C++ traverser pushes the effect input through). Unknown
  types warn once per type id and pass through — no silent no-ops.

Not covered (explicitly): Transform/Crop and the other ~30 built-in
effects have no CPU evaluator in oak-render (they pass through with a
warning), keyframed parameter animation, audio effect chains, and the
CLI's simplified montage.

Acceptance: a real 50% Opacity on real media quarters the rendered
pixels both in-process (renderops test) and through a real worker
process over IPC + shared memory (procpool_integration test);
disabling restores the plain render byte-for-byte.
This commit is contained in:
2026-08-25 04:49:00 +08:00
parent 28f4ed655e
commit f2aab8ce15
10 changed files with 926 additions and 2 deletions
+44
View File
@@ -927,10 +927,47 @@ static EXECUTOR_INSTALLED: OnceLock<()> = OnceLock::new();
pub fn install_render_executor() {
EXECUTOR_INSTALLED.get_or_init(|| {
oak_render::eval::set_plugin_executor(Some(Arc::new(execute_plugin_job)));
oak_render::eval::set_plugin_instance_factory(Some(Arc::new(shared_plugin_instance)));
oak_node::nodes::plugin::set_plugin_duplicator(Some(Arc::new(duplicate_instance)));
});
}
// ---------------------------------------------------------------------------
// montage 渲染路径的共享实例(渲染进程按插件标识惰性创建 + 进程级缓存)
// ---------------------------------------------------------------------------
/// montage 效果栈携带插件标识而非实例 id(montage 在主进程从时间线
/// 解析,实例活在渲染进程)。渲染进程经此工厂按需创建实例并缓存:
/// 参数每次渲染前由 montage 注入([`execute_plugin_job`] 的 values
/// 覆盖),worker 单线程顺序渲染,共享实例无跨帧状态冲突。
fn shared_plugin_instance(identifier: &str) -> Option<u64> {
static SHARED: OnceLock<Mutex<HashMap<String, u64>>> = OnceLock::new();
let cache = SHARED.get_or_init(|| Mutex::new(HashMap::new()));
if let Some(&id) = cache
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(identifier)
{
return Some(id);
}
let host = Host::global();
let plugin = host.cache.find(identifier)?;
// 上下文选择与 [`register_plugin_nodes`] 一致(filter 优先,否则
// 首个支持上下文;factory.cpp:171-177)。
let context = if plugin.contexts.iter().any(|c| c == "OfxImageEffectContextFilter") {
"OfxImageEffectContextFilter".to_string()
} else {
plugin.contexts.first()?.clone()
};
let inst = host.create_instance(identifier, Some(&context)).ok()?;
let id = register_instance(inst);
cache
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(identifier.to_string(), id);
Some(id)
}
// ---------------------------------------------------------------------------
// 测试
// ---------------------------------------------------------------------------
@@ -1085,6 +1122,13 @@ mod tests {
unregister_instance(u64::MAX);
}
#[test]
fn shared_instance_unknown_identifier_yields_none() {
// 缓存里查无此标识 → Nonemontage 路径据此按"无求值器"
// 告警并直通)。
assert!(shared_plugin_instance("com.example.definitely-missing").is_none());
}
/// 构造一个只含 push-button 参数的最小实例(直接登记进注册表)。
fn instance_with_push_button() -> u64 {
use std::ffi::{c_char, c_void};