plugin: abort() must return 0/non-zero, not ReplyYes/No — fixes black frames
CI / Build & test (Linux) (push) Successful in 18m37s
CI / Build & test (Windows) (push) Successful in 44m45s

The OFX abort contract is "0 = keep rendering, anything else = stop"
and the ofxs support library literally checks abort(...) != 0. Our
host answered kOfxStatReplyNo (13) for "not cancelled", so every
openfx-misc processor aborted its pixel loop before writing a single
row: the render action returned OK with the output buffer untouched —
a solid black frame everywhere an effect was applied (the previous
smoke test only asserted "not the purple failure frame", which let
this slip through as a fake pass).

- image-effect abort: return 0 when not cancelled, 1 when cancelled
  (instance cancel flag or progress cancellation), BadHandle for
  descriptor handles as before
- strengthen the real-plugin smoke tests to also reject an all-zero
  output frame, so a silent no-write can no longer pass
- more trace-gated [ofx] diagnostics (value dumps for propGetN /
  propGetPointer / clipGetImage fetches, multiThread entry, render
  output buffer address) — the tooling that pinned this down

Verified with lldb watchpoints: the AddOFX/ChromaKeyerOFX processors
now write the real pixels through the executor path.
This commit is contained in:
2026-08-25 20:30:37 +08:00
parent 3bf914a2b8
commit 3792c49854
5 changed files with 56 additions and 19 deletions
+2
View File
@@ -1200,7 +1200,9 @@ mod tests {
for i in 0..4 {
px[i] = f32::from_le_bytes(out_frame.data[i * 4..i * 4 + 4].try_into().unwrap());
}
eprintln!("[smoke] {identifier} first pixel out = {px:?}");
assert_ne!(px, [1.0, 0.0, 1.0, 1.0], "must not be the purple failure frame");
assert_ne!(px, [0.0, 0.0, 0.0, 0.0], "must not be a black empty frame");
}
/// 构造一个只含 push-button 参数的最小实例(直接登记进注册表)。
+9
View File
@@ -293,6 +293,15 @@ pub fn render_frame(
render_window,
output.clone(),
)?;
if std::env::var_os("OAK_OFX_TRACE").is_some() {
let p = output.pixels();
let dump: Vec<f32> = p
.chunks_exact(4)
.take(4)
.map(|c| f32::from_le_bytes(c.try_into().unwrap()))
.collect();
eprintln!("[ofx] render_frame: output buffer at {:p}, pixels[0..4] = {dump:?}", p.as_ptr());
}
// 输出装配(pluginrenderer.cpp:1762-1834 的 CPU 路径)。
write_output_frame(&mut dst, &output)?;
} else {
+28 -18
View File
@@ -23,8 +23,9 @@
//! - clipGetRegionOfDefinition:读 clip 实例属性里的
//! kOfxImageEffectPropRegionOfDefinition(协商时宿主写入);非法
//! RoD → kOfxStatFailedHS:2143-2147);
//! - abort:实例期 → 当前渲染的进度取消状态(HS:2154-2170
//! HS 默认返回 0,本实现按规范返回 REPLY_YES/REPLY_NO);
//! - abort:实例期 → 当前渲染的取消状态(HS:2154-2170契约是
//! 0 = 继续、非 0 = 中止——REPLY_YES/NO 会让 ofxs 的
//! `abort(...) != 0` 判断误判中止,像素循环一行不写 → 黑帧);
//! - imageMemory*:账本同 memory suiteHS 的 lock 是"锁住防重分配"
//! 语义,第 1 期账本不需要 → OK no-op,见 memory.rs 文档)。
//!
@@ -71,7 +72,7 @@ pub struct ImageEffectSuiteV1 {
/// clipGetRegionOfDefinition
pub clip_get_region_of_definition:
unsafe extern "C" fn(*mut c_void, c_double, *mut c_void) -> c_int,
/// abort:查询是否应中止(进度取消透传)。
/// abort:查询是否应中止(0 = 继续,非 0 = 中止;取消透传)。
pub abort: unsafe extern "C" fn(*mut c_void) -> c_int,
/// imageMemoryAlloc / imageMemoryFree / imageMemoryLock /
/// imageMemoryUnlock:图像内存管理(账本同 memory suite)。
@@ -309,6 +310,15 @@ unsafe extern "C" fn clip_get_image(
let image = c
.fetch_image(time, scale, region)
.map_err(|_| status::FAILED)?;
if std::env::var_os("OAK_OFX_TRACE").is_some() {
let p = image.pixels();
let dump: Vec<f32> = p
.chunks_exact(4)
.take(4)
.map(|c| f32::from_le_bytes(c.try_into().unwrap()))
.collect();
eprintln!("[ofx] clipGetImage({}) fetched pixels[0..4] = {dump:?}", c.name);
}
let image = std::sync::Arc::new(image);
let addr = &image.props as *const _ as usize;
LIVE_IMAGES
@@ -377,26 +387,26 @@ fn read_rod(props: &PropertySet) -> Option<OfxRectD> {
Some(OfxRectD { x1, y1, x2, y2 })
}
/// abort实例期 → 当前渲染进度是否已取消(HS:2154-2170 的
/// `instance->abort()`HS 默认 0,本实现按规范返回 REPLY_YES/NO)。
/// abort查询是否应中止渲染。OFX 契约(ofxImageEffect.h)与
/// ofxs 实现(ofxsImageEffect.cpp `abort(...) != 0`)都是
/// **0 = 继续、非 0 = 中止**——不能回 REPLY_YES/NO13 会被
/// openfx-misc 理解成"立即中止",像素循环一行都不写 → 黑帧)。
/// 取消状态是成功应答而非错误码,不能走 caught 的 Err 通道。
unsafe extern "C" fn abort(effect: *mut c_void) -> c_int {
catch_unwind(AssertUnwindSafe(|| {
match resolve_effect(effect)? {
EffectRef::Instance(_) => {}
let inst = match resolve_effect(effect)? {
EffectRef::Instance(i) => i,
EffectRef::Descriptor(_) => return Err(status::ERR_BAD_HANDLE),
}
Ok(())
};
Ok(inst)
}))
.map_or_else(
|_| status::FAILED,
|r| match r {
Ok(()) => {
if crate::suites::progress::is_cancelled() {
status::REPLY_YES
} else {
status::REPLY_NO
}
Ok(inst) => {
let cancelled = inst.cancel.load(std::sync::atomic::Ordering::Relaxed)
|| crate::suites::progress::is_cancelled();
if cancelled { 1 } else { 0 }
}
Err(c) => c,
},
@@ -555,7 +565,7 @@ mod tests {
}
}
/// abortdescribe 期 → BadHandle;实例期 → REPLY_NO(未取消)。
/// abortdescribe 期 → BadHandle;实例期 → 0(未取消)。
#[test]
fn abort_requires_instance() {
let desc = EffectDescriptor::new();
@@ -565,7 +575,7 @@ mod tests {
assert_eq!((s.abort)(dh), status::ERR_BAD_HANDLE);
assert_eq!((s.abort)(std::ptr::null_mut()), status::ERR_BAD_HANDLE);
}
// 实例期:未取消 → REPLY_NO
// 实例期:未取消 → 0(OFX 契约:0 = 继续,非 0 = 中止)
let inst = std::sync::Arc::new(crate::instance::Instance {
props: PropertySet::new(),
plugin: std::sync::Arc::new(crate::host::Plugin {
@@ -593,7 +603,7 @@ mod tests {
});
let ih = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
unsafe {
assert_eq!((s.abort)(ih), status::REPLY_NO);
assert_eq!((s.abort)(ih), 0);
}
}
@@ -78,6 +78,9 @@ unsafe extern "C" fn multi_thread(
thread_arg: *mut c_void,
) -> c_int {
caught(|| {
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] multiThread: n_threads={n_threads} func={:#x} arg={thread_arg:p}", func as usize);
}
// 裸指针不可 Send:以 usize 搬运,线程内还原(C 侧本即
// 整数传递语义)。
let arg = thread_arg as usize;
+14 -1
View File
@@ -456,6 +456,9 @@ unsafe extern "C" fn prop_get_pointer(
let v = get_scalar(set, name, index, Kind::Pointer)?;
match v {
Value::Pointer(p) => {
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] propGetPointer(h={handle:p}, {name}[{index}]) -> {p:p}");
}
*out = p;
Ok(())
}
@@ -595,7 +598,17 @@ fn get_n(
write(v, i);
}
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] propGetN({name} x{n}) -> ok");
let dump: Vec<String> = p
.values
.iter()
.take(n)
.map(|v| match v {
Value::Int(i) => i.to_string(),
Value::Double(d) => d.to_string(),
_ => "?".to_string(),
})
.collect();
eprintln!("[ofx] propGetN({name} x{n}) -> ok [{dump:?}]");
}
Ok(())
})