Chapter 26. Programming with C++

This chapter provides an overview of some of the differences between programming OpenGL 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.

Overview

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; for example, 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, for example, pfAddChild().

The principal difference in the naming of member functions in the C++ API and the corresponding routine name in the 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 26-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. These functions may have unpredictable side effects and also should not be overridden by application subclasses.


Class Taxonomy

There are three main types of C++ classes in OpenGL 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.

Public Structs

These classes are public structs with exposed data members. They include pfVec2, pfVec3, pfVec4, pfMatrix, pfQuat, pfSeg, pfSphere, pfBox, pfCylinder, and pfSegSet.

libpr Classes

These classes derive from pfMemory. When multiprocessing, all processes share the same copy of the object's data members.

libpf Classes

These classes derive from pfUpdatable and when multiprocessing, each APP, CULL, and ISECT process has a unique copy of the object's data members.

pfType Class

As with the C API, information about the class hierarchy is maintained with pfType objects.

Programming Basics

Header Files

For IRIX and Linux, the C++ include files for libpf and libpr are in /usr/include/Performer/pf and /usr/include/Performer/pr, respectively. For Microsoft Windows, the corresponding header files are in %PFROOT%\Include\pf and %PFROOT%\Include\pr. An application using a class should include the corresponding header file.

Table 26-2. Header Files for libpf Scene Graph Node Classes

libpf Class

Include File

pfASD

<Performer/pf/pfASD.h>

pfBillboard

<Performer/pf/pfBillboard.h>

pfDoubleDCS

<Performer/pf/pfDoubleDCS.h>

pfDoubleFCS

<Performer/pf/pfDoubleFCS.h>

pfDoubleSCS

<Performer/pf/pfDoubleSCS.h>

pfDCS

<Performer/pf/pfDCS.h>

pfFCS

<Performer/pf/pfFCS.h>

pfGeode

<Performer/pf/pfGeode.h>

pfGroup

<Performer/pf/pfGroup.h>

pfIBRnode

<Performer/pf/pfIBRnode.h>

pfLOD

<Performer/pf/pfLOD.h>

pfLayer

<Performer/pf/pfLayer.h>

pfLightPoint

<Performer/pf/pfLightPoint.h>

pfLightSource

<Performer/pf/pfLightSource.h>

pfNode

<Performer/pf/pfNode.h>

pfPartition

<Performer/pf/pfPartition.h>

pfSCS

<Performer/pf/pfSCS.h>

pfScene

<Performer/pf/pfScene.h>

pfSequence

<Performer/pf/pfSequence.h>

pfSwitch

<Performer/pf/pfSwitch.h>

pfText

<Performer/pf/pfText.h>


Table 26-3. Header Files for Other libpf Classes

libpf Class

Include File

pfBuffer

<Performer/pf/pfBuffer.h>

pfChannel

<Performer/pf/pfChannel.h>

pfCullProgram

<Performer/pf/pfCullProgram.h>

pfEarthSky

<Performer/pf/pfEarthSky.h>

pfLODState

<Performer/pf/pfLODState.h>

pfMPClipTexture

<Performer/pf/pfMPClipTexture.h>

pfPipe

<Performer/pf/pfPipe.h>

pfPipeWindow

<Performer/pf/pfPipeWindow.h>

pfRotorWash

<Performer/pf/pfRotorWash.h>

pfShaderObject

<Performer/pf/pfShaderObject.h>

pfShaderProgram

<Performer/pf/pfShaderProgram.h>

pfShadow

<Performer/pf/pfShadow.h>

pfPipeVideoChannel

<Performer/pf/pfPipeVideoChannel.h>

pfTraverser
pfPath

<Performer/pf/pfTraverser.h>

pfVolFog

<Performer/pf/pfVolFog.h>


Table 26-4. Header Files for libpr Graphics Classes

libpr Class

Include File

pfColortable

<Performer/pr/pfColortable.h>

pfClipTexture

<Performer/pr/pfClipTexture.h>

pfDispList

<Performer/pr/pfDispList.h>

pfFog

<Performer/pr/pfFog.h>

pfFont

<Performer/pr/pfFont.h>

pfFBState

<Performer/pr/pfFBState.h>

pfGeoSet
pfHit

<Performer/pr/pfGeoSet.h>

pfGeoState

<Performer/pr/pfGeoState.h>

pfHighlight

<Performer/pr/pfHighlight.h>

pfIBRtexture

<Performer/pr/pfIBRtexture.h>

