Writing an OpenMfx plugin using the C API#

Note

This page needs a bit of clean up.

1. Getting started

Create a shared library project with your favorite C tool chain. Call your main source file for instance mirror_plugin.c, and copy along the OpenMfx include files to your project.

When using Visual Studio, don't forget to set the binary type to dynamic library (.dll) instead of exe. When using CMake, use add_library(... SHARED ...) to create a dynamic library. Set the file extension of the output file to .ofx rather than .dll or .so using set_target_properties(mirror_plugin PROPERTIES SUFFIX ".ofx") (there is a similar option with Visual Studio). When using gcc, use the -shared flag.

2. Basic structure

We will write everything in mirror_plugin.c for the sake of clarity, but for a more substantial use case you might want to split up your code into several files at some point.

We begin with a generic OpenFX skeleton:

{mirror_plugin.c 1}
#include "ofxCore.h"
#include "ofxMeshEffect.h"

{Global variables, 4}

{Action handlers, 3}

static void setHost(OfxHost *host) {
	{Fetch host's function suites, 5}
}

static OfxStatus mainEntry(const char *action,
                           const void *handle,
                           OfxPropertySetHandle inArgs,
                           OfxPropertySetHandle outArgs) {
   {Main entry, 2}
}

OfxExport int OfxGetNumberOfPlugins(void) {
    return 1;
}

OfxExport OfxPlugin *OfxGetPlugin(int nth) {
    static OfxPlugin plugin = {
        /* pluginApi */          kOfxMeshEffectPluginApi,
        /* apiVersion */         kOfxMeshEffectPluginApiVersion,
        /* pluginIdentifier */   "MirrorPlugin",
        /* pluginVersionMajor */ 1,
        /* pluginVersionMinor */ 0,
        /* setHost */            setHost,
        /* mainEntry */          mainEntry
    };
    return &plugin;
}

Note that even though it is a generic OpenFX boilerplate, we need to include ofxMeshEffect.h for the constants kOfxMeshEffectPluginApi and kOfxMeshEffectPluginApiVersion.

This is a simple example in which we only define one plug-in, but keep in mind that a single OpenFX plug-in binary° may contain several plug-ins° at once. This is why the OpenFX standard requires a binary to export the symbols OfxGetNumberOfPlugins and OfxGetPlugin.

NB: In `OfxGetPlugin`, you do not have to check that `nth` is a valid index. It is ensured by the standard.

NB: The functions that are not exported are marked `static` to mean that they are private.

3. Main Entry

The mainEntry is called by the host whenever it needs the plug-in to do something. The something is specified by the first argument, the action. Depending on the action, the remaining arguments will have different meanings.

So the first thing to do in this main entry point is to check which action is requested. Since all actions are different, the behavior for each action will be outsourced in a dedicated function. In the end, the mainEntry just contains a long series of conditions such as:

{Main entry 2}
if (0 == strcmp(action, kOfxActionLoad)) {
    return load(handle, inArgs, outArgs);
}
if (0 == strcmp(action, kOfxActionUnload)) {
    return unload(handle, inArgs, outArgs);
}
{Main entry (cont'd)}

Used in section 1

The load(), unload(), etc. functions must be defined above. It is important to check the reference for each action, because some of them will ignore some arguments, and most of them will require handle to be casted to a more specific type. For instance, the describe action requires it to be a OfxMeshEffectHandle.

4. Actions

For our example, we need to implement the following actions:

  • kOfxActionLoad
  • kOfxActionUnload
  • kOfxActionDescribe
  • kOfxActionCreateInstance
  • kOfxActionDestroyInstance
  • kOfxMeshEffectActionCook

Load and unload actions are called respectively before and after everything else. They don't require any argument.

The describe action is called by the host once to get the overall structure of the plug-in. This is where the plug-in tells what parameters it has, how many inputs it requires, how many outputs it provides, with which attributes, etc. This is fixed and cannot be changed dynamically.

NB: If you feel that you need to change the number of parameters sometimes, it may be that you should declare several plug-ins in the binary.

The create and destroy actions are used before and after processing some geometry. You can think of "instances" as nodes. The describe action gets the node type info, and the create/destroy instance actually create and destroy a node. This node may store runtime data that should not be shared with other instances. There can be several instances of the same effect defined, for instance if several objects have a mirror modifier with different settings.

The last action, the cook, is introduced by the Mesh Effect API, while the previous actions were part of the more generic OpenFX standard. This action is where the actual, potentially computing intensive, mesh processing occurs.

So, at this point, our mainEntry functions relatives has become:

{Action handlers 3}
static OfxStatus load() {
    {Check that host supports mesh effects, 6}
    return kOfxStatReplyDefault;
}

static OfxStatus unload() {
    return kOfxStatReplyDefault;
    }
}

static OfxStatus describe(OfxMeshEffectHandle descriptor) {
    {Declare effect input, 8}
    {Declare effect output, 10}
    {Declare effect parameters, 11}
    return kOfxStatReplyDefault;
}

static OfxStatus createInstance(OfxMeshEffectHandle instance) {
    return kOfxStatReplyDefault;
}

static OfxStatus destroyInstance(OfxMeshEffectHandle instance) {
    return kOfxStatReplyDefault;
}

static OfxStatus cook(OfxMeshEffectHandle instance) {
    return kOfxStatReplyDefault;
}

static OfxStatus mainEntry(const char *action,
                           const void *handle,
                           OfxPropertySetHandle inArgs,
                           OfxPropertySetHandle outArgs) {
    if (0 == strcmp(action, kOfxActionLoad)) {
        return load();
    }
    if (0 == strcmp(action, kOfxActionUnload)) {
        return unload();
    }
    if (0 == strcmp(action, kOfxActionDescribe)) {
        return describe((OfxMeshEffectHandle)handle);
    }
    if (0 == strcmp(action, kOfxActionCreateInstance)) {
        return createInstance((OfxMeshEffectHandle)handle);
    }
    if (0 == strcmp(action, kOfxActionDestroyInstance)) {
        return destroyInstance((OfxMeshEffectHandle)handle);
    }
    if (0 == strcmp(action, kOfxMeshEffectActionCook)) {
        return cook((OfxMeshEffectHandle)handle);
    }
    return kOfxStatReplyDefault; // this means "unhandled action"
}

Used in section 1

5. Suites

We must now fill all our action related functions, for instance the describe() function to define an input and an "axis" parameter for the user to chose along which axis the mirror is. But what does "defining a parameter" means?

While actions are queries from the host to the plug-in, plug-in will ask in return the host to do things. OpenFX provides a mechanism for the host to hand functions to the plug-ins through structures called suites.

A suite is a set of functions defined by the host and called from plug-ins, grouped by topic. There is a suite for properties defined in ofxProperty.h, one for parameters defined in ofxParam.h and one specific to mesh effects in ofxMeshEffect.h. A plug-in will ask the host only for the suites it uses. So for instance our plug-in will never ask for the Image Effect suite, used by 2D compositing plug-ins.

The plug-in asks the host for suites using the fetchSuite function of an OfxHost variable. This is where the setHost function we defined at the beginning gets useful. We also need a global variable holding the returned suite.

{Global variables 4}
const OfxMeshEffectSuiteV1 *meshEffectSuite;
const OfxPropertySuiteV1 *propertySuite;

Used in section 1

{Fetch host's function suites 5}
meshEffectSuite = host->fetchSuite(
    host->host, // host properties, might be useful to the host's internals
    kOfxMeshEffectSuite, // name of the suite we want to fetch
    1 // version of the suite
);

Used in section 1

We are ensured by the standard that setHost is called before any action, but we need to check that the returned suite is valid. Indeed, a host is not required to implement all the suites. For instance, Natron does not implement the Mesh Effect suite, while on the contrary Blender does not implement the Image Effect suites.

The check is expected to be carried in the describe action. If a required suite is found to be null, the special status kOfxStatErrMissingHostFeature must be returned.

{Check that host supports mesh effects 6}
if (NULL == meshEffectSuite) {
    return kOfxStatErrMissingHostFeature;
}

Used in section 3

6. Description

With these suites in hand, we will focus on how a plug-in describes itself to the host. There are basically two things to handle, namely the inputs/outputs and the parameters.

Input

We will start with the former. The mesh effect suite provides an inputDefine suite. It works like any suite function:

{Prototype 7}
OfxStatus function(Type1 arg1, Type2 arg2, ..., ReturnType *return)

The suite function always return a status code, whose set of possible values may vary (see individual functions reference). You should check it, even though to make the code easier for you to read I will omit this here.

The returned value(s) is (are) the last argument, which is a pointer telling where to write the result. This typically points to a local variable, which gives in our case:

{Declare effect input 8}
OfxMeshInputHandle input;
OfxPropertySetHandle inputProperties;
meshEffectSuite->inputDefine(descriptor, kOfxMeshMainInput, &input, &inputProperties);
{Specify input name, 9}

Used in section 3

The standard describes inputs with property sets. These are basically key value stores like what Python calls a dictionary, or what JavaScript calls a JSON object. The input handle contains this property set plus it can be used to request attributes (see bellow).

Its available fields are standardized, and some hosts may extend it with extra properties. For instance, the kOfxPropLabel field is the input label, set by:

{Specify input name 9}
propertySuite->propSetString(inputProperties, kOfxPropLabel, 0, "Main Input");

Used in section 8

With these three lines, we declared that our plug-in has a main input displayed as "Main Input". The identifier kOfxMeshMainInput is standardized for hosts such has Blender modifiers that need a main input, other inputs can have arbitrary identifiers.

Output

I am not sure that it will remain like this, but at the moment outputs are actually also defined as "inputs" in the terms used by the Mesh Effect API. So adding an output is as easy as:

{Declare effect output 10}
OfxPropertySetHandle outputProps;
meshEffectSuite->inputDefine(descriptor, kOfxMeshMainOutput, &outputProps);
propertySuite->propSetString(outputProps, kOfxPropLabel, 0, "Main Output");

Used in section 3

Parameters

The parameters are fields that users can edit, keyframe, etc. They are very different from properties, that are only used internally for host/plug-in interoperability.

The standard for parameters is the same as the one used by the Image Effect API, so you can check out its documentation as well. The effect descriptors (and instances) own an OfxParamSetHandle that is retrieved using the getParamSet function from the mesh effect suite, and then altered through the parameter suite:

{Declare effect parameters 11}
OfxParamSetHandle parameters;
meshEffectSuite->getParamSet(meshEffect, &parameters);
parameterSuite->paramDefine(parameters, kOfxParamTypeDouble, "width", NULL);
parameterSuite->paramDefine(parameters, kOfxParamTypeDouble, "height", NULL);
parameterSuite->paramDefine(parameters, kOfxParamTypeInteger, "axis", NULL);
// ...

Used in section 3

7. Cooking

In our example, we do not need to initialize anything when creating an instance, so we can leave aside the createInstance and destroyInstance functions and focus now on the capital cook action.

The main steps of this function are:

  • Get the input data
  • Allocate new output data
  • Fill in the output data

Before anything, we must hence take a look at the way data is represented in Open Mesh Effect.

Data layout

Mesh data is made of geometrical and topological information. The former is a set of positions in the 3D world. We call them points. The topological information tells us about the connections between these points. It is split into corners and faces. Faces connect corners together, and each corner links to a single point. A corner only belongs to one face, while many corners might link to the same point.

NB: In Blender's vocabulary, what OpenMfx calls a 'corner' is called a 'loop', and what OpenMfx calls a 'point' is called a 'vertex'. If, on the other hand, you are used to Houdini's vocabulary, a point is a point, and what OpenMfx calls a 'corner' is called a 'vertex' there.

Concretely, a mesh in OpenMfx is represented by a collection of attributes attached to either points, corners or faces. They can be arbitrary data, but the following three attributes have a special meaning:

  • the kOfxMeshAttribPointPosition attribute, giving for each point its 3D location;
  • the kOfxMeshAttribCornerPoint attribute, giving for each corner the index of its associated point in the previous array;
  • the kOfxMeshAttribFaceSize attribute, giving for each face the number of corner it connects;

The corners in the corner attribute data buffers are sorted by face. It contains the corners of the first face, then those of the second face, etc. This is why the face data only consists in corner counts. The drawback is that random access to a face requires to sum up the counts of all the previous ones, but this can be easily cached at the beginning of the cook action.

A mesh has a property set telling the number of points, vertices and faces in the properties respectively kOfxMeshPropPointCount, kOfxMeshPropVertexCount and kOfxMeshPropFaceCount. The mesh handle can also be used with meshGetAttribute to get an attribute handle. This attribute is a property set telling the address of its memory buffer as well as information about data type, component count (1 for scalar, more for vectors) and the byte stride between two consecutive items.

Input mesh

To get a mesh from an input, the mesh effect suites provides a inputGetMesh function:

{Get input mesh 12}
OfxMeshInputHandle input;
meshEffectSuite->inputGetHandle(instance, kOfxMeshMainInput, &input, NULL);

OfxMeshHandle input_mesh;
OfxPropertySetHandle input_mesh_props;
OfxTime time = 0.0;
meshEffectSuite->inputGetMesh(input, time, &input_mesh, &input_mesh_props);

From this input mesh, you can retrieve all the properties you need:

{Get input mesh attributes 13}
int input_point_count;
propertySuite->propGetInt(input_mesh, kOfxMeshPropPointCount, 0, &input_point_count);

OfxPropertySetHandle point_position_attrib;
meshEffectSuite->meshGetAttribute(input_mesh, kOfxMeshAttribPoint, kOfxMeshAttribPointPosition, &point_position_attrib);

char *point_position_buffer;
int point_stride;
propertySuite->propGetPointer(point_position_attrib, kOfxMeshAttribPropData, 0, (void**)&point_position_buffer);
propertySuite->propGetPointer(point_position_attrib, kOfxMeshAttribPropStride, 0, &point_stride);

// Position of the i-th point is p[0], p[1], p[2]
float* p = (float*)(point_position_buffer + i * point_stride);

Output mesh

Accessing the data pointers for outputs is done with the same steps than the input, at the difference that for the pointers to be valid, it must first get allocated. The mesh effect suites provides a function for this called meshAlloc.

{Allocate output mesh 14}
int output_point_count = 2 * input_point_count;
propertySuite->propSetInt(output_mesh, kOfxMeshPropPointCount, 0, output_point_count);
// [...] same for corner and face counts.

meshEffectSuite->meshAlloc(output_mesh);

You must call this function before getting data pointers using kOfxMeshAttribPropData, otherwise pointers will be null or invalid.

Releasing meshes

Whenever you use the inputGetMesh, you must then use inputReleaseMesh when you finished editing the data.

Mirror example

In our example of a simple mirror effect, the cooking function can be:

{Cook 15}
static OfxStatus cook(PluginRuntime *runtime, OfxMeshEffectHandle instance) { 
    OfxMeshEffectSuiteV1 *meshEffectSuite = runtime->meshEffectSuite;
    OfxPropertySuiteV1 *propertySuite = runtime->propertySuite;
    OfxTime time = 0;

    // Get input/output
    OfxMeshInputHandle input, output;
    meshEffectSuite->inputGetHandle(instance, kOfxMeshMainInput, &input, NULL);
    meshEffectSuite->inputGetHandle(instance, kOfxMeshMainOutput, &output, NULL);

    // Get meshes
    OfxPropertySetHandle input_mesh, output_mesh;
    meshEffectSuite->inputGetMesh(input, time, &input_mesh);
    meshEffectSuite->inputGetMesh(output, time, &output_mesh);

    // Get input mesh data
    int input_point_count = 0, input_corner_count = 0, input_face_count = 0;
    propertySuite->propGetInt(input_mesh, kOfxMeshPropPointCount,
                              0, &input_point_count);
    propertySuite->propGetInt(input_mesh, kOfxMeshPropCornerCount,
                              0, &input_corner_count);
    propertySuite->propGetInt(input_mesh, kOfxMeshPropFaceCount,
                              0, &input_face_count);

    OfxPropertySetHandle point_position_attrib, corner_point_attrib, face_size_attrib;
    meshEffectSuite->meshGetAttribute(input_mesh, kOfxMeshAttribPoint,
                                      kOfxMeshAttribPointPosition, &point_position_attrib);
    meshEffectSuite->meshGetAttribute(input_mesh, kOfxMeshAttribCorner,
                                      kOfxMeshAttribCornerPoint, &corner_point_attrib);
    meshEffectSuite->meshGetAttribute(input_mesh, kOfxMeshAttribFace,
                                      kOfxMeshAttribFaceSize, &face_size_attrib);

    // Get input attribute data
    char *point_position_buffer;
    int point_position_stride;
    propertySuite->propGetPointer(point_position_attrib, kOfxMeshAttribPropData,
                                  0, (void**)&point_position_buffer);
    propertySuite->propGetPointer(point_position_attrib, kOfxMeshAttribPropStride,
                                  0, &point_position_stride);

    char *corner_point_buffer;
    int corner_point_stride;
    propertySuite->propGetPointer(corner_point_attrib, kOfxMeshAttribPropData,
                                  0, (void**)&corner_point_buffer);
    propertySuite->propGetPointer(corner_point_attrib, kOfxMeshAttribPropStride,
                                  0, &corner_point_stride);

    char *face_size_buffer;
    int face_size_stride;
    propertySuite->propGetPointer(face_size_attrib, kOfxMeshAttribPropData,
                                  0, (void**)&face_size_buffer);
    propertySuite->propGetPointer(face_size_attrib, kOfxMeshAttribPropStride,
                                  0, &face_size_stride);

    // Allocate output mesh
    int output_point_count = 2 * input_point_count;
    int output_corner_count = 2 * input_corner_count;
    int output_face_count = 2 * input_face_count;
    propertySuite->propGetInt(output_mesh, kOfxMeshPropPointCount,
                              0, &output_point_count);
    propertySuite->propGetInt(output_mesh, kOfxMeshPropCornerCount,
                              0, &output_corner_count);
    propertySuite->propGetInt(output_mesh, kOfxMeshPropFaceCount,
                              0, &output_face_count);

    meshEffectSuite->meshAlloc(output_mesh);

    // Get output mesh data
    OfxPropertySetHandle
        output_point_position_attrib,
        output_corner_point_attrib,
        output_face_size_attrib;
    meshEffectSuite->meshGetAttribute(output_mesh, kOfxMeshAttribPoint,
                                      kOfxMeshAttribPointPosition, &output_point_position_attrib);
    meshEffectSuite->meshGetAttribute(output_mesh, kOfxMeshAttribCorner,
                                      kOfxMeshAttribCornerPoint, &output_corner_point_attrib);
    meshEffectSuite->meshGetAttribute(output_mesh, kOfxMeshAttribFace,
                                      kOfxMeshAttribFaceSize, &output_face_size_attrib);

    char *output_point_position_buffer;
    int output_point_position_stride;
    propertySuite->propGetPointer(output_point_position_attrib, kOfxMeshAttribPropData,
                                  0, (void**)&output_point_position_buffer);
    propertySuite->propGetPointer(output_point_position_attrib, kOfxMeshAttribPropStride,
                                  0, &output_point_position_stride);

    char *output_corner_point_buffer;
    int output_corner_point_stride;
    propertySuite->propGetPointer(output_corner_point_attrib, kOfxMeshAttribPropData,
                                  0, (void**)&output_corner_point_buffer);
    propertySuite->propGetPointer(output_corner_point_attrib, kOfxMeshAttribPropStride,
                                  0, &output_corner_point_stride);

    char *output_face_size_buffer;
    int output_face_size_stride;
    propertySuite->propGetPointer(output_face_size_attrib, kOfxMeshAttribPropData,
                                  0, (void**)&output_face_size_buffer);
    propertySuite->propGetPointer(output_face_size_attrib, kOfxMeshAttribPropStride,
                                  0, &output_face_size_stride);


    // Fill in output data
    for (int i = 0 ; i < input_point_count ; ++i) {
        float* input_p = (float*)(point_position_buffer + i * point_position_stride);
        float* output_p = (float*)(output_point_position_buffer + i * output_point_position_stride);
        float* output_p2 = (float*)(output_point_position_buffer + (i + input_point_count) * output_point_position_stride);

        output_p2[0] = output_p[0] = input_p[0];
        output_p2[1] = output_p[1] = input_p[1];
        output_p2[2] = output_p[2] = input_p[2];
    }
    for (int i = 0 ; i < input_corner_count ; ++i) {
        int* input_p = (int*)(corner_point_buffer + i * corner_point_stride);
        int* output_p = (int*)(output_corner_point_buffer + i * output_corner_point_stride);
        int* output_p2 = (int*)(output_corner_point_buffer + (i + input_corner_count) * output_corner_point_stride);

        output_p[0] = input_p[0];
        output_p2[0] = input_point_count + input_p[0];
    }
    for (int i = 0 ; i < input_face_count ; ++i) {
        int* input_f = (int*)(face_size_buffer + i * face_size_stride);
        int* output_f = (int*)(output_face_size_buffer + i * output_face_size_stride);
        int* output_f2 = (int*)(output_face_size_buffer + (i + input_face_count) * output_face_size_stride);

        output_f[0] = input_f[0];
        output_f2[0] = input_f[0];
    }

    // Release meshes
    meshEffectSuite->inputReleaseMesh(input_mesh);
    meshEffectSuite->inputReleaseMesh(output_mesh);
    return kOfxStatOK;
}

You can already see from the code duplication between input and output data that you will quickly feel the need for some utility structures/functions, but I keep it simple for the example.

8. Glossary

TODO

plug-in binary #gl-plugin-binary

A binary file, ending with .ofx though it is technically a .dll or .so file, that contain all the code for one or several plug-ins.

plug-in #gl-plugin

A single plug-in, defined within a plug-in binary.