Extending VEX

Side Effects Software Inc. 2002

It is possible to extend VEX by writing a "plug-in" which adds new functions to VEX. This allows you to write your own functions (in C++) which can be called from within VEX.

In order to specify a plug-in function, you must specify it's signature, an evaluation callback. Optionally, you can specify which VEX contexts the function is valid for, an initialization and cleanup routine and optimization hints to the VEX engine.

Signature

VEX Signatures are used to describe the return types and arguments to your function. The signature is used by the compiler (vcc) to determine the calling syntax for your function. There are some simple rules which apply to how a signature should be described:
  1. The signature takes the form function_name@arg_types where function_name is simply the name of the function as seen by a user of VEX. Following the @ symbol are single letter tokens describing the arguments to the function (see below about return codes). Each internal VEX type has a single character mnemonic associated with it:
    I int
    F float
    V vector
    P vector4
    3 matrix3
    4 matrix4
    S string
    As well, each argument symbol should be prefixed with either an ampersand (&) or an asterix (*). The meaning of the two are subtly different (see below) but indicate to the compiler that those arguments are modified (rather than read-only).

  2. The ampersand and asterix modifiers are used to tell the VEX engine which parameters are written to by your function. The VEX optimizer needs to know the status of each parameter. The possible states for a parameter are ReadOnly, ReadWrite, or WriteOnly.
    No modifier The value of the parameter is read only. The value cannot be modified by your code.
    Ampersand (&) prefix The value of the parameter is write only. The parameter value is set by your function without knowing what the previous value is.
    Asterix (*) prefix The value of the parameter is read/write. The parameter value is possibly read by your function, then the a new value may be set by your callback function.

  3. Internally, VEX has no concept of return codes. However, any function which has a single write-only argument (with no read/write arguments) will be interpreted as having the write-only argument as its return code.

