Skip to Content
Customising & extendingWriting a native plugin

Writing a native plugin

A Premation plugin is a shared library that exports one C function, plus a JSON manifest. Its shape follows the After Effects plugin API: a single entry point per effect that receives command selectors, declared parameters, pixel worlds, and the same ideas of smart render, sequence data and layer checkout. Beyond effects, a plugin can add menu commands, footage importers, output modules and dockable panels — After Effects’ AEGP, AEIO and output-module classes. If you have written an AE plugin, the vocabulary is the same.

This replaces the JavaScript/WGSL plugin system (plugin.json, main.js, panels, the motion.* host API), which was removed in 0.9.0. Nothing in that system carries over. See Plugins for what happens to a project that used it.

What you need

  • The SDK. Each release attaches premation-sdk-<platform>.zip for Windows x64, macOS arm64, macOS x64 and Linux x64 on GitHub Releases : the C99 headers with a versioned ABI, a CMake package, the premation-plugins test tool, the samples, and the pack and sign scripts. The same files are in the editor repository under native/sdk/. The SDK is not part of the app installer.
  • The headers — include premation_sdk.h and you have all of them (pr_effect.h, pr_params.h, pr_world.h, pr_scene.h, pr_gpu.h, pr_host.h, pr_command.h, pr_io.h).
  • A C or C++ toolchain that can build a shared library for your platform. With the released SDK, find_package(PremationSdk) and premation_add_plugin(name SOURCES … MANIFEST premation-plugin.json) build the library and lay the bundle out under <build>/plugins/<name>/. The editor repository itself builds with Clang on every platform (clang-cl on Windows), CMake presets and a vcpkg manifest.

The ABI is plain C on purpose: only integers, doubles, plain structs and pointers the host owns cross it. No C++ exceptions, no STL types, and no memory you must free on the other side — memory the host needs back is a host handle.

The bundle

A plugin ships as a bundle: a folder holding premation-plugin.json and the binary it names for each platform, plus an optional ui/ folder for panels and presets/ folder for presets.

    • premation-plugin.json
    • glow.dll
    • libglow.dylib
premation-plugin.json
{ "manifestVersion": 1, "id": "com.example.glow", "name": "Example Glow", "version": "1.2.0", "vendor": "Example", "sdk": { "major": 1, "minor": 2 }, "binary": { "windows-x64": "glow.dll", "macos-universal": "libglow.dylib" }, "effects": [ { "matchName": "com.example.glow", "name": "Glow", "category": "Example" } ] }
FieldRule
manifestVersion1 is the only version this engine reads
id1–100 characters from [A-Za-z0-9._-], starting with a letter. Use a reverse-DNS name
sdkThe SDK major and minor you built against. See versioning
binaryThe library’s file name inside the bundle, per platform key: windows-x64, macos-arm64, macos-x64, macos-universal, linux-x64, linux-arm64, or the plain windows / macos / linux. The engine takes the most specific key for the machine it runs on
effectsEach has a matchName, a display name and a category. May be empty or absent when the bundle contributes something else
effect matchNameEither the plugin id, or <id>.<name>. This keeps match names unique across plugins and apart from built-in effect types. Listing one twice is an error
presetsNamed starting points for the effects — see Presets
commands · importers · outputModules · panelsWhat the bundle adds besides effects — see Plugins that are not effects

The engine reads and validates the manifest before it loads the binary. A malformed manifest or entry, a missing binary for the platform, or an incompatible SDK leaves the plugin listed as Failed with the reason, and none of your code has run.

The plugin id and each effect’s match name must also match what the module reports at run time, and every command, importer and output module the manifest lists must have its entry point in the module, and the other way round.

The entry point

The module exports exactly one symbol:

PR_EXPORT const PrPluginInfo* PR_CALL PremationPluginInfo(void);

PrPluginInfo names the plugin id, the SDK version the module was built with, and the module’s effects — and, in its SDK 1.2 trailing fields, its commands, importers and output modules. Each effect has its own entry point:

PrErr PR_CALL EffectMain(PrCmd cmd, const PrInData* in_data, PrOutData* out_data, PrParamDef* const* params, PrWorld* output, void* extra);

Every selector and every host callback returns a PrErr. To fail a call, return non-zero and put an explanation in out_data->return_msg; the host records it against the layer and renders the layer as if the effect were off.

Selectors

