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 Precision

VEX can be run in with different precision (see VEX_Precision). At the time, this enum has two values:

  • VEX_32 : Run in 32 bit precision
  • VEX_64 : Run in 64 bit precision

There are typedefs for all the base types used throughout VEX which are templated based on the precision. For example, a callback might look something like:

template <VEX_Precision PREC>
callback(int argc, void *argv[], void *data)
{
auto ival = reinterpret_cast<const VEXint<PREC> *>(argv[0]);
auto vval = reinterpret_cast<const VEXvec3<PREC> *>(argv[1]);
auto m2_array = reinterpret_cast<const UT_Array<VEXmat2<PREC>> *>(argv[2]);
}

Please see VEX_PodTypes.h for more detail.

There are multiple constructors for the VEX_VexOp object. The simplest (and for backward compatibility) just takes 32 bit precision callbacks. When run in 64 bit mode, VEX will wrap these callbacks, automatically casting the data so the callback gets the precision it expects.

The preferred constructor though, takes callbacks for both 32 and 64 bit precision. If the callbacks are templated based on precision, this makes the constructor creation fairly straight-forward:

VEX_VexOp("mread@&IS&4", // Signature
callback<VEX_32>, // 32-bit evaluator
callback<VEX_64>, // 64-bit evaluator
VEX_ALL_CONTEXT, // VEX Contexts
nullptr, // VEX_VexOpInit (32 bit)
nullptr, // VEX_VexOpInit (64 bit)
nullptr, // VEX_VexOpCleanup (32 bit)
nullptr, // VEX_VexOpCleanup (64 bit)
VEX_OPTIMIZE_2, // Optimization level
true); // Forced return code

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
  • U = vector2
  • V = vector
  • P = vector4
  • 2 = matrix2
  • 3 = matrix3
  • 4 = matrix
  • S = string

Array of POD types use the single character mnemonic for the type preceeded by the open square bracket ([) character. So, they are represented by:

  • [I = int array
  • [F = float array
  • [U = vector2 array
  • [V = vector array
  • [P = vector4 array
  • [2 = matrix2 array
  • [3 = matrix3 array
  • [4 = matrix array
  • [S = string array

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
nullptr, // VEX_VexOpInit
nullptr, // 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
template <VEX_Precision PREC>
void
Callback(int argc, void *argv[], void *data)
{
const auto *arg0 = (const VEXint<PREC> *)argv[0];
const auto *arg1 = (const VEXfloat<PREC> *)argv[1];
const auto *arg2 = (const VEXvec2<PREC> *)argv[2];
const auto *arg3 = (const VEXvec4<PREC> *)argv[3];
const auto *arg4 = (const VEXmat3<PREC> *)argv[4];
const auto *arg5 = (const VEXmat4<PREC> *)argv[5];
...
}

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

// func@[I[F[V[P[3[4[S
void
Callback(int argc, void *argv[], void *data)
{
const auto *arg0 = (const UT_Array<VEXint<PREC>> *)argv[0];
const auto *arg1 = (const UT_Array<VEXfloat<PREC>> *)argv[1];
const auto *arg2 = (const UT_Array<VEXvec3<PREC>> *)argv[2];
const auto *arg3 = (const UT_Array<VEXvec4<PREC>> *)argv[3];
const auto *arg4 = (const UT_Array<VEXmat3<PREC>> *)argv[4];
const auto *arg5 = (const UT_Array<VEXmat4<PREC>> *)argv[5];
const auto *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.

template <VEX_Precision PREC>
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 VEXint<PREC> *)argv[i].myArg);
break;
printf(" float %f\n", *(const VEXfloat<PREC> *)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

Houdini will automatically search for the newVEXOp() function in all plugins found in the HOUDINI_VEX_DSO_PATH. You can run hconfig -ap to see the path definition.

The easiest way to test to see whether your plugin is being loaded is to run vcc -X cvex which will print out all the functions available in the CVEX context.

You can also set HOUDINI_DSO_ERROR to see any dynamic loading errors. See hconfig -h HOUDINI_DSO_ERROR for more information.