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:
2026-09-12 23:10:43 +08:00
parent 4337559ed0
commit fec6e9dba7
14 changed files with 2054 additions and 27 deletions
+21
View File
@@ -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
}
+2
View File
@@ -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(),