PremationAfter EffectsWhat the plugin does
ABOUTPF_Cmd_ABOUTWrites text into return_msg
GLOBAL_SETUP / GLOBAL_SETDOWNsameSets out-flags, version and global_data; frees it
PARAMS_SETUPsameCalls add_param for each parameter, in order
SEQUENCE_SETUP / RESETUP / FLATTEN / SETDOWNsamePer-instance state. FLATTEN hands back the pointer-free bytes the project stores
FRAME_SETUP / RENDER / FRAME_SETDOWNsameNon-smart render: params[0]->world to output
SMART_PRE_RENDER / SMART_RENDERsamecheckout_layer in pre-render; checkout_layer_pixels and checkout_output in render
USER_CHANGED_PARAMsameReacts to a button, or to a param flagged SUPERVISE. May write parameter values and change sequence data
UPDATE_PARAMS_UIsameset_param_ui — enable, hide or rename a parameter
GPU_DEVICE_SETUP / SETDOWN, SMART_RENDER_GPUPF_Cmd_GPU_DEVICE_SETUP, PF_Cmd_SMART_RENDER_GPUThe GPU path, on the engine’s own device
DRAW_OVERLAY / OVERLAY_DRAG (1.1)PF_Cmd_EVENT (draw / drag)The viewer overlay — see Viewer overlays
LIST_ASSET_FILES (1.2)noneThe files beside a FILE param’s asset, so Collect Files carries them — see Assets

Out-flags you set in GLOBAL_SETUP say what the effect can do and what it reads: 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; since 1.1 USES_CAMERA, USES_LIGHTS, USES_LAYER_TRANSFORMS and CUSTOM_OVERLAY; since 1.2 SAMPLES_TIME, USES_SHUTTER, COMP_SPACE and USES_SCENE_DEPTH. A callback that needs a flag the effect did not declare answers PR_ERR_INVALID_CALLBACK.

The host may run the render selectors of different frames on different threads at once. Everything else — setup, sequence, parameter UI — is serialised per plugin module, and sequence data is read-only during a render.

Parameters

You declare parameters; the engine keyframes and evaluates them. At every render you receive each parameter’s value at the frame’s time. In the document each one is an ordinary effect property, which is why undo, keyframes, expressions, copy and paste, save and open all work for it without any plugin code.

ParameterWhat it is in the document
SLIDER, FLOAT_SLIDER, ANGLEA number (an angle in degrees)
POINT, POINT_3DNumbers, in layer pixels from the layer centre; you receive layer pixels
COLORA colour as the colour picker shows it; the plugin receives straight RGBA in linear light (1.2)
POPUPA choice, 1-based
CHECKBOXA boolean
LAYERA reference to another layer (empty means none)
PATHA mask of the layer — its Bézier at the frame’s time
ARBITRARY_DATABytes stored in the document
BUTTONAn action
GROUP_START / GROUP_ENDA twirl-down section
STRING (1.1)Text, edited in a multi-line field. Not keyframed
CURVE (1.1)A Curves-style point list. Not keyframed
GRADIENT (1.1)Gradient stops; their colours reach the plugin in linear light (1.2). Not keyframed
FILE (1.1)A project item, chosen with Choose… — so Collect Files, Relink and Remove Unused treat it like footage. The plugin reads it with get_asset_path / get_asset_bytes. A missing file never fails the frame: the plugin is told it is missing and the layer reports it

Flags: CANNOT_ANIMATE, SUPERVISE, HIDDEN, START_COLLAPSED, DISABLED.

Colours are linear (1.2). Every world a plugin reads and writes is in the linear working space, and so are its colours: a COLOR value and a GRADIENT stop’s r, g, b arrive decoded from the document’s sRGB-encoded values with the same transfer the built-in effects use, so a #808080 grey is 0.2159, not 0.5. A colour written back with set_param_value is taken as linear and encoded for the document. Declared defaults and preset colours are document values, as the picker shows them. SDK 1.0 and 1.1 plugins see the same linear values.

Pixels

  • Worlds are full-frame, premultiplied, in the linear working space, at the size of the effect chain’s buffer. layer_to_world maps layer pixels to world pixels; scale pixel-sized parameters (a blur radius) by pixel_scale_x and pixel_scale_y.
  • Depth follows After Effects’ down-conversion. In a 32-bit project, an effect flagged FLOAT_COLOR_AWARE gets 32-bit float worlds, a DEEP_COLOR_AWARE effect gets 16-bit, and any other effect gets 8-bit; 16-bit and 8-bit projects work the same way. The conversions are pure functions with fixed rounding, so the same frame always converts to the same bytes.
  • iterate(count, fn) runs fn once per row on the host’s worker pool, each job guarded against crashes.

Time and checkouts

Times are integer ticks of PR_TIME_SCALE — 705,600,000 per second — which is exact for every standard and NTSC frame rate. current_time is the layer’s time — its start offset, Time Stretch, reverse and freeze — and comp_time is the composition’s (before 1.2 both were the comp time). layer_start, comp_width, comp_height and layer_is_3d describe the layer and its composition.