pfPassList

<Performer/pr/pfPassList.h>

pfLPointState

<Performer/pr/pfLPointState.h>

pfLight
pfLightModel

<Performer/pr/pfLight.h>

pfMaterial

<Performer/pr/pfMaterial.h>

pfSprite

<Performer/pr/pfSprite.h>

pfState

<Performer/pr/pfState.h>

pfString

<Performer/pr/pfString.h>

pfTexture
pfTexGen
pfTexEnv

<Performer/pr/pfTexture.h>


Table 26-5. Header Files for Other libpr Classes

libpr Class

Include File

pfCycleBuffer
pfCycleMemory

<Performer/pr/pfCycleBuffer.h>

pfDataPool

<Performer/pr/pfDataPool.h>

pfEngine

<Performer/pr/pfEngine.h>

pfFile

<Performer/pr/pfFile.h>

pfFlux

<Performer/pr/pfFlux.h>

pfSphere
pfBox
pfCylinder
pfPolytope
pfFrustum
pfSeg
pfSegSet

<Performer/pr/pfGeoMath.h>

pfVec2
pfVec3
pfVec4
pfMatrix
pfQuat
pfMatStack

<Performer/pr/pfLinMath.h>

pfList

<Performer/pr/pfList.h>

pfMemory

<Performer/pr/pfMemory.h>

pfObject

<Performer/pr/pfObject.h>

pfQueue

<Performer/pr/pfQueue.h>

pfStats

<Performer/pr/pfStats.h

pfType

<Performer/pr/pfType.h

pfWindow

<Performer/pr/pfWindow.h

pfVideoChannel

<Performer/pr/pfVideoChannel.h>

There are additional C++ object classes in the utility libraries. Header files for those classes are similarly named with their own library name for the directory and prefix for the header file name. The libpui C++ has a full C++ API and its header files are named like the example <Performer/pfui/pfiTrackball.h>. The libpfutil library has some C++ classes and the header files are named as <Performer/pfu/pfuProcessManager.h>.

Creating and Deleting OpenGL Performer Objects

The OpenGL Performer base classes all provide operator new and operator delete. All libpr and libpf objects, except pfObject, pfMemory, and their derivatives, 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.

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


Note: Public structs such as pfVec3, pfSphere, etc. may be deleted either with pfDelete() or the delete operator.

The default new operator creates objects in the current shared memory arena if one exists. libpr objects and public structures 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 acreate().

Example 26-1. Valid Creation of Objects in C++

// valid creation of libpf objects
pfDCS    *dcs = new pfDCS;         // only way
 
// valid creation of libpr objects
pfGeoSet *gs = new pfGeoSet;       // from default arena
pfGeoSet *gs = new(NULL) pfGeoSet; // from heap
 
// valid 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


Example 26-2. Invalid Creation of Objects in C++

// invalid creation of libpf objects
pfDCS    *dcs = new(NULL) pfDCS;    // not in shared mem
pfDCS    *dcs = new pfDCS[10];      // array
 
// invalid creation of libpr objects
pfGeoSet *gs = new pfGeoSet[10];    // array
 
// invalid creation of public structs
pfVec3 *vert = new(NULL) pfVec3[10];// array, non-default new


Caution: This last item in Example 26-2 is invalid because C++ does not provide a mechanism to delete arrays of objects allocated with a new operator defined to take additional arguments, for example, operator new(size_ts, 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.



Invoking Methods on OpenGL Performer Objects

Since libpr and libpf objects are allocated, they can only be maintained by reference.

Passing Vectors and Matrices to Other Libraries

Passing arrays of floats is very common in graphics programming. Calls to 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; the following is an example:

     pfMatrix ident;
     pfMakeIdentMat(ident);
     glLoadMatrix(ident);

In the C++ API, the data field of the pfMatrix must be passed instead, as in this example:

     pfMatrix ident;
     ident.makeIdent();
     glLoadMatrix(ident.mat);

Porting from C API to C++ API

When compiled with C++, OpenGL Performer supports three usages of the API:

  1. Pure C++ API. This is the default style of usage.

  2. Pure C API. This can be achieved by defining the token PF_CPLUSPLUS_API to be 0, for example, by adding the following line in source files before they include any OpenGL Performer header files:

    #define PF_CPLUSPLUS_API 0
    

    In this mode all data types are the same as when compiling with C.

  3. C++ API and C API. This mode can be enabled by defining the token PF_C_API to be 1, for example, by adding the following line in source files before they include any OpenGL Performer header files:

    #define PF_C_API 1
    

    In this mode, both C++ and C functions are available and data types are C++. See the section below concerning passing certain data types.

Typedefed Arrays Versus Structs

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 C API 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 following C API routine 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];
     }

