mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-06 18:41:26 +00:00
* Plug-ins: API 2 (PluginHost, padding, layer_span) Split out of the OpenFX work so it can be reviewed on its own. Additive; the WebAssembly ABI stays at 1. - point3 / layer / text / hidden parameters, integer sliders, parameter groups. - EffectPlugin::padding (transparent pixels around the buffer, at most 4096 per side). - EffectPlugin::render_v2 with a PluginHost: the input at other times, other layers, parameters at other times, frame rate, per-instance key, the layer's source range. - EffectEnv::layer_span from the renderer; MAX_PLUGINS 1024. - plugin_error: a plug-in effect's last render error, for the UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxacwTpUwqyfpcLgKW8yjS * Plug-ins: external effect helpers (crates/helper-host) Native plug-in formats run in separate helper programs, never in EffectCraft's process (docs/adr/0001-external-effect-helpers.md). New safe-Rust crate effectcraft-helper-host (L3, native only, unsafe forbidden): it finds helper programs, starts each as a child process and registers its effects through plug-in API 2, so the UI and command surface need nothing new. - Protocol v1 (docs/external-helpers.md): JSON lines over stdin / stdout (helper replies are prefixed so plug-in chatter is ignored); pixels go through a frame exchange file (/dev/shm on Linux), never through the pipe. Render-time callbacks for the input at other times, other layers and parameters at other times, with nested renders through the same helper. - A helper that exits, hangs past its timeout or breaks the protocol is stopped. The render fails with a message, the frame is left as it was, and the helper restarts for the next render. An effect that stops its helper 3 times is turned off (helper_blocklist.txt). - Opt-in: nothing starts helpers unless the user asks (Effect > Scan for Effect Helpers), or opts in to starting them at launch with EFFECTCRAFT_HELPERS=1, an environment variable, so writing to the settings folder alone can't run code. effect.helpers.scan / load are refused for confined agent sessions (confine.rs), like file.executeFile. - Discovery: helpers/ next to the app (Contents/Helpers on macOS), helpers/ in the settings folder, EFFECTCRAFT_HELPER_PATH. - Commands effect.helpers.list / scan / load; Effect menu entries (translated); the last render error of an effect shows as a warning sign after its name in Effect Controls. - serve(): the helper side for Rust helpers. tests/it/main.rs runs the whole protocol (render, padding, callbacks, nesting, threads, crash, restart, block list, hang) against a fake helper, with nothing to install. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxacwTpUwqyfpcLgKW8yjS * Effect helpers: address review (start failures, frame file mode, nextest listing) - A helper that can't start (not installed any more, restarting too often, no frame exchange file) is reported but no longer charged to the effect being rendered: it is Failure::Refused, not Failure::Broken, so it never counts toward turning that effect off. New check in tests/it: once the helper is out of restarts, renders of an effect fail without blocking it (fails without the fix: the effect was turned off after 3 such renders). - The frame exchange file is created owner-only (0o600) on Unix: it holds the user's frames and /dev/shm is shared. Asserted in the frames unit test. - tests/it (harness = false) speaks the libtest listing protocol cargo-nextest uses: `--list --format terse` prints `helper_protocol: test` (nothing with `--ignored`), and `helper_protocol --exact` runs the checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxacwTpUwqyfpcLgKW8yjS * Effect helpers: bound writes to a helper, drain stderr, keep callbacks in step - Writes to a helper go through a writer thread with the request timeout: a helper that stops reading its stdin (hung with a large callback answer or render request pending) no longer blocks the render thread forever while holding the helper. - stderr is drained as bytes (lossy, in 64 KiB pieces) to the end: output that isn't UTF-8 stopped the reader, closing the pipe under the helper (EPIPE / SIGPIPE on its next write); lines were unbounded. - Callback images that can't be written are answered with `null` instead of failing the render while the helper waits (which left the protocol out of step); a frame that can't be written before the request is sent is not charged to the effect. - Mid-render calls use the render's own process: no respawn or heartbeat ping while the helper is waiting for an answer. - Waiting for a busy helper is bounded (load timeout) instead of forever; status() no longer blocks on a running render. - Reap the child when start-up fails after spawning; cap the block list read. - Scripts start helpers only with Allow Scripts to Write Files and Access Network on (they run native code), as for File.execute(). - helper-host declares the workspace's no-panic crate lints; the desktop main() keeps its doc comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Effect helpers: rustfmt the fake provider, build the test session in its initializer (clippy) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Plug-in Library: offer WebAssembly API 1 releases only; a helper already stopped mid-render isn't charged again - WasmPlugin loads only modules at the WebAssembly ABI version, which stays 1 while PLUGIN_API_VERSION is 2: share it as effects::plugin::WASM_API_VERSION and have the library's compatibility check use it, so it never installs an API 2 release that can't load. - with_running: a process that is already gone was stopped by what broke it (a nested render, charged there), so this render fails without charging its own effect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * frames: plug-in effects count as reading other frames (plug-in API 2) With main's narrowed frame invalidation (#2795), an edit inside a precomp under an effect that reads its layer at other times must reach the layer's whole span. Plug-in API 2 gives every plug-in effect the input at other times, so any effect outside ec. counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
99 lines
7.9 KiB
Markdown
99 lines
7.9 KiB
Markdown
# External effect helpers
|
||
|
||
An **external effect helper** is a separate program that offers effects to EffectCraft. Each
|
||
helper runs as a child process and talks to EffectCraft over the protocol below. EffectCraft
|
||
registers every effect a helper lists as an ordinary plug-in effect: it shows up in Effects &
|
||
Presets, has normal properties (keyframes, expressions, Effect Controls) and renders through
|
||
the helper. Helpers exist to run native code, such as an OpenFX plug-in host, **outside**
|
||
EffectCraft's process. If a helper crashes or hangs, EffectCraft shows the error on the effect
|
||
and carries on. See [ADR 0001](adr/0001-external-effect-helpers.md) for why.
|
||
|
||
## Using helpers
|
||
|
||
| What | How |
|
||
|---|---|
|
||
| Install | Put the helper program (`effectcraft-helper-<name>`, `.exe` on Windows) in `helpers/` next to EffectCraft, in `helpers/` in the [settings folder](preferences.md), or in a folder named in `EFFECTCRAFT_HELPER_PATH` (a path list; entries can be programs or folders). Subfolders one level down are searched too. |
|
||
| Start | Helpers run native code, so nothing starts them unless the user asks: Effect ▸ Scan for Effect Helpers (`effect.helpers.scan`) starts them. To start them every time EffectCraft launches, opt in with `EFFECTCRAFT_HELPERS=1`. It is an environment variable, not a setting, so writing to the settings folder can't turn it on. Agents confined with `--automation-read-root` / `--automation-write-root` can't run `effect.helpers.scan` or `effect.helpers.load`, and scripts can run them only with Allow Scripts to Write Files and Access Network on. |
|
||
| Load more plug-ins | Effect ▸ Load Plug-ins Through Helper… (`effect.helpers.load {path}`) hands a file or folder to the helpers that accept it (an OpenFX helper takes `.ofx` plug-ins and `.ofx.bundle` folders). |
|
||
| Inspect | `effect.helpers.list` → `{helpers: [{program, name, version, loads, running, effects, restarts, error}], blocked}`; `effect.plugins.list` shows each effect's `source` (`helper: …`) and last render `error`. |
|
||
| Errors | A failed render leaves the frame as it was and puts ⚠ after the effect's name in Effect Controls (hover for the message). The next render starts the helper again. An effect that stops its helper 3 times is turned off and listed in `helper_blocklist.txt` in the settings folder. `effect.helpers.scan {"retry": true}` turns it back on. A helper that stops 5 times in a minute stays off until the next scan. |
|
||
| Timeouts | `EFFECTCRAFT_HELPER_TIMEOUT` (seconds per render step, default 120) and `EFFECTCRAFT_HELPER_LOAD_TIMEOUT` (start-up, describe, load; default 600). A helper idle for 30 s is pinged before it is used again. |
|
||
|
||
Helpers are not available in the web build.
|
||
|
||
## Protocol (version 1)
|
||
|
||
EffectCraft starts the helper with the single argument `--effectcraft-helper`, with stdin,
|
||
stdout and stderr piped. The environment variable `EFFECTCRAFT_HELPER_STATE_DIR` names a folder
|
||
where the helper may keep its own records (for example, plug-ins that crashed it while
|
||
loading).
|
||
|
||
- **Requests** (EffectCraft → helper): one JSON object per line on stdin.
|
||
- **Replies** (helper → EffectCraft): one JSON object per line on stdout, prefixed with
|
||
`@ec-helper ` (with the space). Lines without the prefix, or text before it, are logged and
|
||
otherwise ignored, so plug-ins that print to stdout don't break the protocol. stderr is
|
||
logged (whatever bytes it holds).
|
||
- Every object has an `"op"`. Lines are at most 256 MB.
|
||
- The helper exits when stdin closes or on `shutdown`.
|
||
|
||
### Frame exchange file
|
||
|
||
Pixels never go through the pipe. EffectCraft creates a file (in `/dev/shm` on Linux when
|
||
available, otherwise the temp folder) and names it in `hello`. Images are addressed by an
|
||
**image ref**:
|
||
|
||
```json
|
||
{"offset": 0, "width": 1920, "height": 1080, "origin": [0, 0], "scale": 1}
|
||
```
|
||
|
||
The image is `width × height` pixels starting `offset` bytes into the file. Each pixel is
|
||
premultiplied RGBA as four `f32`s in native byte order (16 bytes), rows top to bottom, and an
|
||
image is at most 2²⁸ pixels. `origin` is the buffer position of layer pixel (0, 0); `scale` is
|
||
buffer pixels per layer pixel. EffectCraft decides every offset, so the helper only reads and
|
||
writes where it's told. On Unix, EffectCraft unlinks the file once the helper has opened it, so
|
||
nothing is left behind if either side dies. Helpers may map the file instead of reading it.
|
||
|
||
### Messages
|
||
|
||
| Request | Reply | |
|
||
|---|---|---|
|
||
| `{"op":"hello","version":1,"frames":"<path>"}` | `{"op":"hello","version":1,"name":"OpenFX","helper_version":"1.0","loads":["ofx"]}` | First message. The helper opens `frames` read/write. `loads` lists the extensions `load` accepts. A different `version` means EffectCraft won't use the helper. |
|
||
| `{"op":"describe"}` | `{"op":"effects","effects":[{"manifest":{…},"padding":true}],"errors":["…"]}` | `manifest` is a [plug-in API 2 manifest](plugins.md#plug-in-api-2-native-plug-ins) (id, name, category, params). `padding: true` means EffectCraft asks `padding` before each render. |
|
||
| `{"op":"load","path":"…"}` | `effects` (only the new ones) or `error` | A file or folder the user picked. Replayed after a restart. |
|
||
| `{"op":"padding","effect":"…","instance":7,"time":1.0,"scale":1.0,"layer_size":[w,h],"flat":[…],"texts":[…]}` | `{"op":"padding","pixels":3}` | Transparent pixels to add on each side before the render (at most 4096). |
|
||
| `{"op":"render","effect":"…","instance":7,"time":1.0,"frame":<image ref>,"layer_size":[w,h],"flat":[…],"texts":[…],"frame_rate":30,"frame_range":[0,10]}` | callbacks, then `{"op":"done"}` or `{"op":"error","message":"…"}` | Process `frame` in place: read it, write the result to the same ref, reply `done`. `flat` / `texts` are the parameter values flattened by plug-in API 2 rules. `instance` identifies the applied effect (stable across frames). `frame_range` is the layer's source range in layer-time seconds, or `null`. |
|
||
| `{"op":"destroy","instance":7}` | none | The instance is gone; free what it holds. |
|
||
| `{"op":"ping"}` | `{"op":"pong"}` | Heartbeat. |
|
||
| `{"op":"shutdown"}` | none | Exit. |
|
||
|
||
**Callbacks during a render.** Before it replies `done`, the helper may ask:
|
||
|
||
| Helper asks | EffectCraft answers |
|
||
|---|---|
|
||
| `{"op":"input_at","time":t}` | `{"op":"image","image":<image ref>\|null}`: the effect's input (the layer, its masks, the effects above) at layer time `t`, in its own extent. |
|
||
| `{"op":"layer_input","param":"<layer param id>","time":t}` | `image`: that layer at layer time `t` with effects and masks, resampled into this frame's grid. |
|
||
| `{"op":"params_at","time":t}` | `{"op":"params","values":[[flat…],[texts…]]\|null}` |
|
||
|
||
While a helper waits for an answer, EffectCraft may send another `render` (or `padding`). This
|
||
happens when producing the answer needs an effect of the same helper: for example, motion blur
|
||
asking for the previous frame of a layer that also has a retime effect from the same helper.
|
||
The helper must serve it completely (with its own callbacks, nested up to 16 deep) and then
|
||
keep waiting. Images for callbacks are placed after the frame in the exchange file and freed
|
||
when the render ends.
|
||
|
||
### Errors and restarts
|
||
|
||
`error` replies are per request: the helper carries on. EffectCraft stops (kills) the helper
|
||
when it exits, closes stdout, sends a reply that doesn't parse or doesn't fit, or misses a
|
||
timeout, or stops reading its stdin for longer than the timeout. The request then fails. The next request starts a fresh process, runs `hello`, replays
|
||
earlier `load`s and recreates instances lazily: the helper sees a `render` for an `instance` it
|
||
doesn't know and creates it.
|
||
|
||
## Writing a helper
|
||
|
||
Any language works: read lines, write prefixed lines, read and write a file. Rust helpers can
|
||
use `effectcraft_helper_host::serve::serve` with a `Provider` that lists `EffectPlugin`s;
|
||
`crates/helper-host/tests/it/main.rs` is a complete example and the protocol test that runs
|
||
in `cargo xtask ci`. An unofficial OpenFX helper built this way lives in its own repository,
|
||
[ec-extension-ofx](https://github.com/glumbis/ec-extension-ofx).
|