Examples:
  • vector_length@&FV

    This will be interpreted as float vector_length(vector) by the compiler (vcc). Because there is one write-only argument (declared by the &F), VEX interprets this as the return code for the function. The vector argument (declared by V) is a read only argument.

  • add_float@*FF

    This decodes to void add_float(float &, float). Since the first argument to your callback function is flagged as read-write, this cannot be used as a return code by the VEX engine. This function might add the second argument to the first argument. Thus, implicitly, the callback function needs to "read" the previous value of the first argument in order to add to it.

  • mynoise@&I&F4 This would decode to void mynoise(int &, float &, matrix) . Because there are more than one variable modified by the callback function, there will not be a return code generated.

  • Callbacks

    There are three separate callbacks which can be declared for your user function. Two callbacks are used to allocate and free user data for your function. These functions are called for each "instance" of your user function. That is for every time your function is called in the code, the initialization function is called. When the code using your custom function is no longer used, the cleanup function is called. As a warning, this means that if your function is used three times in a single VEX function, the initialization call will be made three times. Specific data may be allocated for each instance of your user function.

    The initialization function should return a void * to data. Whatever the value that is returned is the value passed back to the evaluation and cleanup callbacks.

    It is not necessary to allocate data for each instance, nor is it even necessary to have initialization or cleanup function calls.

    The evaluation callback simply has three arguments: an argc (the number of arguments being passed to your function), an array of void * data which contains pointers to the data for the parameters, and lastly, the void * returned by your initialization routine. If the there is no initialization callback specified, then the last void * will be a null pointer.

    Each void * in the argv[] array should simply be cast to the data type that you expect. vector, vector4, matrix3, and matrix4 types are stored as contiguous arrays. Thus a vector element will point to an array of 3 floating point numbers.

    At the current time, string values cannot be modified by user callback functions. There are no checks in the run-time code, so if your code tries to modify a string value, there will most likely be a core dump.

    Context specification

    It is possible to limit the context scope of your function to a specific set of contexts. This is done by setting the context mask in the constructor.

    Optimization

    The VEX engine has an internal optimizer which may cause your function to mis-behave. If you find this is the case, try changing the optimization level to 0 (instead of the default 2). If your function works properly, don't mess with the optimization level. Basically, if your function is non-deterministic (i.e. returns a random number or the current system time or something like that), you might want to turn off optimization. Otherwise, the optimizer should do a reasonable job.

    As an aside, if your user function doesn't modify any variables, then the function may be optimized out of the code. This may not be desirable since your function may be doing other tasks (i.e. writing to a disk file, communicating via sockets, etc.). In this case, the easiest way to trick the compiler is to make one of the parameters to your function read/write. Your function doesn't have to modify the value, it just has to "trick" the VEX engine into thinking that it does. This "trick" may not work if the variable passed in does not get used elsewhere in the code... There is currently no work-around for this.

    Examples

    #include <stdio.h> #include <time.h> #include <math.h> #include "VEX_VexOp.h" // // Callback for drand() function // static void drand_Evaluate(int, void *argv[], void *) { float *result = (float *)argv[0]; const int *seed = (const int *)argv[1]; srand48(*seed); *result = drand48(); } // // Callback for time() function // static void time_Evaluate(int, void *argv[], void *) { int *result = (int *)argv[0]; *result = time(0); } // // Callbacks for gamma() function // #define SIZE 1024 class gamma_Table { public: gamma_Table(); // User responsibility to write this ~gamma_Table(); // User responsibility float evaluate(float val); // User responsibility int myRefCount; }; static gamma_Table *theTable = 0; static void * gamma_Init() { if (!theTable) theTable = new gamma_Table(); else theTable->myRefCount++; return 0; } static void gamma_Cleanup(void *) { theTable->myRefCount--; if (!theTable->myRefCount) { delete theTable; theTable = 0; } } static void gamma_Evaluate(int, void *argv[], void *) { float *result = (float *)argv[0]; *result = theTable->evaluate(*result); } // // Installation function // void newVEXOp(void *) { // // Usage: float drand48(int seed); // Returns a random number based on the seed passed in new VEX_VexOp("drand@&FI", // Specify signature drand_Evaluate); // evaluation callback // // Usage: int time(void) // Returns the system time (in seconds) // // Because the time() callback produces non-deterministic results, we have // to turn off full optimization. new VEX_VexOp("time@&I", time_Evaluate, (VEX_OP_CONTEXT|VEX_SHADING_CONTEXT), 0, 0, VEX_OPTIMIZE_1); // // Usage: gamma(float &value) // Do a table lookup on a value between 0 and 1. new VEX_VexOp("gamma@*F", gamma_Evaluate, // evaluation callback (VEX_OP_CONTEXT|VEX_SHADING_CONTEXT), gamma_Init, gamma_Cleanup); }

    Compiling

    On most platforms, compiling the VEX OP plug-in is fairly straight-forward. Just make sure that you can include "VEX_VexOp.h" in your code, then compile using:

    SGI CC -n32 -mips3 -shared -o myfunc.so myfunc.C
    Note: The SGI C++ compiler must be used
    NT Compiling VEX Ops for NT requires the Houdini Development Kit. The hcustom command can be used to make object.
    Linux g++ -shared -ldl -ldb1 -o myfunc.so myfunc.C
    Solaris CC -c -D_POSIX_PTHREAD_SEMANTICS -instances=extern -features=%all,no%conststrings,anachronisms -xtarget=ultra2 -xarch=v9 -xcode=pic32 -xbuiltin=%all -o myfunc.so myfunc.C

    CC -xtarget=ultra2 -xarch=v9 -xcode=pic32 -G -hmyfunc.so -o myfunc.so myfunc.o -lGLU -lGL -lX11 -lXext -lnsl -lsocket -lc -lsunmath -ldl -lm -mt -lpthread -lmalloc -lkstat

    Installing the .so

    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 simply 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 will tell the VEX engine to look for the first occurance 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).



    Copyright © 1999 Side Effects Software Inc.
    477 Richmond Street West, Toronto, Ontario, Canada M5V 3E7