In SMART_PRE_RENDER you may check out:

  • the effect’s own input (parameter index 0) at the current time — the chain’s buffer;
  • another layer, through a LAYER parameter;
  • your own layer at another time, if you set WIDE_TIME_INPUT.

The engine resolves each checkout from the frame itself. A frame stays a pure function of the document, so preview, export and a restarted engine render the same pixels.

The comp camera, lights and layer transforms (1.1)

Declare USES_CAMERA, USES_LIGHTS or USES_LAYER_TRANSFORMS, then:

CallbackGives
get_comp_camera(host, time, &camera)Camera-to-world and world-to-camera matrices, the projection to comp pixels, eye, zoom, field of view, film size, depth of field. With no camera layer, the comp’s default view
get_comp_lights(host, time, lights, capacity, &count)Every comp light: type, colour, intensity, position, direction, cone, falloff, shadows
get_layer_transform(host, param_index, time, m)A LAYER parameter’s world matrix

World space is After Effects’: pixels, +x right, +y down, +z away from the viewer, matrices column-major. They answer at the frame’s time and, since 1.2, at any time the effect checked out. An effect that reads the camera re-renders when the camera moves, and no other.

Other times and motion blur (1.2)

A trail, a plugin’s own motion blur or a simulation’s history reads the scene at times other than the frame’s. Here the effect says up front what it will read, and the engine evaluates exactly that:

  1. Declare SAMPLES_TIME (with SMART_RENDER) and the USES_* flags of what it reads.
  2. In SMART_PRE_RENDER, call checkout_time(host, time, what) for each layer time, with what any of PR_SAMPLE_PARAMS, PR_SAMPLE_CAMERA, PR_SAMPLE_LIGHTS and PR_SAMPLE_LAYER_TRANSFORMS (PR_SAMPLE_ALL) — at most 1,024 times per instance and frame.
  3. In the render, checkout_param(host, index, time, &def) and the camera, light and transform callbacks answer for the frame’s time and every checked-out time. Any other time answers PR_ERR_NOT_FOUND: skip that sample, never fail the frame.

The engine evaluates the requested values off the document — never a whole frame per time — and the frame stays a pure function of the document.

PrInData carries the composition’s shutter_angle, shutter_phase and motion_blur_samples, and motion_blur is 1 when the render, the composition and the layer all have motion blur on. An effect that declares USES_SHUTTER renders its own blur over the shutter, and the engine then draws its layer once rather than supersampling it through the effect.

Output beyond the layer (1.2)

A result_rect from SMART_PRE_RENDER that reaches past the layer — a glow’s halo, a flare off the layer’s edge — grows the layer’s effect buffer by that much, up to the engine’s texture limit. With COMP_SPACE, the effect’s output covers the whole composition frame, whatever the size of its layer: draw with PrInData.comp_to_world, which maps composition pixels to world pixels.

Assets

Since 1.2, a FILE parameter’s file is available beyond its path and bytes:

  • get_asset_image — the image decoded by the engine (PNG, JPEG, WebP, GIF, BMP, TIFF, OpenEXR, Radiance .hdr, binary .ppm / .pfm) as linear, premultiplied 32-bit float. get_asset_image_gpu gives the same image as a texture on the engine’s device, uploaded once per file and modification time.
  • get_asset_sibling_bytes(host, index, "textures/albedo.png", …) — a file beside the asset: a glTF’s .bin and textures, an .obj’s .mtl. No .., no absolute paths, no symlinks out of the asset’s folder.
  • LIST_ASSET_FILES — at Collect Files, the engine asks the effect which files beside its asset it needs (add_file once per file), and collects them with it. Opening the collected project lays them out under their names again.

Scene depth (1.2)

After Effects gives a plugin no depth buffer; Premation does, so a model can pass behind the comp’s own 3D layers (“composite with scene depth”). Declare USES_SCENE_DEPTH, then call checkout_scene_depth_gpu in SMART_RENDER_GPU (an r32float texture; read it with textureLoad) or checkout_scene_depth on the CPU path. Each pixel holds the camera-space depth of the nearest 3D layer other than the effect’s own, in comp pixels, or +inf where there is none. On a render with no GPU the call answers PR_ERR_UNSUPPORTED: render unoccluded. For motion blur, add PR_SAMPLE_SCENE_DEPTH to a time’s checkout_time bits and read it with checkout_scene_depth_gpu_at / checkout_scene_depth_at.

Presets

