HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
Adding Custom VEX Functions

Table Of Contents

VEX Introduction

VEX is an interpreted language which runs in on SIMD virtual machine.

Users can add function to the VEX library by writing a plug-in using the VEX_VexOp class.

In order to specify a plug-in function, you must specify

  • A function signature (which parameters are accepted)
  • A callback function
  • Optional: Initialization and cleanup functions
  • Optional: Optimization hints to the VEX run-time engine
See Also
Calling VEX from C++

VEX Function Signatures

VEX Signatures are used to describe the return types and arguments to your function. The signature is used by the compiler (vcc) to determine what types of parameters are passed to your function. The signature is specified as a mangled name consisting of two parts separated by a @ character. The first part of the signature string represents the function name, the second, the types of parameters accepted by the function. For example

foobar@*FV&IF

Where foobar is the name of the function, and the arguments are specified by the string *FV&IV.

Each argument is specified by a single character with an optional single character modifier which precedes the letter. The letter is case sensitive and specifies the type of the parameter

  • I = int
  • F = float
  • V = vector
  • P = vector4
  • 3 = matrix3
  • 4 = matrix
  • S = string

If the type specifier is not prefixed, the parameter is considered read-only. Otherwise, the type specifier may be prefixed by either

  • No prefix
    The parameter is read-only.
  • &
    The parameter is write-only. That is, the callback function shouldn't expect the value to be initialized, but simply writes to the parameter.
  • *
    The parameter is marked as read/write. The function may read from the value and may also write to the value. The VEX compiler (http://www.sidefx.com/docs/current/vex/vcc) will warn the user if the variable is not initialized before the function is called.

Variadic callbacks are indicated by a trailing + token in the signature, indicating that additional arguments may be provided beyond the last concrete argument.

Examples

// If there is a single write-only variable, VEX will turn it into
// a return code.
getpid@&I // int getpid()
// Again, the first argument is interpreted as a return code.
// The vector is read-only (no * or & modifiers)
vector_length@&FV // float vector_length(vector)
// Again, the first argument is interpreted as a return code. The
// two following vectors are read-only.
cross@&VVV // vector cross(vector, vector)
// Since the first argument is read-write, it is not interpreted
// as a return code. The VEX compiler will expect the first
// argument to be initialized before it's passed to the callback.
// The second argument is a read-only float.
add_float@*FF // add_float(float &, float)
// Since there are multiple write-only variables, VEX will not
// use any of them as return codes. Instead, all arguments will
// be passed as parameters. In the VEX_VexOp constructor, it's
// possible to change this behavior.
mread@&IS&4 // void mread(int &, string, matrix &)
// A variadic function. VEX will allow any number of arguments of any
// type.
myprint@+ // void myprint(...)

As seen with the second last example, sometimes VEX may not generate the signature you expect. You can verify your signatures by running

% vcc -X surface

which lists all the functions available in the surface context.

There is an argument in the VEX_VexOp constructor to force the function to take the first write-only argument as a return code. Thus,

VEX_VexOp("mread@&IS&4", // Signature
callback, // VEX_VexOpCallback
VEX_ALL_CONTEXT, // VEX Contexts
NULL, // VEX_VexOpInit
NULL, // VEX_VexOpCleanup
VEX_OPTIMIZE_2, // Optimization level
true); // Forced return code

will result in the signature: int mread(string, matrix &)

VEX Callbacks

There are three separate callbacks which can be declared for your user function.

  • The evaluation callback (VEX_VexOpCallback).
  • An optional initialization function (VEX_VexOpInit)
  • A optional cleanup function (VEX_VexOpCleanup)

The initialization and cleanup functions are used to allocated and free user data for your function. The void pointer returned by the initialization function is passed to the evaluation and cleanup callbacks.

The initialization/cleanup functions are called for each instance of your user function. That is for every time your function is referenced in the VEX code, the initialization function is called. When the code using your custom function is no longer used, the cleanup function is called (one time for each instance). This means that if your function is used two times in a single VEX function, the initialization call will be made two times. This allows you to allocate data per-instance, or to have shared data between instances (using a static/global singleton). For example:

cvex
foo()
{
float a, b;
a = myFunction(); // First instance of callback
b = myFunction(); // Second instance of callback
}

The evaluation callback takes three arguments:

  • argc (the number of arguments being passed to your function)
  • argv (an array of void * data which contains pointers to the data for the parameters
  • the void * returned by the initialization function

Each void pointer in the argv[] array should be cast to the data type that you expect. For example:

// func@IFVP34
void
Callback(int argc, void *argv[], void *data)
{
const int *arg0 = (const int *)argv[0];
const fpreal32 *arg1 = (const fpreal32 *)argv[1];
const UT_Vector3 *arg2 = (const UT_Vector3 *)argv[2];
const UT_Vector4 *arg3 = (const UT_Vector4 *)argv[3];
const UT_Matrix3 *arg4 = (const UT_Matrix3 *)argv[4];
const UT_Matrix4 *arg5 = (const UT_Matrix4 *)argv[5];
...
}

Array types are provided to the callback function in UT_Array objects. For example:

// func@[I[F[V[P[3[4[S
void
Callback(int argc, void *argv[], void *data)
{
const UT_Array<int> *arg0 = (const UT_Array<int> *)argv[0];
const UT_Array<fpreal32> *arg1 = (const UT_Array<fpreal32> *)argv[1];
const UT_Array<UT_Vector3> *arg2 = (const UT_Array<UT_Vector3> *)argv[2];
const UT_Array<UT_Vector4> *arg3 = (const UT_Array<UT_Vector4> *)argv[3];
const UT_Array<UT_Matrix3> *arg4 = (const UT_Array<UT_Matrix3> *)argv[4];
const UT_Array<UT_Matrix4> *arg5 = (const UT_Array<UT_Matrix4> *)argv[5];
const UT_Array<const char *> *arg6 = (const UT_Array<const char *> *)argv[6];
...
}

String arguments must be modified using the provided stringAlloc() and stringFree() functions. For example, strcat() might be implemented as:

void
vexStrCat(int argc, void *argv[], void *)
{
// arg[0] = result, arg[1..2] == strings to concat
// Create the resulting string
wbuf.strcpy((const char *)argv[1]);
wbuf.strcat((const char *)argv[2]);
// Free previous string value
VEX_VexOp::stringFree((const char *)argv[0]);
// Assign the new value
argv[0] = VEX_VexOp::stringAlloc(wbuf.buffer());
}

If your function takes variadic arguments, you should use the VEX_VexOpTypedCallback instead of VEX_VexOpCallback. This alternative callback provides type information with each argument so you can implement the correct behavior for different types that may be provided to the variadic function.

static void
myprint_Evaluate(int argc, VEX_VexOpArg argv[], void *data)
{
printf("%d args:\n", argc);
for (int i = 0; i < argc; i++)
{
if (argv[i].myArray)
continue; // Doesn't support arrays
switch (argv[i].myType)
{
printf(" int %d\n", *(const int *)argv[i].myArg);
break;
printf(" float %f\n", *(const float *)argv[i].myArg);
break;
default:
break;
}
}
}

VEX Types and Casting

The VEX evaluation callback currently takes arguments as void pointers. Users are required to cast these pointers to the expected types.

Please use the types defined in VEX_PodTypes.h when performing casts. This will help future-proof your code.

VEX Context Specification

VEX has multiple contexts (i.e. the surface or vex contexts). It's possible to limit your VEX function to a specific set of contexts This is done through the context mask in the constructor.

See Also
enum::VEX_ContextType

VEX Optimization

The VEX runtime engine has an internal optimizer which can reduce a function to a constant value (constant folding), or simply remove it altogether (also known as elision). The choice is based on whether the input argument(s) are all constant values, and whether the function's result is used.

If a function returns a value that is not dependent on the input argument(s), or it has a side-effect on some internal state, the optimizer can be instructed to not attempt to optimize the function call out.

For example, if a function returns the system time:

VEX_VexOp( "time@&I" );

In this case, VEX cannot automatically determine that this function may return different values at different call times. In that case, set the optimize level for this function to 1 (see the optimize_level argument to the VEX_VexOp constructor).

If the function modifies some internal state, and should not be removed at all from the optimized code, the optimize level should be set to 0.

Note
Lowering the optimization level may have a significant impact on performance.

Examples

VEX/VEX_Example.C has examples

VEX/VEX_Ops.C has examples

  • int getpid()
  • int gettid()
    Uses UT_Thread::getMyThread() to query the running thread
  • int sticky()
    A function which will not be optimized out
  • int rcode(float &, float &, float, float) A function which forces a return code
  • int rcode(float &, float &, string A function which forces a return code (with a different signature)

Compiling VEX Plug-Ins

Creating VEX plug-ins using the HDK allows you to use most classes and functions available in the HDK. When compiling plugins for Mantra, there are some libraries which mantra doesn't link against, so if your plug-in needs to access SOPs, you may have to wrangle the link line so that the plug-in links against the appropriate libraries.

The .so file still needs to be installed in the VEXdso file (see below).

See Also
Compiling

Installing the Plug-In

The last thing required to install your DSO function is to create a table in the Houdini path so that the VEX engine can find your plug-ins. The table lists the location of the dynamic link objects which should be loaded by the VEX engine. For example (on Unix):

% echo vex/myfunc.so > ~/houdiniVERSION/vex/VEXdso

This tells VEX to look for the first occurrence of the file vex/myfunc.so in your HOUDINI_DSO_PATH path. With the default HOUDINI_DSO_PATH, the above table entry would tell the VEX engine to search for a sub-directory in the HOUDINI_DSO_PATH (run "hconfig -ap" for more information).