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:
- 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).
- 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.
|
- 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:
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