render: the M3 OFX host — one oak-worker --ofx-host process for every plugin job
docs/zh/plans/render-pipeline-threads.md M3 (design 3.2): OpenFX crash isolation moves from "every worker hosts plugins" to a single dedicated host process, served over NDJSON + shared memory. - oak-worker --ofx-host mode (src/ofx_host.rs): loads every plugin once, resolves jobs by the cross-process-stable OFX identifier, and renders through the same in-process executor the workers used to install. - oak-render/ofxhost.rs: the single-host client. The render manager creates and installs it for the Pipeline backend (lazy spawn on the first plugin job); eval::process_plugin_job prefers it and falls back to the in-process executor otherwise, so the process backend keeps its current behavior until M4. - Data plane: input/output FrameSlotPool pairs (the handshake's input_* fields are used for the first time). Named clips and the source frame are written to input slots after the explicit CPU readback; the plugin output returns through an output slot. Pool size/capacity grow by a host restart when a job needs more (safe: submissions are serialized and one job is in flight). - Crash loop: reader EOF fails the in-flight submit, which respawns the host and re-posts the same job (frames are read back once); after three consecutive crashes the client is permanently dead and the evaluator falls back to a purple frame. The dead child is reaped immediately, and a submit mutex enforces the one-job-in-flight contract. - Progress/cancel: the host flushes plugin_progress immediately (live progress), and reads stdin on its own thread so plugin_cancel takes effect mid-render at the plugin's next progressUpdate; the sticky flag resets at progressStart and request_plugin_cancel_all broadcasts to both the worker pool and the host. - JobSpec::Plugin / PluginJobPayload carry the plugin type_id (stable across processes); `--ofx-crash-once` / `--ofx-crash-always` are the deterministic crash hooks, matching the worker's env hooks. - Tests: wire round-trips; host unit tests (crash budget, cancel-flag reset through the factory, source mapping); oak-worker integration tests against the real host + bundled test plugin (render + progress, crash respawn and re-post, three-crash give-up, mid-render cancel on the new slow variant, concurrent submits); eval's purple fallback.
This commit is contained in:
@@ -37,6 +37,8 @@
|
||||
#include <stdarg.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
/* Windows 上插件 DLL 带自己的 CRT 环境块:宿主 Rust 侧的
|
||||
@@ -373,6 +375,90 @@ static OfxStatus actionRender(OfxImageEffectHandle inst, OfxPropertySetHandle in
|
||||
return kOfxStatOK;
|
||||
}
|
||||
|
||||
/* ---------- 慢速变体(M3):取消 e2e 测试 ---------- */
|
||||
|
||||
/* progressUpdate 之间的睡眠(宿主取消在渲染中置位,下一次
|
||||
* progressUpdate 返回非 OK,插件立即中止)。 */
|
||||
static void slow_sleep_ms(int ms)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
Sleep((DWORD)ms);
|
||||
#else
|
||||
usleep((useconds_t)ms * 1000);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* 100 次 progressUpdate x 20ms(约 2 秒):把输出填成常量 0.25。
|
||||
* 任一次 progressUpdate 返回非 OK(宿主取消)→ kOfxStatFailed。 */
|
||||
static OfxStatus actionRenderSlow(OfxImageEffectHandle inst, OfxPropertySetHandle inArgs)
|
||||
{
|
||||
double time = 0.0;
|
||||
propGetDouble(inArgs, kOfxPropTime, 0, &time);
|
||||
|
||||
OfxImageClipHandle clip = NULL;
|
||||
OfxPropertySetHandle clipProps = NULL;
|
||||
OfxStatus st = g_imageEffectSuite->clipGetHandle(inst, "Output", &clip, &clipProps);
|
||||
if (st != kOfxStatOK)
|
||||
return st;
|
||||
|
||||
OfxPropertySetHandle image = NULL;
|
||||
st = g_imageEffectSuite->clipGetImage(clip, time, NULL, &image);
|
||||
if (st != kOfxStatOK)
|
||||
return st;
|
||||
|
||||
void *data = NULL;
|
||||
int rowBytes = 0;
|
||||
int bounds[4] = { 0, 0, 0, 0 };
|
||||
propGetPointer(image, kOfxImagePropData, 0, &data);
|
||||
propGetInt(image, kOfxImagePropRowBytes, 0, &rowBytes);
|
||||
propGetIntN(image, kOfxImagePropBounds, 4, bounds);
|
||||
|
||||
if (g_progressSuite) {
|
||||
g_progressSuite->progressStart((OfxImageEffectHandle)inst, "slow-render");
|
||||
for (int i = 0; i < 100; i++) {
|
||||
OfxStatus ps = g_progressSuite->progressUpdate((OfxImageEffectHandle)inst,
|
||||
(double)(i + 1) / 100.0);
|
||||
if (ps != kOfxStatOK) {
|
||||
g_progressSuite->progressEnd((OfxImageEffectHandle)inst);
|
||||
return kOfxStatFailed;
|
||||
}
|
||||
slow_sleep_ms(20);
|
||||
}
|
||||
}
|
||||
|
||||
int w = bounds[2] - bounds[0];
|
||||
int h = bounds[3] - bounds[1];
|
||||
if (data && w > 0 && h > 0) {
|
||||
for (int y = 0; y < h; y++) {
|
||||
float *row = (float *)((char *)data + (size_t)y * (size_t)rowBytes);
|
||||
for (int x = 0; x < w; x++) {
|
||||
row[x * 4 + 0] = 0.25f;
|
||||
row[x * 4 + 1] = 0.25f;
|
||||
row[x * 4 + 2] = 0.25f;
|
||||
row[x * 4 + 3] = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_imageEffectSuite->clipReleaseImage(image);
|
||||
if (g_progressSuite) {
|
||||
g_progressSuite->progressEnd((OfxImageEffectHandle)inst);
|
||||
}
|
||||
return kOfxStatOK;
|
||||
}
|
||||
|
||||
/* 慢速变体的效果入口:render 走慢速路径,其余 action 委托基础实现。 */
|
||||
static OfxStatus mainEntry(const char *action, const void *handle,
|
||||
OfxPropertySetHandle inArgs, OfxPropertySetHandle outArgs);
|
||||
|
||||
static OfxStatus mainEntrySlow(const char *action, const void *handle,
|
||||
OfxPropertySetHandle inArgs, OfxPropertySetHandle outArgs)
|
||||
{
|
||||
if (strcmp(action, kOfxImageEffectActionRender) == 0) {
|
||||
return actionRenderSlow((OfxImageEffectHandle)handle, inArgs);
|
||||
}
|
||||
return mainEntry(action, handle, inArgs, outArgs);
|
||||
}
|
||||
/* ---------- ofxColour(M11 §4):GetOutputColourspace ---------- */
|
||||
|
||||
static OfxStatus actionGetOutputColourspace(OfxPropertySetHandle inArgs,
|
||||
@@ -827,9 +913,20 @@ static const OfxPlugin test_plugin_interact = {
|
||||
/* mainEntry */ mainEntryInteractEffect,
|
||||
};
|
||||
|
||||
/* M3 取消 e2e:慢速 filter(progressUpdate x20ms 循环,取消即中止)。 */
|
||||
static const OfxPlugin test_plugin_slow = {
|
||||
/* pluginApi */ kOfxImageEffectPluginApi,
|
||||
/* apiVersion */ kOfxImageEffectPluginApiVersion,
|
||||
/* pluginIdentifier */ "org.oak.test-plugin.slow",
|
||||
/* pluginVersionMajor */ 1,
|
||||
/* pluginVersionMinor */ 0,
|
||||
/* setHost */ setHost,
|
||||
/* mainEntry */ mainEntrySlow,
|
||||
};
|
||||
|
||||
OfxExport int OfxGetNumberOfPlugins(void)
|
||||
{
|
||||
return 4;
|
||||
return 5;
|
||||
}
|
||||
|
||||
OfxExport OfxPlugin *OfxGetPlugin(int nth)
|
||||
@@ -842,5 +939,7 @@ OfxExport OfxPlugin *OfxGetPlugin(int nth)
|
||||
return (OfxPlugin *)&test_plugin_id;
|
||||
if (nth == 3)
|
||||
return (OfxPlugin *)&test_plugin_interact;
|
||||
if (nth == 4)
|
||||
return (OfxPlugin *)&test_plugin_slow;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -72,3 +72,24 @@ pub mod property;
|
||||
pub mod render;
|
||||
pub mod render_driver;
|
||||
pub mod suites;
|
||||
|
||||
/// The minimal test plugin shared library built by this crate's build
|
||||
/// script (`$OUT_DIR/oak_test_plugin.{so,dylib}`), for integration tests
|
||||
/// that exercise the real plugin path — including the single OFX host
|
||||
/// process (M3) — without a system plugin. The build script always
|
||||
/// compiles it, so this is `None` only if the build directory was
|
||||
/// tampered with (tests treat that as a hard failure, not a skip).
|
||||
pub fn bundled_test_plugin() -> Option<std::path::PathBuf> {
|
||||
let dir = std::path::Path::new(env!("OUT_DIR"));
|
||||
for name in [
|
||||
"oak_test_plugin.dylib",
|
||||
"oak_test_plugin.so",
|
||||
"oak_test_plugin.dll",
|
||||
] {
|
||||
let path = dir.join(name);
|
||||
if path.exists() {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -837,6 +837,7 @@ fn execute_plugin_job(
|
||||
|
||||
let oak_render::eval::JobSpec::Plugin {
|
||||
instance,
|
||||
type_id: _,
|
||||
time,
|
||||
effect_input_id,
|
||||
inputs,
|
||||
@@ -1167,6 +1168,7 @@ mod tests {
|
||||
}
|
||||
let spec = oak_render::eval::JobSpec::Plugin {
|
||||
instance,
|
||||
type_id: "org.oak.test-plugin".to_string(),
|
||||
time: 0.0,
|
||||
effect_input_id: Some("Source".to_string()),
|
||||
inputs: Vec::new(),
|
||||
|
||||
Reference in New Issue
Block a user