Guide 02
Build a native plugin
What a Premation plugin is now, how to build the samples, how an effect is put together against the C SDK, how to test it, and how the engine treats it when it crashes.
A plugin is a shared library — a .dll on Windows, a .dylib on macOS — plus a small JSON manifest. The engine, the C++ process that draws every Premation frame, loads it and calls it. The API is modelled on After Effects' effect API, so if you have written an After Effects effect the selectors, the parameter types and the smart-render checkouts will be familiar.
This replaces the JavaScript plugin system
The JavaScript and WGSL plugin system was removed in 0.9.0, along with its permissions, panels, registry and publishing screen. Projects that used it still open; their plugin effects and layers are removed as one undoable step, with a notice. Native plugins are the only plugin system now.
Where it stands today
Native plugins cover effects and generators only. The editor has no plugin manager yet, and its Effects browser lists the built-in effects only, so there is no menu item that adds a plugin effect to a layer. The engine accepts a plugin effect by its match name, renders it in the viewport and in export, and reports every plugin it found. That is enough to build, test and ship a plugin's rendering; it is not yet a finished user experience, and this guide does not pretend otherwise.
1. What is in the SDK
| Piece | Where |
|---|---|
| Headers | native/sdk/include/premation_sdk/ — a C interface with a versioned ABI; premation_sdk.h includes all of it |
| Samples | native/sdk/samples/ — ripple, rings, checkout and grade |
| Host | native/engine/src/plugins/ — runs inside premation-engine |
| Test tool | premation-plugins — list, render and crash-check a plugin folder without the editor |
The four samples cover deliberately different halves of the API.
| Sample | What it shows |
|---|---|
| ripple | A CPU effect using smart render, 8, 16 and 32-bit pixels, and rows in parallel through the host |
| rings | A generator with per-instance sequence data and a button that reshuffles it |
| checkout | Layer Displace, which reads another layer, and Time Echo, which reads its own layer at other times |
| grade | A GPU effect on the engine's device, with a CPU fallback |
2. Build and run the samples
You need the repository and the native toolchain described in native/README.md: clang (clang-cl on Windows), CMake, Ninja and vcpkg. The samples build with the engine, from the native directory.
# Windows
cmake --preset windows-clang-cl-engine
cmake --build --preset windows-clang-cl-engine
# macOS
cmake --preset macos-clang-engine
cmake --build --preset macos-clang-engine- 1
Look in the build tree for a plugins folder.
What happens Each sample is laid out as an installable bundle: plugins/<name>/ holding premation-plugin.json and the binary it names. The grade sample and the GPU half of checkout need Dawn's headers, so they appear only in the engine build.
- 2
Run premation-plugins --plugins <that folder> list.
What happens Every plugin the engine finds is listed as loaded, disabled, failed (with the reason) or quarantined.
- 3
Run premation-plugins --plugins <that folder> --bits 32 render com.premation.samples.ripple.
What happens Renders the effect at 32-bit float without an editor. Use --bits 8 or 16 for the other depths.
3. The manifest
{
"manifestVersion": 1,
"id": "com.example.glow",
"name": "Example Glow",
"version": "1.2.0",
"vendor": "Example",
"sdk": { "major": 1, "minor": 0 },
"binary": { "windows": "glow.dll", "macos": "libglow.dylib", "linux": "libglow.so" },
"effects": [ { "matchName": "com.example.glow", "name": "Glow", "category": "Example" } ]
}| Rule | Why |
|---|---|
| The manifest is read and version-checked before the binary is loaded | A malformed manifest, a missing binary or an incompatible SDK leaves the plugin listed as failed with the reason, and none of its code has run |
| The id is 1 to 100 characters from A-Z, a-z, 0-9, dot, underscore and hyphen, and starts with a letter | An effect's match name is the id, or the id followed by a dot and a name, which keeps match names unique across plugins and apart from the built-in effect types |
| The plugin's SDK major must equal the host's, and its minor must not exceed the host's | A plugin built against a newer SDK than the engine offers is refused up front |
4. The entry point
A module exports one symbol, PremationPluginInfo, which lists its effects. Each effect has its own entry function, which receives a selector and dispatches on it. This is ripple's, trimmed to its shape; the prs:: helpers are the samples' own convenience layer in samples/common, not part of the SDK.
#include <premation_sdk/premation_sdk.h>
PrErr PR_CALL ripple_main(PrCmd cmd, const PrInData* in, PrOutData* out,
PrParamDef* const* params, PrWorld*, void*) {
switch (cmd) {
case PR_CMD_ABOUT:
prs::message(out, "Ripple 1.0 — Premation SDK sample (CPU effect, 8/16/32 bpc).");
return PR_ERR_NONE;
case PR_CMD_GLOBAL_SETUP:
out->my_version = PR_VERSION(1, 0, 0);
out->out_flags = PR_OUT_FLAG_DEEP_COLOR_AWARE | PR_OUT_FLAG_FLOAT_COLOR_AWARE |
PR_OUT_FLAG_SMART_RENDER | PR_OUT_FLAG_THREADED_RENDER;
return PR_ERR_NONE;
case PR_CMD_PARAMS_SETUP: return params_setup(in);
case PR_CMD_SMART_PRE_RENDER: return in->host->checkout_layer(in->host_ref, 0, 0, in->current_time, nullptr);
case PR_CMD_SMART_RENDER: return smart_render(in, out, params);
default: return PR_ERR_NONE;
}
}
const PrEffectEntry kEffects[] = {{"com.premation.samples.ripple", &ripple_main}};
const PrPluginInfo kInfo = {sizeof(PrPluginInfo), PR_SDK_VERSION,
"com.premation.samples.ripple", 1, kEffects};
extern "C" PR_EXPORT const PrPluginInfo* PR_CALL PremationPluginInfo(void) { return &kInfo; }The plugin id and the match names in that table must equal the manifest's. The selectors map one to one onto After Effects':
| Selector | What the plugin does |
|---|---|
| ABOUT | Puts text in the return message |
| GLOBAL_SETUP / GLOBAL_SETDOWN | Sets the out flags and version, and owns any global data |
| PARAMS_SETUP | Calls add_param for each parameter, in order |
| SEQUENCE_SETUP / RESETUP / FLATTEN / SETDOWN | Per-instance state. FLATTEN hands back the pointer-free bytes the project stores |
| FRAME_SETUP / RENDER / FRAME_SETDOWN | Non-smart render: read the input world, write the output world |
| SMART_PRE_RENDER / SMART_RENDER | Check out layers in pre-render; read the checkouts and write the output in render |
| USER_CHANGED_PARAM | A button was pressed, or a supervised parameter changed. May set parameter values and sequence data |
| UPDATE_PARAMS_UI | Enable, hide or rename parameters |
| GPU_DEVICE_SETUP / SETDOWN, SMART_RENDER_GPU | Set up and draw on the engine's Dawn device |
Out flags declare what the effect can do: DEEP_COLOR_AWARE (16-bit), FLOAT_COLOR_AWARE (32-bit float), SMART_RENDER, GPU_RENDER, SEQUENCE_DATA, GENERATOR, WIDE_TIME_INPUT, NON_PARAM_VARY, SEND_UPDATE_PARAMS_UI and THREADED_RENDER.
5. Parameters
Parameters are declared, and the engine keyframes and evaluates them. At every render the plugin receives each parameter's value at that frame's time; it never interpolates anything itself. In the project each parameter is an ordinary effect property, so undo, keyframes, expressions, copy and paste, save and open, and change events work as they do for a built-in effect.
| Parameter | In the document |
|---|---|
| SLIDER, FLOAT_SLIDER, ANGLE | A number (an angle in degrees) |
| POINT, POINT_3D | Numbers, in layer pixels from the layer centre; the plugin receives layer pixels |
| COLOR | A colour, delivered as straight RGBA from 0 to 1 |
| POPUP | A choice, counted from 1 |
| CHECKBOX | A boolean |
| LAYER | A reference to another layer, or none |
| PATH | A mask of the layer, as its bezier at the frame's time |
| ARBITRARY_DATA | Bytes stored in the project, written by the plugin |
| BUTTON | An action |
| GROUP_START / GROUP_END | A twirl-down section |
Parameter flags include CANNOT_ANIMATE, SUPERVISE, HIDDEN, START_COLLAPSED and DISABLED.
6. Pixels and time
| Topic | What to know |
|---|---|
| The world | Full-frame, premultiplied, in the linear working space, at the size of the effect chain's buffer. in_data->layer_to_world maps layer pixels to world pixels; scale pixel-sized parameters by pixel_scale_x and pixel_scale_y |
| Depth | As in After Effects, a 32-bit project gives FLOAT_COLOR_AWARE effects a 32-bit float world, DEEP_COLOR_AWARE effects a 16-bit one, and every other effect an 8-bit one. The conversions are pure functions, so the same frame always converts to the same bytes |
| Rows in parallel | iterate(count, fn) runs your function for each row on the host's worker pool, and each job is crash-guarded |
| Time | Times are integer ticks, 705,600,000 to the second. current_time is the layer's time and comp_time is the composition's |
| Checkouts | In SMART_PRE_RENDER an effect may check out its own input, another layer named by a LAYER parameter, or its own layer at another time (with WIDE_TIME_INPUT) |
Checkouts at another time are resolved from the frame itself, so a frame stays a pure function of the document: preview, export and a restarted engine render the same pixels.
7. Sequence data and buttons
Per-instance state lives in the project, not only in the process. Adding the effect runs SEQUENCE_SETUP, FLATTEN and SETDOWN and stores the flat bytes with the effect in the same undo entry. A render rebuilds the instance from those bytes whenever they change. A button press produces new bytes and any parameter writes as one undo entry, so undo brings back the old bytes and the old bytes render the old picture. This is how the rings sample keeps its palette: shuffle, undo and redo all render exactly.
8. A GPU effect
An effect with GPU_RENDER records WebGPU commands into the host's command encoder, on the engine's own Dawn device. The input is the effect chain's texture as it is, the output is a texture the host owns, and nothing is copied or read back.
| Rule | Reason |
|---|---|
| Call WebGPU through device->procs, Dawn's procedure table | Never link a second copy of Dawn |
| Expect the call to run inside a crash guard and a validation and out-of-memory error scope | A GPU error drops the command buffer unsubmitted, and the effect renders on its CPU path for that frame |
| GPU_DEVICE_SETUP runs once per device; if it fails the CPU path is used | When the engine's device changes, the host sends GPU_DEVICE_SETDOWN for the old one first and sets up again on the next frame |
| Keep the GPU and CPU paths the same maths | At 16-bit they can still differ on over-range values: the CPU world is integer and clips, the GPU buffer is half float and does not |
premation-render --plugins <dir> renders a scene that contains plugin effects outside the editor, and --plugin-gpu 0 forces the CPU path so you can compare the two.
9. What happens when it crashes
Every selector call runs through a guard and a crash journal. Each sample has a Debug ▸ Fault popup that injects each class of fault, and the host tests use it to prove every row below in a child process.
| What the plugin does | What happens |
|---|---|
| An access violation, divide by zero, stack overflow, illegal instruction or C++ exception in a render selector | That instance is disabled: the layer renders as if the effect were off, the failure is recorded against the layer, and the frame and every other layer continue |
| The same in setup, parameter or sequence selectors | The plugin fails to load and is listed as failed, with the reason |
| Returns an error | The call fails and the frame renders without it; the instance stays enabled |
| Hangs for longer than the watchdog (10 seconds by default) | The plugin is quarantined in the journal and the engine ends; the supervisor restarts it and replays the document |
| abort(), exit(), or heap corruption that does not fault | The engine dies. The journal survives, and the next start quarantines the plugin |
A quarantined plugin is listed but not loaded. Re-enabling it through the engine API retries it and clears the quarantine. On macOS a stack overflow is reported as an access violation, because both arrive as SIGSEGV.
premation-plugins --plugins <dir> crash-check com.premation.samples.ripple 2 # 2 = access violationThis is fault containment, not a sandbox
A native plugin runs inside the engine process with your user's privileges. The guards stop a bug from taking down a frame or the editor; they do not stop hostile code. Install only plugins you trust.
10. Install a bundle
- 1
Copy the bundle folder — premation-plugin.json and its binary together — into the plugins folder inside the app's user-data directory.
What happens The desktop app passes that folder to the engine as native-plugins. The engine scans it at start, and a folder may be a bundle itself or a folder of bundles. Load order is sorted, so it is deterministic.
- 2
Or set PREMATION_PLUGIN_PATH to one or more folders, separated by semicolons.
What happens The engine scans those as well.
- 3
Restart Premation.
What happens The engine lists every plugin as loaded, disabled, failed or quarantined. A failed plugin says why and none of its code has run.
Plugins work the same in the installers and in a build made from the repository: the engine reads a local folder, and no account or registry is involved. A project that contains a native plugin's effect keeps that effect's data if the plugin is missing when the project opens, ready for when it comes back.
What is not here
Settled decisions, so you do not plan around them. There are no plugin panels, custom layer types or scripting API: a native plugin is an effect or a generator. There is no registry, marketplace or publishing screen, and the JavaScript plugin permissions model is gone with the system it belonged to. The editor lists no plugin effects in its Effects browser yet. If you need any of those today, After Effects has them.
Try it
Free and open source, and everything above is in the build you can download right now.