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 model. Projects that used it still open; their plugin effects and layers are kept in the project (an effect passes the picture through), and one notice names the missing plugins. Native plugins are the only plugin system now.
Where it stands in 1.0
SDK 1.2 shipped with Premation 1.0. Plugin effects are listed in the Effects browser's Plugins folder, with any presets the bundle ships. A plugin can also be something other than an effect — a menu command, an importer, an export format or a panel — the way After Effects splits AEGP and AEIO plugins from effects. The installers' Dashboard ▸ Plugins has a store with Store, Installed and Publish sections, and a .pplugin file installs with a double-click. SDK 1.0 and 1.1 plugins still load unchanged.
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. pr_command.h and pr_io.h are the SDK 1.2 headers for commands, importers and export formats |
| Samples | native/sdk/samples/ — twelve, listed below |
| Host | native/engine/src/plugins/ — runs inside premation-engine |
| Test tool | premation-plugins — list, render and crash-check a plugin folder without the editor |
| Reference | docs/PLUGIN_SDK.md in the repository, with an After Effects mapping for every part, and docs/PLUGIN_STORE.md for packaging and signing |
The samples cover deliberately different parts 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, a button that reshuffles it, an on-canvas handle and a panel |
| 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 |
| grademap | The SDK 1.1 parameter types: a tone curve, a gradient and a .cube LUT file |
| particles | The comp camera, lights and layer transforms; trails and its own motion blur from other times; presets |
| model3d | The Element 3D class: a glTF model drawn through the comp camera, hidden behind the scene's own 3D layers, with a handle that moves it in 3D |
| silkwaves | A looping GPU and CPU generator |
| distribute | A menu command that edits the project through the engine API |
| ppmio | An importer for formats the engine cannot read: PPM / PFM footage, and a sound-only format |
| ppmseq | An export format, listed in the Export dialog and the Render Queue |
| statspanel | A dockable panel with a command behind it |
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) |
| SDK 1.2 | checkout_time and checkout_param give parameter values, the comp camera, lights and layer transforms at other times (PR_OUT_FLAG_SAMPLES_TIME); checkout_scene_depth gives the depth of the comp's own 3D layers (PR_OUT_FLAG_USES_SCENE_DEPTH); the shutter fields let an effect render its own motion blur; and the result can extend past the layer, in layer or composition space |
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. Plugins that are not effects
Since SDK 1.2 a bundle can list commands, importers, export formats and panels in its manifest, with or without effects. They map onto the After Effects plugin classes that are not effects.
| Kind | After Effects | What Premation does with it |
|---|---|---|
| Command (pr_command.h) | AEGP menu command | Listed in the Plugins menu and the command palette. It reads and edits the document through the engine API as JSON; one run is one undo entry, and if it fails or crashes every edit rolls back |
| Importer (pr_io.h) | AEIO input | Files with its extensions import and render through the plugin, sound included, and the analysis jobs read the frames through it too |
| Export format (pr_io.h) | Output module | Listed in the Export dialog's Plugins category and the Render Queue, with its declared options as controls; it runs in the export job's own process |
| Panel (manifest) | AEGP panel | An HTML page from the bundle, in a sandbox with no Node, storage or network. It docks or pops out into its own window and edits the project only through engine commands, each one an undo entry |
A bundle can also ship presets (listed under its effect in the Effects browser) and draw on-canvas handles that drag its parameters. The distribute, ppmio, ppmseq and statspanel samples show one of each kind.
10. 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.
11. 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. In the installers, Dashboard ▸ Plugins ▸ Installed ▸ Open plugins folder opens it.
What happens The desktop app passes that folder to the engine as native-plugins. 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, along with a machine-wide Plug-ins folder that a vendor's installer can write to.
- 3
Click Rescan on the Installed page, or restart Premation.
What happens A new bundle loads without a restart. The engine lists every plugin as loaded, disabled, failed or quarantined; a failed plugin says why and none of its code has run. An update or removal of a plugin that is already loaded applies at the next start.
To hand a plugin to someone else, node scripts/pack-plugin.mjs <bundle> --key <key.json> packs it into a signed .pplugin. They install it with Install from file or a double-click, and the dialog names the publisher, what the plugin adds and how far its signature can be trusted before anything is installed. The Publish section of Dashboard ▸ Plugins lists it in the store, where every third-party plugin is free; a new public plugin needs a verified publisher.
The plugins folder works the same in the installers and in a build made from the repository; the store and publishing need the installers' account. A project that uses a plugin that is missing, disabled or locked keeps the effect's data, passes the layer through and says why, ready for when the plugin comes back.
What is not here
Settled decisions, so you do not plan around them. A plugin cannot add a custom layer type, and there is no scripting language like ExtendScript: automation is a C++ command or a panel that sends engine commands. After Effects, OpenFX and FxPlug plugins do not load. The store only opened with 1.0, so its catalogue is young. The JavaScript plugin permissions model is gone with the system it belonged to, and crash guards are not a security sandbox. 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.