Chapter 1. OpenGL Performer Programming Interface

This chapter describes the fundamental ideas behind the OpenGL Performer programming interface in the following sections:

General Naming Conventions

The OpenGL Performer application programming interface (API) uses naming conventions to help you understand what a given command will do and even predict the appropriate names of routines for desired functionality. Following similar naming practices in the software that you develop will make it easier for you and others on your team to understand and debug your code.

The API is largely object-oriented; it contains classes of objects comprised of methods that do the following:

  • Configure their parent objects.

  • Apply associated operations, based on the current configuration of the object.

Both C and C++ bindings are provided for OpenGL Performer. In addition, naming conventions provide a consistent and predictable API and indicate the kind of operations performed by a given command.

Prefixes

The prefix of the command tells you in which library a C command or C++ class is found. All exposed OpenGL Performer base library C commands and C++ classes begin with 'pf'. The utility libraries use an additional prefix letter, such as 'pfu' for the libpfutil general utility library, 'pfi' for the libpfui input handling library, and 'pfd' for the libpfdu database utility library. libpr-level commands still have the 'pf' prefix as they are still in the main libpf library

Header Files

Each OpenGL Performer library contains a main header file in /usr/include/Performer on IRIX and Linux and in %PFROOT%\Include on Microsoft Windows that contains type and class definitions, the C API for that library, and global routines that are part of the C and C++ API. libpf is broken into two distinct pieces: the low-level rendering layer, libpr, and the application layer, libpf, and each has its own main header file: pr.h and pf.h. Since libpf is considered to include libpr, pf.h includes pr.h. C++ class header files are found under the following directories:

/usr/include/Performer/{pf, pr, ...} (IRIX and Linux)
%PFROOT%\Include\{pf, pr, ...} (Microsoft Windows)

Each class has its own C++ header file and that header must be included to use that class.

#include <Performer/pf.h>
#include <Performer/pf/pfGroup.h>
.....
pfGroup *group;

Naming in C and C++

All C++ class method names have an expanded C counterpart. Typically, the C routine ( function)will include the class name in the routine, whereas the C++ method will not.

C: pfGetPipeScreen();
C++: pipe->getScreen();

For some very general routines on the most abstract classes, the class name is omitted. This is the case with the child API on pfNodes:

C: pfAddChild(node,child);
C++: node->addChild(child);

Command and type names are mixed case where the first letter of a new word in a name is capitalized. C++ method names always start with a lower case letter.

pfTexture *texture;
texture->loadFile();

Abbreviations

Type names do not use abbreviations. The C API acting on that type will often use abbreviations for the type names, as will the associated tokens and enums.

In procedure names, a name will always be abbreviated or never, and the same abbreviation will always be used and will be in the pfNew* C command. For example: the pfTexture object uses `Tex' in its API, such as pfNewTex(). If a type name has multiple words, the abbreviation will use the first letter of the first words and then the first syllable of the last word.

pfPipeWindow *pwin = pfNewPWin();
pfPipeVideoChannel *pvchan = pfNewPVChan();
pfTexLOD *tlod = pfNewTLOD();

Macros, Tokens, and Enums

Macros, tokens, and enums all use full upper-case. Token names associated with a class and methods of a class start with the abbreviated name for that class, such as texture to “tex” in PFTEX_SHARPEN.

Class API

The API of a given class, such as pfTexture, is comprised of the following:

  • API to create an instance of the object

  • API to set parameters on the object

  • API to get those parameter settings

  • API to perform actions on the configured object

O bject Creation

Objects are always created with the following:

C: pfThing *thing = pfNewThing();
C++: pfThing *thing = new pfThing;

libpf objects are automatically created out of the shared memory arena. libpr objects take as an argument an arena pointer which, if NULL, will cause allocation off the heap.

Set Routines

A set routine has the following form:

C: pfThingParam(thing, ... ) 
C++: thing->setParam()

Note that there is no `Set' in the name in the C version.

Set routines are usually very fast and are not order dependent. Work required to process the settings happens once when the object is first used after settings have changed. If particularly expensive options must be done, there will be a pfConfigThing routine or method to explicitly force this work that must be called before the object is to be used.

Get Routines

For every `set' routine there is a matching `get' routine to get back the value that was set.

C: pfGetThingParam(thing, ... ) 
C++: thing->getParam()