Since 1.2 a bundle ships named starting points for its effects, After Effects’ .ffx, in the manifest or as presets/*.json:

"presets": [ { "name": "Warm Fountain", "category": "Fountains", "effect": "com.example.particles", "values": { "p2": 600, "p8": "#ffb347", "p5": 2, "p1": [0, -100] } } ]

effect defaults to the plugin’s first effect; values are by parameter key. A key the effect lacks, or a value of the wrong kind, is dropped; a preset without a name, or naming an effect the bundle does not list, is skipped — never a failed plugin. The editor lists presets under their effect in Effects & Presets, and applying one is one undo entry.

Viewer overlays

Declare CUSTOM_OVERLAY (1.1). In DRAW_OVERLAY, call overlay_line, overlay_path and overlay_handle in layer pixels; in OVERLAY_DRAG (begin, move, end) write parameters with set_param_value. A whole drag is one undo entry, and a plugin that crashes while drawing draws nothing — the frame still renders.

For a handle on a 3D point (1.2), read the camera in the overlay selectors, project the point yourself, and call overlay_set_space(host, PR_OVERLAY_SPACE_COMP) to draw in composition pixels; a drag then reports the pointer in composition pixels too. The model3d sample moves its model in the camera’s view plane this way.

Panels

An effect panel (1.1). A bundle may ship ui/index.html, with its scripts, styles, images and fonts beside it in ui/. The effect card then shows Open Panel, and the editor shows the page in a sandboxed frame: served from the bundle’s ui/ folder only, scripts allowed, no Node, no storage, no popups and no network. The page talks to the editor with window.parent.postMessage messages — set one or several parameters (several are one undo entry), write arbitrary data, press a hidden button with a payload, ask for a rendered preview of the layer, and forward undo / redo — and receives the effect’s state after every change.

A dockable panel (1.2) has no effect behind it: a manifest panels entry { "id", "name", "page" } shows ui/<page> in the same sandbox. It opens from Plugins ▸ Panels, docks like any panel and pops out into its own window. Its messages are engine API queries and edit commands (one undo entry, or several as one), and it receives the active composition, the selected layers and the time after every document, selection or time change.

Plugins that are not effects

After EffectsPremationHeaderSample
CommandAEGP menu commandmanifest commands, PrCommandEntrypr_command.hdistribute
ImporterAEIO inputmanifest importers, PrImporterEntrypr_io.hppmio
Output moduleAEIO outputmanifest outputModules, PrOutputModuleEntrypr_io.hppmseq
PanelAEGP panelmanifest panels (no code: a page)—statspanel

Commands. A manifest entry { "id", "name", "menu" } puts the command in the Plugins menu (in a submenu named by menu, or by the plugin) and the command palette. PR_COMMAND_RUN receives the active composition, the selected layers, the playhead and the frame rate, and reads and edits the document through the engine API as JSON (engine_query, engine_command) — the same API the editor speaks. Only edit commands are allowed, and one run is one undo entry: if the command returns an error, any edit fails or the plugin crashes, all of it rolls back.

Importers. A manifest entry lists file extensions; those files then import and render through the plugin (PR_IMPORT_PROBE, OPEN, READ_FRAME, READ_AUDIO, CLOSE). The footage plays, exports, and is what the tracker and the other analysis jobs read; its sound reaches the mixer and the export.

Output modules. A manifest entry with an extension and options (popup, number and checkbox controls) becomes an export format in the Export dialog’s Plugins category and the Render Queue (PR_OUTPUT_BEGIN, WRITE_FRAME, WRITE_AUDIO, END). The export job runs the module in its own process, and the module writes its files itself.

Sequence data

Per-instance state lives in the document, not only in the process. When an effect is added, the host runs SEQUENCE_SETUP, then FLATTEN, then SETDOWN, and stores the flat bytes with the effect in the same undo entry. A render rebuilds the instance from those bytes with RESETUP whenever they change. A button press (USER_CHANGED_PARAM) produces new bytes and any parameter writes as one undo entry, so undo restores the old bytes and the old bytes render the old picture.

The rings sample uses this to keep a random palette: shuffle, undo and redo all render exactly.

The GPU path

A GPU_RENDER effect records WebGPU commands into the host’s command encoder, on the engine’s own Dawn device. You receive the chain’s texture as it is and write to a texture the host owns; nothing is copied or read back.

  • Call WebGPU through device->procs, the DawnProcTable the host hands you. Never link a second copy of Dawn.
  • The call runs inside a crash guard and a validation and out-of-memory error scope. A GPU error drops your 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 effect uses its CPU path on that device. When the device changes, the host sends GPU_DEVICE_SETDOWN for the old one first.
  • Keep the two paths the same maths. At 16 bits per channel they can still differ on over-range values: the CPU world is integer and clips, the GPU buffer is half-float and does not.

Versioning

The SDK is versioned major.minor. The plugin’s major must equal the host’s major, and the plugin’s minor must be no greater than the host’s — a plugin whose manifest says "minor": 2 fails on an older engine with “needs SDK 1.2”. Minor versions only add — new selectors, parameter types, host callbacks (appended to the end of the host suite), trailing struct fields and new flags. Every struct starts with struct_size; a field past the size the other side reported does not exist, so check it before calling a newer callback. Enum values are forever: a removed value is never reused. The SDK in this release is 1.2, and 1.0 and 1.1 plugins load and render unchanged.

What the host guarantees

Every selector call runs behind a crash guard (structured exception handling on Windows; signal handlers on an alternate stack on macOS) and a crash journal.

What your plugin doesWhat happens
Faults or throws in a render selectorThat instance is disabled; its layer renders as if the effect were off, the failure is on the layer’s error list, and everything else continues
Faults in setup, params 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 the effect; the instance stays enabled
Faults in a command, importer or output moduleThe command rolls back and is off for the session; the importer is off for the session; the export job fails. The engine carries on
Hangs longer than the watchdog (10 s by default)The plugin is quarantined and the engine ends; the supervisor restarts it and replays the document
Calls abort() or exit(), or corrupts the heap without faultingThe engine dies. The next start quarantines the plugin

A quarantined plugin is listed but not loaded. On macOS a stack overflow is reported as an access violation.

The samples

The samples live in the SDK’s samples/ folder (native/sdk/samples/ in the repository), one per idea:

SampleShows
rippleA CPU effect with smart render at 8, 16 and 32 bits, threaded rows
ringsA generator with sequence data, a button (USER_CHANGED_PARAM), a viewer overlay and an effect panel
checkoutLayer checkouts: Layer Displace reads another layer, Time Echo reads its own layer at other times
gradeA GPU effect (SMART_RENDER_GPU) with a CPU fallback
grademapThe 1.1 parameter types — string, curve, gradient and a file (a .cube LUT)
particlesThe comp camera and lights (1.1); trails and its own motion blur through other times, comp space and presets (1.2)
model3dAn Element 3D class renderer: a glTF model through the comp camera and lights, on the GPU and the CPU, with motion blur, scene-depth occlusion, a 3D handle and a material panel
silkwavesA looping generator, GPU with a CPU path
distributeA command (Distribute Layers)
ppmioAn importer for .ppm / .pfm, including moving footage and sound
ppmseqAn output module writing a .ppm sequence, with a .wav beside it
statspanelA dockable panel and a command

Most effect samples have a Debug ▸ Fault popup that injects each class of fault, which the host’s tests use to prove every row of the table above. Building the samples lays each out as an installable bundle, at <build>/plugins/<name>/, in exactly the folder layout the engine loads.

Testing without the editor

premation-plugins runs the plugin host on its own. It ships in the SDK, not in the app installer.

# list what a plugin folder loads to premation-plugins --plugins <build>/plugins list # render an effect on a test pattern at 32 bits per channel premation-plugins --plugins <build>/plugins --bits 32 render com.premation.samples.ripple # inject a fault and prove it is contained (2 = access violation) premation-plugins --plugins <build>/plugins crash-check com.premation.samples.ripple 2
OptionMeaning
--plugins <dir>A bundle folder, or a folder of bundles. Repeatable. Defaults to PREMATION_PLUGIN_PATH
--bits 8|16|32The project bit depth to render at (default 8)
--size WxHThe test pattern’s size (default 64x48)
--journal <file>The crash journal; plugins that died last time are quarantined
--watchdog <ms>A call longer than this is a hang (default 10000)
--enable <pluginId>Enable a plugin before the command, clearing a quarantine

render prints ok followed by a hash of the output pixels, or fail and the message. crash-check renders one instance with the fault and expects it contained, then renders a second instance and expects pixels. The exit code is 0 on success, 1 when the check failed and 2 for a usage error. The test host has no GPU depth pass, so scene-depth checkouts answer PR_ERR_UNSUPPORTED there.

To render a whole scene with plugin effects outside the editor, the engine’s premation-render tool takes --plugins <dir>, and --plugin-gpu 0 forces the CPU path.

Installing what you built

Copy the bundle into the plugins folder and press Rescan — see Plugins — or point PREMATION_PLUGIN_PATH at the folder while you develop. To hand a plugin to other people, pack and sign it, and publish it to the store if you like: see Publishing a plugin.

Last updated on