Skip to content
All guides

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.

15 min readIntermediate

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

PieceWhere
Headersnative/sdk/include/premation_sdk/ — a C interface with a versioned ABI; premation_sdk.h includes all of it
Samplesnative/sdk/samples/ — ripple, rings, checkout and grade
Hostnative/engine/src/plugins/ — runs inside premation-engine
Test toolpremation-plugins — list, render and crash-check a plugin folder without the editor

The four samples cover deliberately different halves of the API.

SampleWhat it shows
rippleA CPU effect using smart render, 8, 16 and 32-bit pixels, and rows in parallel through the host
ringsA generator with per-instance sequence data and a button that reshuffles it
checkoutLayer Displace, which reads another layer, and Time Echo, which reads its own layer at other times
gradeA 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. 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. 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. 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" } ]
}
RuleWhy
The manifest is read and version-checked before the binary is loadedA 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 letterAn 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'sA 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':

SelectorWhat the plugin does
ABOUTPuts text in the return message
GLOBAL_SETUP / GLOBAL_SETDOWNSets the out flags and version, and owns any global data
PARAMS_SETUPCalls add_param for each parameter, in order
SEQUENCE_SETUP / RESETUP / FLATTEN / SETDOWNPer-instance state. FLATTEN hands back the pointer-free bytes the project stores
FRAME_SETUP / RENDER / FRAME_SETDOWNNon-smart render: read the input world, write the output world
SMART_PRE_RENDER / SMART_RENDERCheck out layers in pre-render; read the checkouts and write the output in render
USER_CHANGED_PARAMA button was pressed, or a supervised parameter changed. May set parameter values and sequence data
UPDATE_PARAMS_UIEnable, hide or rename parameters
GPU_DEVICE_SETUP / SETDOWN, SMART_RENDER_GPUSet 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.

ParameterIn the document
SLIDER, FLOAT_SLIDER, ANGLEA number (an angle in degrees)
POINT, POINT_3DNumbers, in layer pixels from the layer centre; the plugin receives layer pixels
COLORA colour, delivered as straight RGBA from 0 to 1
POPUPA choice, counted from 1
CHECKBOXA boolean
LAYERA reference to another layer, or none
PATHA mask of the layer, as its bezier at the frame's time
ARBITRARY_DATABytes stored in the project, written by the plugin
BUTTONAn action
GROUP_START / GROUP_ENDA twirl-down section

Parameter flags include CANNOT_ANIMATE, SUPERVISE, HIDDEN, START_COLLAPSED and DISABLED.

6. Pixels and time

TopicWhat to know
The worldFull-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
DepthAs 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 paralleliterate(count, fn) runs your function for each row on the host's worker pool, and each job is crash-guarded
TimeTimes are integer ticks, 705,600,000 to the second. current_time is the layer's time and comp_time is the composition's
CheckoutsIn 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.

RuleReason
Call WebGPU through device->procs, Dawn's procedure tableNever link a second copy of Dawn
Expect the call to run inside a crash guard and a validation and out-of-memory error scopeA 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 usedWhen 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 mathsAt 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 doesWhat happens
An access violation, divide by zero, stack overflow, illegal instruction or C++ exception in a render selectorThat 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 selectorsThe plugin fails to load and is listed as failed, with the reason
Returns an errorThe 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 faultThe 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 violation

This 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. 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. 2

    Or set PREMATION_PLUGIN_PATH to one or more folders, separated by semicolons.

    What happens The engine scans those as well.

  3. 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.

Read the docs