If the set/get is for a single value, that value is usually the return value of the routine. If there are multiple values together, the `get' routine will then take as arguments pointers to result variables.

Getting Current In-Use Values

Get routines return values that have been previously set by the user, or default values if no settings have been made. Sometimes a value other than the user-specified value is currently in use and that is the value that you would like to get. For these cases, there is a separate `GetCur' routine to get the current in-use value.

C:   pfGetCurThingParam() 
C++: thing->getcurParam()

These `cur' routines may only be able to give reasonable values in the process which associated operations are happening. For example, to get the current texture (pfGetCurTex()), you need to be in the draw process since that is the only process that has a current texture.

Action Routines

An action routine has the following form:

C: pfVerbThing(), such as pfApplyTex()
C++: thing->verb(), such as tex->apply()

Action routines can have parameter scope and apply only to that parameter. These routines have the following form

C: pfVerbThingParam(), such as pfApplyTexMinLOD()
C++: thing->verbParam(), such as tex->applyMinLOD()

Apply and Draw Routines

The Apply and Draw action routines do graphics operations and must happen either in the draw process or in display list mode.

C: pfApplypfGState()
pfDrawGSet()
C++: gstate->apply()
gset->draw()

Enable and Disable of Modes

Features that can be enabled and disabled are done so with pfEnable() and pfDisable(), respectively.

pfGetEnable() takes PFEN_* tokens naming the graphics state operation to enable or disable. A GetEnable() is used to query enable status and will return 1 or 0 if the given mode is enabled or disabled, respectively.

ex: pfEnable(PFEN_TEXTURE), pfDisable(PFEN_TEXTURE),
pfGetEnable(PFEN_TEXTURE);

Mode, Attribute, or Value

Classes instances are configured by having their internal fields set. These fields may be simple modes or complex attribute structures. Mode values are ints or tokens, attributes are typically pointers to objects, and values are floats.

pfGStateMode(gstate, PFSTATE_DECAL, PFDECAL_LAYER)
pfGStateAttr(gstate, PFSTATE_TEXTURE, texPtr)
pfGStateVal(gstate, PFSTATE_ALPHAREF, 0.5)

Base Classes

OpenGL Performer provides an object-oriented programming interface to most of its data structures. Only OpenGL Performer functions can change the values of elements of these data structures; for instance, you must call pfMtlColor() to set the color of a pfMaterial structure rather than modifying the structure directly.

For a more transparent type of memory, OpenGL Performer provides pfMemory. All object classes are derived from pfMemory. pfMemory instances must be explicitly allocated with the new operator and cannot be allocated statically, on the stack, or included directly in other object definitions. pfMemory is managed memory; it includes special fields, such as size, arena, and ref count, that are initialized by the pfMemory new() function.

Some very simple and unmanaged data types are not encapsulated for speed and easy access. Examples include pfMatrix, pfSphere and pfVec3. These data types are referred to as public structures and are inherited from pfStruct.

Unlike pfMemory, pfStructs can be handled as follows:

  • Allocated statically

  • Allocated on the stack

  • Included directly in other structure and object definitions

pfStructs allocated off the stack or allocated statically are not in the shared memory arena and thus are not safe for multiprocessed use. Also, pfStructs allocated off the stack in a procedure do not exist after the procedure exits so they should not be given to persistent objects, such as a pfVec3 array of vertices for a pfGeoSet.

In order to allow some functions to apply to multiple data types, OpenGL Performer uses the concept of class inheritance. Class inheritance takes advantage of the fact that different data types (classes) often share attributes. For example, a pfGroup is a node that can have children. A pfDCS (Dynamic Coordinate System) has the same basic structure as a pfGroup, but also defines a transformation to apply to its children—in other words, the pfDCS data type inherits the attributes of the pfGroup and adds new attributes of its own. This means that all functions that accept a pfGroup* argument will alternatively accept a pfDCS* argument.

For example, pfAddChild() takes a pfGroup* argument, but appends child to the list of children belonging to dcs:

pfDCS *dcs = pfNewDCS();
pfAddChild(dcs, child);

Because the C language does not directly express the notion of classes and inheritance, arguments to functions must be cast before being passed, as shown in this example:

pfAddChild((pfGroup*)dcs, (pfNode*)child);

In the example above, no such casting is required because OpenGL Performer provides macros that perform the casting when compiling with ANSI C, as shown in this example:

#define pfAddChild(g, c) pfAddChild((pfGroup*)g, (pfNode*)c)


Note: Using automatic casting eliminates type checking—the macros will cast anything to the desired type. If you make a mistake and pass an unintended data type to a casting macro, the results may be unexpected.

No such trickery is required when using the C++ API. Full type checking is always available at compile time.

Inheritance Graph

The relations between classes can be arranged in a directed acyclic inheritance graph in which each child inherits all of its parent's attributes, as illustrated in Figure 1-1. OpenGL Performer does not use multiple inheritance, so each class has only one parent in the graph.


Note: It is important to remember that an inheritance graph is different from a scene graph. The inheritance graph shows the inheritance of data elements and member functions among user-defined data types; the scene graph shows the relationship among instances of nodes in a hierarchical scene definition.

Figure 1-1. Partial Inheritance Graph of OpenGL Performer Data Types

Partial Inheritance Graph of OpenGL Performer Data Types

OpenGL Performer objects are divided into two groups: those found in the libpf library and those found in the libpr library. These two groups of objects have some common attributes, but also differ in some respects.

While OpenGL Performer only uses single inheritance, some objects encapsulate others, hiding the encapsulated object but also providing a functional interface that mimics its original one. For example a pfChannel has a pfFrustum, a pfFrameStats has a pfStats, a pfPipeWindow has a pfWindow, and a pfPipeVideoChannel has a pfVideoChannel. In these cases, the first object in each pair provides functions corresponding to those of the second. For example, pfFrustum has a routine:


pfMakeSimpleFrust(frust, 45.0f);

pfChannel has a corresponding routine:


pfMakeSimpleChan(channel, 45.0f);

libpr and libpf Objects

All of the major classes in OpenGL Performer are derived from the pfObject class. This common, base class unifies the data types by providing common attributes and functions. libpf objects are further derived from pfUpdatable. The pfUpdatable abstract class provides support for automatic multibuffering for multiprocessing. pfObjects have no special support for multiprocessing and so all processes share the same copy of the pfObject in the shared arena. libpr objects allocated from the heap are only visible in the process in which they are created or in child processes created after the object. Changes made to such an object in one process are not visible in any other process.

Explicit multibuffering of pfObjects is available through the pfFlux class. In general, libpr provides lightweight and low-level modular pieces of functionality that are then enhanced by more powerful libpf objects.

User Data

The primary attribute defined by the pfObject class is the custom data a user gets to define on any pfObject called “ user data.” pfUserDataSlot attaches the user-supplied data pointer to user data. pfUserData attaches the user-supplied data pointer to user data slot. Example 1-1 shows how to use user data.

Example 1-1. How to Use User Data

typedef struct
    {
      float coeffFriction;
      float density;
      float *dataPoints;
    }
    myMaterial;
 
    myMaterial     *granite;
 
    granite = (myMaterial *)pfMalloc(sizeof(myMaterial), NULL);
    granite->coeffFriction = 0.5f;
    granite->density = 3.0f;
    granite->dataPoints = (float *)pfMalloc(sizeof(float)*8, NULL);
    graniteMtl = pfNewMtl(NULL);
 
    pfUserData(graniteMtl, granite);


pfDelete() and Reference Counting

Most kinds of data objects in OpenGL Performer can be placed in a hierarchical scene graph, using instancing when an object is referenced multiple times. Scene graphs can become quite complex, which can cause problems if you are not careful. Deleting objects can be a particularly dangerous operation, for example, if you delete an object that another object still references.

R eference counting provides a bookkeeping mechanism that makes object deletion safe: an object is never deleted if its reference count is greater than zero.

All libpr objects (such as pfGeoState and pfMaterial) have a reference count that specifies how many other objects refer to it. A reference is made whenever an object is attached to another using the OpenGL Performer routines shown in Table 1-1.

Table 1-1. Routines that Modify libpr Object Reference Counts

Routine

Action

pfGSetGState ()

Attaches a pfGeoState to a pfGeoSet.

pfGStateAttr ()

Attaches a state structure (such as a pfMaterial) to a pfGeoState.

pfGSetHlight ()

Attaches a pfHighlight to a pfGeoSet.

pfTexDetail ()

Attaches a detail pfTexture to a base pfTexture.

pfGSetAttr ()

Attaches attribute and index arrays to a pfGeoSet.

pfTexImage ()

Attaches an image array to a pfTexture.

pfAddGSet() , pfReplaceGSet() , pfInsertGSet()

Modify pfGeoSet/pfGeode association.

When object A is attached to object B, the reference count of A is incremented. Additionally, if A replaces a previously referenced object C, then the reference count of C is decremented. Example 1-2 demonstrates how reference counts are incremented and decremented.

Example 1-2. Objects and Reference Counts

pfGeoState *gstateA, *gstateC;
pfGeoSet *gsetB;
 
/* Attach gstateC to gsetB. Reference count of gstateC
 * is incremented. */
pfGSetGState(gsetB, gstateC);	
 
/* Attach gstateA to gsetB, replacing gstateC. Reference
 * count of gstateC is decremented and that of gstateA
 * is incremented. */
pfGSetGState(gsetB, gstateA);	

This automatic reference counting done by OpenGL Performer routines is usually all you will ever need. However, the routines pfRef(), pfUnref(), and pfGetRef() allow you to increment, decrement, and retrieve the reference count of a libpr object should you wish to do so. These routines also work with objects allocated by pfMalloc().

An object whose reference count is equal to 0 can be deleted with pfDelete(). pfDelete() works for all libpr objects and all pfNodes but not for other libpf objects like pfPipe and pfChannel. pfDelete() first checks the reference count of an object. If the reference count is nonpositive, pfDelete() decrements the reference count of all objects that the current object references, then it deletes the current object. pfDelete() does not stop here but continues down all reference chains, deleting objects until it finds one whose count is greater than zero. Once all reference chains have been explored, pfDelete returns a boolean indicating whether it successfully deleted the first object or not. Example 1-3 illustrates the use of pfDelete() with libpr.

Example 1-3. Using pfDelete() with libpr Objects

pfGeoState *gstate0, *gstate1;
pfMaterial *mtl;
pfGeoSet *gset;
 
gstate0 = pfNewGState(arena); /* initial ref count is 0 */
gset = pfNewGSet(arena); /* initial ref count is 0 */
mtl = pfNewMtl(arena); /* initial ref count is 0 */
 
/* Attach mtl to gstate0. Reference count of mtl is
 * incremented. */
pfGStateAttr(gstate0, PFSTATE_FRONTMTL, mtl);
 
/* Attach mtl to gstate1. Reference count of mtl is
 * incremented. */
pfGStateAttr(gstate1, PFSTATE_FRONTMTL, mtl);
 
/* Attach gstate0 to gset. Reference count of gstate0 is
 * incremented. */
pfGSetGState(gset, gstate0);
 
/* This deletes gset, gstate0, but not mtl since gstate1 is
 * still referencing it. */
pfDelete(gset);

Example 1-4 illustrates the use of pfDelete() with libpf.

Example 1-4. Using pfDelete() with libpf Objects

pfGroup *group;
pfGeode *geode;
pfGeoSet *gset;
 
group = pfNewGroup(); /* initial parent count is 0 */
geode = pfNewGeode(); /* initial parent count is 0 */
gset = pfNewGSet(arena); /* initial ref count is 0 */
 
/* Attach geode to group. Parent count of geode is
 * incremented. */
pfAddChild(group, geode);
 
/* Attach gset to geode. Reference count of gset is
 * incremented. */
pfAddGSet(geode, gset);
 
/* This has no effect since the parent count of geode is 1.*/
pfDelete(geode);
 
/* This deletes group, geode, and gset */
pfDelete(group);

Some notes about reference counting and pfDelete():

  • All reference count modifications are locked so that they guarantee mutual exclusion when multiprocessing.

  • Objects added to a pfDispList do not have their counts incremented due to performance considerations.

  • In the multiprocessing environment of libpf, the successful deletion of a pfNode does not have immediate effect but is delayed one or more frames until all processes in all processing pipelines are through with the node. This accounts for the fact that pfDispLists do not reference-count their objects.

  • pfUnrefDelete(obj) is shorthand for the following:

    if(pfUnref(obj) ==0)
        pfDelete(obj);
    

    This is true when pfUnrefGetRef is atomic.

  • Objects whose count reaches zero are not automatically deleted by OpenGL Performer. You must specifically request that an object be deleted with pfDelete() or pfUnrefDelete().

Copying Objects with pfCopy()

pfCopy() is currently implemented for libpr (and pfMalloc()) objects only. Object references are copied and reference counts are modified appropriately, as illustrated in Example 1-5.

Example 1-5. Using pfCopy()

pfGeoState *gstate0, *gstate1;
pfMaterial *mtlA, *mtlB;
 
gstate0 = pfNewGState(arena);
gstate1 = pfNewGState(arena);
mtlA = pfNewMtl(arena); /* initial ref count is 0 */
mtlB = pfNewMtl(arena); /* initial ref count is 0 */
 
/* Attach mtlA to gstate0. Reference count of mtlA is
 * incremented. */
pfGStateAttr(gstate0, PFSTATE_FRONTMTL, mtlA);	
 
/* Attach mtlB to gstate1. Reference count of mtlB is
 * incremented. */
pfGStateAttr(gstate1, PFSTATE_FRONTMTL, mtlB);	
 
/* gstate1 = gstate0. The reference counts of mtlA and mtlB
 * are 2 and 0 respectively. Note that mtlB is NOT deleted
 * even though its reference count is 0. */
pfCopy(gstate1, gstate0);

pfMalloc and the related routines provide a consistent method to allocate memory, either from the user's heap (using the C-library malloc() function) or from a shared memory arena.

Printing Objec ts with pfPrint()

pfPrint() can print many different kinds of objects to a file; for example, you can print nodes and geosets. To do so, you specify in the argument of the function the object to print, the level of verbosity, and the destination file. An additional argument, which, specifies different data according to the type of object being printed.

The different levels of verbosity include the following:

  • PFPRINT_VB_OFF—no printing

  • PFPRINT_VB_ON—minimal printing (default)

  • PFPRINT_VB_NOTICE—minimal printing (default)

  • PFPRINT_VB_INFO—considerable printing

  • PFPRINT_VB_DEBUG—exhaustive printing

If the object to print is a type of pfNode, which specifies whether the print traversal should only traverse the current node (PFTRAV_SELF) or the entire scene graph where the node specified in the argument is the root node (PFTRAV_SELF | PFTRAV_DESCEND). For example, to print an entire scene graph, in which scene is the root node, to the file, fp, with default verbosity, use the following line of code:

file = fopen (“scene.out”,”w”); 
pfPrint(scene, PFTRAV_SELF | PFTRAV_DESCEND, PFPRINT_VB_ON, fp); fclose(file);

If the object to print is a pfFrameStats, which should specify a bitmask of the frame statistics classes that you want printed. The values for the bitmask include the following:

  • PFSTATS_ON enables the specified classes.

  • PFSTATS_OFF disables the specified classes.

  • PFSTATS_DEFAULT sets the specified classes to their default values.

  • PFSTATS_SET sets the class enable mask to enmask.

For example, to print select classes of a pfFrameStats structure, stats, to stderr, use the following line of code:


pfPrint(stats, PFSTATS_ENGFX | PFFSTATS_ENDB | PFFSTATS_ENCULL,PFSTATS_ON, NULL);

If the object to print is a pfGeoSet, which is ignored and information about that pfGeoSet is printed according to the verbosity indicator. The output contains the types, names, and bounding volumes of the nodes and pfGeoSets in the hierarchy. For example, to print the contents of a pfGeoSet, gset, to stderr, use the following line of code:


pfPrint(gset, NULL, PFPRINT_VB_DEBUG, NULL);


Note: When the last argument, file, is set to NULL, the object is printed to stderr.


Determining Ob ject Type

Sometimes you have a pointer to a pfObject but you do not know what it really is—is it a pfGeoSet, a pfChannel, or something else? pfGetType() returns a pfType which specifies the type of a pfObject. This pfType can be used to determine the class ancestry of the object. Another set of routines, one for each class, returns the pfType corresponding to that class, for example, pfGetGroupClassType() returns the pfType corresponding to pfGroup.

pfIsOfType() tells whether an object is derived from a specified type, as opposed to being the exact type.

With these functions you can test for class type as shown in Example 1-6.

Example 1-6. General-Purpose Scene Graph Traverser

void
travGraph(pfNode *node)
{
    if (pfIsOfType(node, pfGetDCSClassType()))
        doSomethingTransforming(node);
 
    /* If 'node' is derived from pfGroup then recursively
     * traverse its children */
    if (pfIsOfType(node, pfGetGroupClassType()))
        for (i = 0; i < pfGetNumChildren(node); i++)
            travGraph(pfGetChild(node, i));
}

Because OpenGL Performer allows subclassing of built-in types, when decisions are made based on the type of an object, it is usually better to use pfIsOfType() to test the type of an object rather than to test for the strict equality of the pfTypes. Otherwise, the code will not have reasonable default behavior with file loaders or applications that use subclassing.

The pfType returned from pfGetType() is useful for programs but it is not in a readable form for you. Calling pfGetTypeName() on a pfType returns a null-terminated ASCII string that identifies an object's type. For a pfDCS, for example, pfGetTypeName() returns the string “pfDCS.” The type returned by pfGetType() can then be compared to a class type using pfIsOfType(). Class types can be returned by methods such as pfGetGroupClassType().