This chapter provides an overview of some of the differences between programming IRIS Performer using the C++ Application Programming Interface (C++ API) rather than the C-language Application Programming Interface (C API) which is described in the earlier chapters of this guide.
Although this guide uses the C API throughout, the C++ API is in every way equal and in some cases superior in functionality and performance to the C API.
Every function available in the C API is available in the C++ API. All of the C API routines tightly associated with a class have a corresponding member function in the C++ API, e.g. pfGetDCSMat() becomes pfDCS::getMat(). Routines not closely associated with a class are the same in both APIs. Examples include high-level global functions such as pfMultiprocess() and pfFrame() and low-level graphics functions such as pfAntialias().
Most of the routines associated with a class can be divided into three categories: setting an attribute, getting attribute and acting on the object. In the C API, sets were usually expressed as pf<Class><Attribute>, gets as pfGet<Class><Attribute> and simple actions as pf<Action><Class>, where <Class> is the abbreviation for the full name of the class. In some cases where there was no room for confusion or this usage was awkward, the routine names were shortened, e.g. pfAddChild().
The principal difference in the naming of member functions in the C++ API and the corresponding routine name in t he C-language API is in the naming of member functions where the “pf” prefix and the <Class> identifier are dropped. In addition, the word “set” or “get” is prefixed when attribute values are being set or retrieved. Hence, value setting functions are usually have names of the form pfClass::set<Attribute>, value getting functions are named pfClass::get<Attribute>, and actions appear as pfClass::<Action>.
Table 14-1. Corresponding routines in the C and C++ API
C Routine | C++ Member Function | Description |
|---|---|---|
pfMtlColor() | pfMaterial::setColor() | Set material color |
pfGetMtlColor() | pfMaterial::getColor() | Get material color |
pfApplyMtl() | pfMaterial::apply() | Apply the material |
![]() | Note: Member function whose names begin with “pf_”, “pr_” or “nb_” are internal functions and should not be used by applications. |
There are three main types of C++ classes in IRIS Performer. The following description is based on this categorization of the main types: public structs, libpr classes, and libpf classes. A fourth distinct class is pfType, the class used to represent the type of libpr and libpf classes.
These classes are public structs with exposed data members. They include pfVec2, pfVec3, pfVec4, pfMatrix, pfQuat, pfSeg, pfSphere, pfBox, pfCylinder and pfSegSet.
These classes derive from pfMemory. When multiprocessing, all processes share the same copy of the object's data members.
These classes derive from pfUpdatable and when multiprocessing, each APP, CULL and ISECT process has a unique copy of the object's. data members.
The C++ include files for libpf and libpr are in /usr/include/Performer/pf and /usr/include/Performer/pr, respectively. An application using a class should include the corresponding header file.
Table 14-2. Header Files for libpf Scene Graph Node Classes
libpf Class | Include File |
|---|---|
<Performer/pf/pfBillboard.h> | |
<Performer/pf/pfDCS.h> | |
<Performer/pf/pfGeode.h> | |
<Performer/pf/pfGroup.h> | |
<Performer/pf/pfLOD.h> | |
<Performer/pf/pfLayer.h> | |
<Performer/pf/pfLightPoint.h> | |
<Performer/pf/pfLightSource.h> | |
<Performer/pf/pfMorph.h> | |
<Performer/pf/pfNode.h> | |
<Performer/pf/pfPartition.h> | |
<Performer/pf/pfSCS.h> | |
<Performer/pf/pfScene.h> | |
<Performer/pf/pfSequence.h> | |
<Performer/pf/pfSwitch.h> | |
<Performer/pf/pfText.h> |
Table 14-3. Header Files for Other libpf Classes
libpf Class | Include File |
|---|---|
<Performer/pf/pfBuffer.h> | |
<Performer/pf/pfChannel.h> | |
<Performer/pf/pfEarthSky.h> | |
<Performer/pf/pfLODState.h> | |
<Performer/pf/pfPipe.h> | |
<Performer/pf/pfPipeWindow.h> | |
<Performer/pf/pfTraverser.h> |
Table 14-4. Header Files for libpr Graphics Classes
libpr Class | Include File |
|---|---|
<Performer/pr/pfColortable.h> | |
<Performer/pr/pfDispList.h> | |
<Performer/pr/pfFog.h> | |
<Performer/pr/pfFont.h> | |
<Performer/pr/pfGeoSet.h> | |
<Performer/pr/pfGeoState.h> | |
<Performer/pr/pfHighlight.h> | |
<Performer/pr/pfLPointState.h> | |
<Performer/pr/pfLight.h> | |
<Performer/pr/pfMaterial.h> | |
<Performer/pr/pfSprite.h> | |
<Performer/pr/pfState.h> | |
<Performer/pr/pfString.h> | |
<Performer/pr/pfTexture.h> |
Table 14-5. Header Files for Other libpr Classes
The IRIS Performer base classes all provide operator new and operator delete. All libpr and libpf objects must be explicitly created with operator new and deleted with operator delete. Objects of these classes cannot be created statically, on the stack or in arrays.
The default new operator creates objects in the current shared memory arena, if one exists. libpr objects and public structs have an additional new operator that takes an arena argument. This new operator allows allocation from the heap (indicated by an arena of NULL) or from a shared memory arena created by the application with IRIX acreate().
// legal creation of libpf objects pfDCS *dcs = new pfDCS; // only way // legal creation of libpr objects pfGeoSet *gs = new pfGeoSet; // from default arena pfGeoSet *gs = new(NULL) pfGeoSet; // from heap // legal creation of public structs pfVec3 *vert = new pfVec3; // from default arena pfVec3 *verts = new pfVec3[10]; // array from default static pfVec3 vert(0.0f, 0.0f, 0.0f); // static |
// illegal creation of libpf objects pfDCS *dcs = new(NULL) pfDCS; // not in shared mem pfDCS *dcs = new pfDCS[10]; // array // illegal creation of libpr objects pfGeoSet *gs = new pfGeoSet[10]; // array // illegal creation of public structs pfVec3 *vert = new(NULL) pfVec3[10];// array, non-default new |
![]() | Caution: This last item in Example 14-2 is illegal because C++ does not provide a mechanism to delete arrays of objects allocated with a new operator defined to take additional arguments, e.g. operator new(size_t s, void *arena). Attempting to delete an array of objects allocated in this manner can cause unpredictable and fatal results such as the invocation of the destructor a large number of times on pointers inside and outside of the original allocation. |
All objects of classes derived from pfObject or pfMemory are reference counted and must be deleted using pfDelete(), rather than the delete operator. pfDelete() checks the reference count of the object and when multiprocessing, delays the actual deletion until other processes are done with the object. To decrement the reference count and delete with a single call use pfUnrefDelete().
Public structs such as pfVec3, pfSphere, etc. may be deleted either with pfDelete() or the delete operator.
Since libpr and libpf objects are allocated, they can only be maintained by reference.
Passing arrays of floats is very common in graphics programming. Calls to IRIS GL or OpenGL often require an array of floats or a matrix. In the C API, the data types such as pfMatrix are arrays and so can be passed straight through to OpenGL routines, e.g.
pfMatrix ident;
pfMakeIdentMat(ident);
glLoadMatrix(ident);
|
In the C++ API, the data field of the pfMatrix must be passed instead, e.g.
pfMatrix ident;
ident.makeIdent();
glLoadMatrix(ident.mat);
|
When compiled with C++, IRIS Performer supports three usages of the API:
Pure C++ API. This is the default style of usage.
Pure C API. This can be achieved by defining the token PF_CPLUSPLUS_API to be 0, e.g. by adding the line:
#define PF_CPLUSPLUS_API 0 |
in source files before they include any IRIS Performer header files. In this mode all data types are the same as when compiling with C.
C++ API and C API. This mode can be enabled by defining the token PF_C_API to be 1, e.g. by adding the line
#define PF_C_API 1 |
in source files before they include any IRIS Performer header files. In this mode, both C++ and C functions are available and data types are C++. See the section below concerning passing certain data types.
In the C API, the pfVec2, pfVec3, pfVec4, pfMatrix and pfQuat data types are all typedefed arrays. In the C++ API, they are all structs. When converting C code to use the C++ API or when compiling CAPI code with both APIs enabled, be sure to change routines in your code that pass objects of these types. In the C++ API, you almost always want to pass arguments of these types by reference rather than by value.
For example, the C API routine
void MyVectorAdd(pfVec2 dst, pfVec2 v1, pfVec2 v2)
{
dst[0] = v1[0] + v2[0];
dst[1] = v1[1] + v2[1];
}
|
should be rewritten for the C++ API to pass by reference
void MyVectorAdd(pfVec2& dst, pfVec2& v1, pfVec2& v2)
{
dst[0] = v1[0] + v2[0];
dst[1] = v1[1] + v2[1];
}
|
or
void MyVectorAdd(pfVec2* dst, pfVec2* v1, pfVec2* v2)
{
dst->vec[0] = v1->vec[0] + v2->vec[0];
dst->vec[1] = v1->vec[1] + v2->vec[1];
}
|
Without this change, time will be wasted copying v1 and v2 by value and the result will not be returned to the routine calling MyVectorAdd().
The same difference in passing conventions applies if you are calling a C function from code that uses the C++ API. Functions passing typedefed arrays with the C API must have a different prototype for use with the C++ API. Macros for use in C prototypes bilingual can be found in /usr/include/Performer/prmath.h.
These macros are used in the C API prototypes for IRIS Performer that pass typedefed arrays, e.g.
extern float pfDotVec2(const PFVEC2 v1, const PFVEC2 v2); |
But they are not necessary or appropriate for when passing pointers to typedefed arrays in C, e.g.
extern void pfFontCharSpacing(pfFont *font, int ascii,
pfVec3 *spacing);
|
because a pointer to a struct is passed in the same manner as a pointer to an array.
With the C API, the main mechanism for extending the functionality of the classes provided in IRIS Performer is the specification of the user data pointer on pfObjects with pfUserData() and the specification of callbacks on pfNodes with pfNodeTravFuncs() and pfNodeTravData(). The C++ API also supports these mechanisms, but also provides the additional capacity to subclass new data types from the classes defined in IRIS Performer. Subclassing allows additional member data fields and functions to be added to IRIS Performer classes. At it's simplest, subclassing merely provides a way of adding additional data fields that is more elegant than hanging new data structures off of a pfObject's user data pointer. But in some uses, subclassing also allows significantly more control over the functional behavior of the new object because virtual functions can be overloaded to bypass, replace or augment the processing handled by the parent class from IRIS Performer.
The new object should provide two static functions, a constructor that initializes the instances pfType* and a static data member for the type system as shown in the following table;
Table 14-6. Data and Functions Provided by User Subclasses
Class Data or Function | Function |
|---|---|
static void init() | Initialize the new class |
static pfType* getClassType() | Returns the pfType* of the new class |
static pfType* classType | Stores the pfType* of the new class |
constructor | Sets the pfType* for each instance |
The init() member function should initialize any data structures that are related to the class as a whole, as opposed to any particular instance. The most important of these is the entry of the class into the type system. For example, the Rotor class defined in the Open Inventor loader (see Rotor.h and Rotor.C in /usr/share/Performer/src/lib/libpfdb/libpfiv) is a subclass of pfDCS. It's initialization function merely enters the class into the type system.
public Rotor : public pfDCS
{
static void init();
static pfType* getClassType(){ return classType; };
static pfType* classType;
}
pfType *Rotor::classType = NULL;
Rotor::Rotor()
{
setType(classType); // set the type of this instance
...
}
void
Rotor::init()
{
if (classType == NULL)
{
pfDCS::init();
classType =
new pfType(pfDCS::getClassType(), “Rotor”);
}
}
|
As described in the section below, the initialization function, Rotor::init() should be called before pfConfig().
Below is the example of the Rotor class which specifies the traversal function for the libpf application traversal. When overloading a traversal function, it is usually desirable to invoke the parent class function, in this case, pfDCS::app(). It is not currently possible to overload libpf's intersection or culling traversals. See “libpf Objects and Multiprocessing”.
Example 14-4. Overloading the libpf Application Traversal
int
Rotor::app(pfTraverser *trav)
{
if (enable)
{
pfMatrix mat;
double now = pfGetFrameTimeStamp();
// use delta and renorm for large times
prevAngle += (now - prevTime)*360.0f*frequency;
if (prevAngle > 360.0f)
prevAngle -= 360.0f;
mat.makeRot(prevAngle, axis[0], axis[1], axis[2]);
setMat(mat);
prevTime = now;
}
return pfDCS::app(trav);
}
int
Rotor::needsApp(void)
{
return TRUE;
}
|
The same behavior could also be implemented in either the C or C++ IRIS Performer API using a callback function specified with pfNodeTravFuncs().
![]() | Note: Classes of pfNodes that need to be visited during the application traversal even in the absence of any application callbacks should define the virtual function needsApp() to return TRUE. |
In general to assure safe multiprocess operation with any DSOs providing C++ virtual functions or defining new pfTypes. Initialization should be carried out in the following sequence:
Call pfInit(). This initializes the type system and for libpf applications sets up shared memory.
Initialize any application-supplied classes:
a) Load any application-specific C++ DSOs
b) Call pfdInitConverter() to initialize and load any converter DSOs.
c) Enter any user-supplied pfTypes into the type system, e.g. call
Rotor::init()
|
Call pfConfig(). This forks off other processes as specified by pfMultiprocess()
Create libpf and libpr objects.
IRIS Performer objects or other objects that use pfTypes can only be shared between related processes. Related processes are those created with fork() or sproc() from the main process after pfInit() in a libpf application, e.g. processes created by pfConfig().
New pfTypes should be added before pfConfig() forks off other processes so that the static data member containing the class type is visible in all processes, otherwise pf<Class>::getClassType() will return NULL in other processes. This effectively precludes the creation of subclasses of IRIS Performer objects after pfConfig().
When using virtual functions, it's very important that the object code reside at the same address in all processes. Normally, this is not an issue since the object code for all IRIS Performer classes is loaded (whether statically linked or loaded as dynamic shared objects, DSOs) before pfConfig() is called to fork off processes. For user-defined C++ classes with virtual functions, it's important that the object code reside at the same virtual address space in all processes that access them. For this reason, the DSOs for any user-defined classes should be loaded before pfConfig() regardless of whether they use the pfType system or not.
The default operator new for objects derived from pfObject causes all instances to be created in shared memory, so that objects will be visible to other related processes that need to see them.
Classes having static data members that may change value and need to be visible from all processes, should allocate shared memory for the data (e.g. pfMalloc or new pfMemory) and set the static data member to point to this memory before pfConfig() as shown in the following example.
class Rotor : public pfDCS
{
static int* instanceCount;
}
Rotor::instanceCount = NULL;
void Rotor::init()
{
...
instanceCount = new(sizeof(int)) pfMemory;
*instanceCount = 0;
}
Rotor::Rotor()
{
...
(*instanceCount)++; // increment the creation counter
}
|
A static data member whose value is set before pfConfig() and never changes thereafter does not need to be allocated from shared memory. The classType member of Rotor is an example of this since the class should be initialized, i.e. Rotor::init() called, before pfConfig().
The multiprocessing behavior of libpf objects (i.e. those deriving from pfNode or pfUpdatable) differs from that of libpr objects. Both are typically created in shared memory, but with a libpr object, all processes share the same data members, while libpf objects have a built in multiprocessing data mechanism that provides different copies in the APP, CULL and ISECT stages of the IRIS Performer pipeline. The term multibuffering refers to the maintenance and frame-accurate updating of these data.
With a user-defined subclass of a libpf class, the original data elements of the libpf parent class are still multibuffered. However, the parallel multibuffer copies maintained in the other processes are instances of the parent class rather than the subclass. This is not normally visible to the application, since even for callbacks in the CULL and ISECT processes, the application always works from the pointer to the copy used in the APP process, in part so that objects can be identified by comparison of pointers. However, this difference would be visible if the virtual traversal functions for culling or intersection were overloaded. These virtual functions should not be overloaded by the subclass since they will not have any effect when the CULL or ISECT stages are in separate processes. Node callbacks specified with pfNodeTravFuncs() should be used instead.
Subclassing will be vastly simplified and more flexible in a future release.
It's quite natural to frequently construct and destroy arrays of public structs such as pfVec3 on the stack. Beware, even though the constructors for these classes are empty, it still requires a function call for each element of the array. The same applies to classes such which contain arrays of structs, e.g. pfSegSet contains an array of pfSegs.