Two possible alternatives follow:

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

Interface Between C and C++ API Code

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 for IRIX and Linux and in %PFROOT%\Include\prmath.h for Microsoft Windows.

#if PF_CPLUSPLUS_API
#define 
PFVEC2 pfVec2&
#define 
PFVEC3 pfVec3&
#define 
PFVEC4 pfVec4&
#define 
PFQUAT pfQuat&
#define 
PFMATRIX pfMatrix&
#else
#define PFVEC2 pfVec2
#define PFVEC3 pfVec3
#define PFVEC4 pfVec4
#define PFQUAT pfQuat
#define PFMATRIX pfMatrix
#endif /* PF_CPLUSPLUS_API */

These macros are used in the C API prototypes for OpenGL Performer that pass typedefed arrays, as shown in the following example:

   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, because a pointer to a struct is passed in the same manner as a pointer to an array. This is shown in the following example:

     extern void pfFontCharSpacing(pfFont *font, int ascii,
         pfVec3 *spacing);

Subclassing pfObjects

With the C API, the main mechanism for extending the functionality of the classes provided in OpenGL 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 OpenGL Performer. Subclassing allows additional member data fields and functions to be added to OpenGL Performer classes. At its 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 OpenGL Performer.

Initialization and Type Definition

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 26-6.  Data and Functions Provided by User Subclasses

Class Data or Function

Function

static void init()

Initializes 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 for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfiv for Microsoft Windows) is a subclass of pfDCS. Its initialization function merely enters the class into the type system.

Example 26-3. Class Definition for a Subclass of pfDCS

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 following section, the initialization function, Rotor::init() should be called before pfConfig().

Defining Virtual Functions

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 “Multiprocessing and libpf Objects”.

Example 26-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++ OpenGL 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.


Accessing Parent Class Data Members

Accesses to parent class data is made through the functions on the parent class. Data members on built-in classes should never be accessed directly.

Multiprocessing and Shared Memory


Note: This section does not pertain to Microsoft Windows.


Initializing Shared Memory

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:

  1. Call pfInit(). This initializes the type system and, for libpf applications, sets up shared memory.

  2. Call the init function for utility libraries that you are using if you use their C++ API. This includes pfuInit() for libpfutil and pfiInit() for libpfui.

  3. Initialize any application-supplied classes:

    1. Load any application-specific C++ DSOs.

    2. Call pfdInitConverter() to initialize and load any converter DSOs and allow those DSOs to initialize any potential C++ classes.

    3. Enter any user-supplied pfTypes into the type system; for example, call this function:

      Rotor::init()
      

  4. Call pfConfig(). This forks off other processes as specified by pfMultiprocess(). New pfTypes created after this point cannot be used in any forked processes.

  5. Create libpf and libpr objects.


    Note: Pure libpr applications that do their own multiprocessing outside of OpenGL Performer with fork() should explicitly create shared memory with pfInitArenas() before calling pfInit(). Otherwise, the type system will not be visible in the address space of other processes.


More on Shared Memory and the Type System


Note: This is an advanced section.

OpenGL 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, for example, 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 OpenGL Performer objects after pfConfig().

Virtual Address Spaces and Virtual Functions


Note: This is an advanced section.

When using virtual functions, it is 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 OpenGL 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 is 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.

Data Members and Shared Memory

Non-static Member Data

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.

Static Member Data

Classes having static data members that may change value and need to be visible from all processes should allocate shared memory for the data (for example, pfMalloc or new pfMemory) and set the static data member to point to this memory before pfConfig() as shown in the following example.

Example 26-5. Changeable Static Data Member

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; that is, call Rotor::init() before pfConfig().

Multiprocessing and libpf Objects


Note: This is an advanced topic.

The multiprocessing behavior of libpf objects (that is, 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 OpenGL 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.

If you require multibuffering of your subclassed data members, use a pfCycleBuffer or a pfFlux to hold this data.

Performance Hints

Constructor Overhead

It is 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 that contain arrays of structs; for example, pfSegSet contains an array of pfSegs.

Math Operators

Assignment operators, for example, “+=”, are significantly faster than their corresponding binary operators, for example, “+”, because the latter involves constructing a temporary object for the return value.