This chapter outlines the steps involved in using libpf, the visual simulation development library. The outline follows the development sequence of a skeleton application program that introduces you to the basic concepts involved in creating a visual simulation application with libpf. Each step at which more complex constructions are possible gives a cross-reference to a later section where you can learn more about the topic.
It takes only a few lines of code to set up an IRIS Performer libpf application. Furthermore, once you have an application framework that you like you can use it again to create other libpf applications.
Certain configuration and control routines are required in all applications, while others depend on the features needed and the platform for which the application is designed. The basic requirements for simple programs are the same as for more complex programs, so you can learn the basic structure from a very simple framework application and then build on it to suit your needs.
Take a few moments to browse through the introductory program, simple.c, shown in Example 3-1. If you want to compile this program, refer to the section of this chapter titled “Compiling and Linking IRIS Performer Applications.”
Example 3-1 shows the basic framework of an IRIS Performer application.
Example 3-1. Structure of an IRIS Performer Application
#include <stdlib.h>
#include <Performer/pf.h>
#include <Performer/pfutil.h>
#include <Performer/pfdu.h>
int
main (int argc, char *argv[])
{
float t = 0.0f;
pfScene *scene;
pfNode *root;
pfPipe *p;
pfPipeWindow *pw;
pfChannel *chan;
pfSphere bsphere;
if (argc < 2)
{
pfNotify(PFNFY_FATAL, PFNFY_USAGE,
“Usage: simple file.ext\n”);
exit(1);
}
/* Initialize Performer */
pfInit();
/*
* Select multiprocessing mode based on
* number of processors
*/
pfMultiprocess( PFMP_DEFAULT );
/* Load all loader DSO's before pfConfig() forks */
pfdInitConverter(argv[1]);
/*
* Initiate multi-processing mode set by pfMultiprocess
* FORKs for Performer processes, CULL and DRAW, etc.
* happen here.
*/
pfConfig();
/*
* Append to Performer search path, PFPATH, files in
* /usr/share/Performer/data
*/
pfFilePath(“.:/usr/share/Performer/data”);
/* Read a single file, of any known type. */
if ((root = pfdLoadFile(argv[1])) == NULL)
{
pfExit();
exit(-1);
}
/* Attach loaded file to a new pfScene. */
scene = pfNewScene();
pfAddChild(scene, root);
/* Create a pfLightSource and attach it to scene. */
pfAddChild(scene, pfNewLSource());
/* Configure and open graphics window */
p = pfGetPipe(0);
pw = pfNewPWin(p);
pfPWinType(pw, PFPWIN_TYPE_X);
pfPWinName(pw, “IRIS Performer”);
pfPWinOriginSize(pw, 0, 0, 500, 500);
/* Open and configure the GL window. */
pfOpenPWin(pw);
/* Create and configure a pfChannel. */
chan = pfNewChan(p);
pfChanScene(chan, scene);
pfChanFOV(chan, 45.0f, 0.0f);
/* determine extent of scene's geometry */
pfGetNodeBSphere (root, &bsphere);
pfChanNearFar(chan, 1.0f, 10.0f * bsphere.radius);
/* Simulate for twenty seconds. */
while (t < 20.0f)
{
pfCoord view;
float s, c;
/* Compute new view position. */
t = pfGetTime();
pfSinCos(45.0f*t, &s, &c);
pfSetVec3(view.hpr, 45.0f*t, -10.0f, 0);
pfSetVec3(view.xyz, 2.0f * bsphere.radius * s,
-2.0f * bsphere.radius *c,
0.5f * bsphere.radius);
pfChanView(chan, view.xyz, view.hpr);
/* Initiate cull/draw for this frame. */
pfFrame();
}
/* Terminate parallel processes and exit. */
pfExit();
}
|
If you would like to compile simple.c and try it out, use the copy in /usr/share/Performer/src/pguide/libpf/C; the Makefile in that directory provides all the necessary compilation options. (For more information about IRIS Performer compiler options, see the “Compiling and Linking IRIS Performer Applications” section of this chapter.) Once you've compiled the code, try executing it with some of the sample data files in /usr/share/Performer/data, such as blimp.flt or sampler.nff.
Here's a description of the steps involved in a simple IRIS Performer application. Refer to the sample code in Example 3-1 as you read through these steps.
Include the necessary system header files.
#include <stdlib.h> |
Include the relevant IRIS Performer header files.
#include <Performer/pf.h> #include <Performer/pfutil.h> #include <Performer/pfdu.h> |
Declare variables for the required elements.
| pfScene | a scene graph to be rendered on a channel | |
| pfPipe | a graphics pipeline to perform the rendering | |
| pfChannel | a view to be rendered on the designated pipe |
You can configure IRIS Performer to use multiple scenes, multiple pipes (if your system has them), and multiple channels per pipe. (See “Using Multiple Channels” in Chapter 4.)
pfInit(); |
This sets up the shared-memory arena used for multiprocessing, initializes the high-resolution clock, and resets IRIS Performer's state.
pfConfig(); |
This configures the number of pipes and starts processes based on the selected multiprocessing model. The code in Example 3-1 uses the defaults: a single pipe and a multiprocessing model that is tailored to the number of processors on the machine.
root = pfdLoadFile(argv[1]) |
pfdLoadFile() loads a database from the disk using whichever file importer seems appropriate (based on the three-letter extension at the end of the given filename). There are other ways to set up scenes, too; for instance, you can call a specific importing routine in place of pfdLoadFile() if you want to load only databases of a particular format, or you can create geometric objects directly using libpr and place them in a database hierarchy. See “Geometry” in Chapter 10 for information on constructing pfGeoSets, and “Scene Graph Hierarchy” in Chapter 6 for information on creating a scene graph.
Create a new scene for the channel to draw.
scene = pfNewScene(); |
Add the root of the database that you loaded or created in step 6 to the scene.
pfAddChild(scene, root); |
Initialize a graphics-rendering pipeline.
p = pfGetPipe(0); pw = pfNewPWin(p); pfPWinType(pw, PFPWIN_TYPE_X); pfPWinName(pw, “IRIS Performer”); pfPWinOriginSize(pw, 0, 0, 500, 500); /* Open and configure the graphics window. */ pfOpenPWin(pw); |
This sets up a callback to open a graphics library window, sized and positioned as specified in OpenPipeWin().
Specify the frame rate and the synchronization method.
Because neither a frame rate nor a synchronization method is specified in simple.c, the application “free runs” without frame-rate control, which is the default. See “Frame Rate and Synchronization” and Chapter 7, “Frame and Load Control,” for more information on controlling frame rates.
Create a channel on the specified pipe.
chan = pfNewChan(p); |
A channel is a viewport into a pipe. Because simple.c doesn't configure any screen dimensions for the channel, it renders to the full window of the pipe.
Configure the channel: set the viewpoint, field-of-view (FOV), and near and far clipping planes (based on the size of the scene).
pfChanScene(chan, scene); pfChanFOV(chan, 45.0f, 0.0f); pfGetNodeBSphere (root, &bsphere); pfChanNearFar(chan, 1.0f, 10.0f * bsphere.radius); |
When you pass in zero as a field of view—in this case, the vertical FOV—IRIS Performer matches the FOV to the aspect ratio of the channel.
Render the scene repeatedly until the specified time is up.
Set up the viewpoint for the next frame:
pfChanView(chan, view.xyz, view.hpr); |
Initiate the next cull/draw cycle to render the frame:
pfFrame(); |
When the time is up, exit IRIS Performer.
pfExit(); |
The remainder of this chapter discusses portions of the outline in detail. You may find it helpful to continue to refer to simple.c while you read the following sections.
This section describes how to set up the basic requirements of an IRIS Performer libpf application.
The header files for the IRIS Performer libraries are in the /usr/include/Performer directory. They include pf.h and pr.h (header files for libpf and libpr, respectively), irisgl.h and opengl.h (to set up declarations for whichever graphics library your application uses), and other header files for use with the other IRIS Performer libraries.
The header files contain useful macros as well as function declarations, including macros for transparently casting a variable from one data type to another. (ANSI C requires that expressions used as function arguments be cast to match function prototypes.) Some routines therefore accept more than one type of argument, with automatic casting between usable types. For example, a routine accepting a pfGroup as an argument can also take a pfSwitch. In the code below, switch is automatically cast to a pfGroup* and geode is automatically cast to a pfNode* by a macro within pf.h:
pfGeode *geode; pfSwitch *switch; pfAddChild(switch, geode); |
Before you can set up a pipe, you have to set up any areas of shared memory that you intend to use, and you have to determine how many processors to use (and in what configuration).
IRIS Performer uses shared memory to share data among the application, the visibility cull traversal, and the draw traversal, all of which can run in parallel on different processors. pfInit() sets up the shared memory arena from which libpf objects are allocated. The shared memory arena uses either swap space or a file in the directory specified by the environment variable PFTMPDIR. For more information on shared memory arenas, see “Memory Allocation” in Chapter 10.
pfConfig() starts up multiple processes, which allow visibility culling and drawing to run in parallel with the application process. The number of processes created depends on the process model (specified by a call to pfMultiprocess()), the number of processors, and the number of pipes (one by default; call pfMultipipe() to specify more than one pipe). The order of the calls is important—pfMultiprocess() and pfMultipipe() are effective only if called between pfInit() and pfConfig().
The default is a single pipe running with one, two (separate draw process), or three (separate cull and draw processes) processes, depending on the number of processors on the machine. When you run the application from the root login account, pfConfig() also sets nondegradable priorities for the processes to improve the consistency of the run-time behavior.
For information on controlling multiple pipes, see “Using Pipes” in Chapter 4. For information on multiprocessing, see “Successful Multiprocessing With IRIS Performer” in Chapter 7.
In addition to setting up shared memory, pfInit() initializes a high-resolution clock by calling pfInitClock(). Depending on the hardware, this may start up a process to service the clock. The clock process consumes few system resources because it sleeps most of the time.
A pfPipe variable (also called a pipe) represents an IRIS Performer software graphics pipeline. You gain access to a pipe using pfGetPipe(); for instance,
p = pfGetPipe(0); |
sets p to point to the IRIS Performer graphics pipeline numbered zero.
IRIS Performer maintains its own representation of the global graphics state. Therefore, changes that you make to the graphics state using graphics library (IRIS GL or OpenGL) commands can create inconsistencies. IRIS Performer provides state management routines that let you manipulate both the graphics library state and the IRIS Performer state. When you want to change graphics states, use these routines rather than their graphics library counterparts.
See “Using Pipes” in Chapter 4 for more information on configuring graphics on a pipe and “Graphics State” in Chapter 10 for more information on controlling the graphics state.
The frame rate is the number of times per second the application intends to draw the scene. The period for a frame must be an integer multiple of the video vertical retrace period, which is typically 1/60th of a second. Thus, with a 60 Hz video rate, possible frame rates are 60 Hz, 30 Hz, 20 Hz, 15 Hz, and so on. simple.c doesn't specify a frame rate, so it attempts to free run at the default rate of 60 Hz.
The synchronization mode or phase defines how the system behaves if drawing takes more than the requested time. Free-running mode (the default) is useful for applications that don't require a fixed frame rate. pfSync() delays the application until the next appropriate frame boundary.
See Chapter 7, “Frame and Load Control,” to learn more about frame rates, phase, and synchronization modes.
A channel is a rendering viewport into a pipe. A pipe can have many channels within it, but by default a channel occupies the full window of a pipe. You can tell the channel to use a portion of the window using pfChanViewport():
pfChanViewport(chan, left, right, bottom, top); |
Channels support the standard viewing concepts such as eyepoint, view direction, field of view, and near and far clipping planes.
For displays using multiple adjacent screens, you can slave channels together to a single viewpoint. You can also use channels to control scene management functions such as the switching of level-of-detail models based on graphics stress and pixel size.
See “Using Channels” in Chapter 4 to learn more about setting up channels.
Databases exist in a variety of formats. IRIS Performer doesn't define a file format for databases; instead, it supports extensible run-time scene definitions of sufficient generality to support many database formats. Source code for several file importers is included with IRIS Performer; the provided importers are described in Chapter 9, “Importing Databases.”
You can create a database with any modeler, or write your own modeler using libpr routines. If you use a modeler that has its own database format, you can develop a file importer for it by modifying one of the sample importers. See Chapter 9, “Importing Databases,” for more information about import routines.
If you write your own modeler using libpr routines, you don't have to convert the data structures for libpf to be able to use them. In this case, you create a database by using a series of calls to construct geometry in pfGeoSets, to define state and texture definitions in pfGeoStates, and to construct a scene graph of pfNodes. The sample programs in Chapter 10, “libpr Basics,” show how to construct simple geometry. The source code for the sample importers demonstrate the construction of more complex scenes.
Database files are often scattered about a file system, making file-loading operations tedious. IRIS Performer provides a general mechanism for defining multiple search paths.
When IRIS Performer attempts to open a file, it first tries the name as specified. If that fails, it begins to search for the file using a search path, which specifies where to look for data.
You can specify a search path using pfFilePath(path) or with the environment variable PFPATH. You can specify any number of directories in this way. The search path consists of a colon (:) separated list of absolute or relative path names of the directories where data might reside. Directories are searched in the order given, beginning with those specified in PFPATH, followed by those specified by pfFilePath().
For example, the following function call tells IRIS Performer to search for data first in the current directory, then in the data directory within the current directory, and then in data directories one and two levels above the current directory:
pfFilePath(".:./data:../data:../../data");
|
Calls to pfFindFile() with the name of the file you want to locate return the complete pathname of the file if the result of the search is successful.
After the pipes and channels are configured and the scene is loaded, the main simulation loop begins and manages scene updates, viewpoint updates, scene intersection inquiries, and image generation.
The loop has two principal control calls: pfSync() and pfFrame().
The order of operations is this:
Call pfSync() to put the process to sleep until the next frame boundary. This step is typically only used when viewpoint information is being updated from a streaming input device such as a head-tracker.
Perform latency-critical operations such as setting the viewpoint or reading positional input/output devices.
Call pfFrame() to initiate the next cull traversal.
Perform any time-consuming calculations that are required.
Return to step 1.
Time-consuming operations such as intersection inquiries and simulator dynamics computations that are performed in the main simulation loop should go after pfFrame() but before pfSync(). If these calculations are done after pfSync() but before pfFrame(), the calculations can delay the start of the cull process and thereby reduce the time available for the cull traversal on multiprocessor systems.
This chapter doesn't specifically discuss performance tuning (see Chapter 13, “Performance Tuning and Debugging,” for detailed information on that topic), but every IRIS Performer-based application should be written with performance in mind. Speed is not something that you can easily build into an application as a last-minute addendum; it's something that you need to consider as you structure your database, as you decide what needs to happen in your main loop, and so on—during the design of your program rather than after debugging it. Once you understand the basics of building a visual simulation application, you should go on to learn how to enhance performance by reading Chapter 13. It might be useful to skim Chapter 3 before reading any further, so that as you read more about the details of building an application with IRIS Performer you'll have performance issues in mind from the start.
This section describes how to compile and link IRIS Performer applications.
The following libraries are required when linking an executable:
| libpf | IRIS Performer visual simulation development library. Comes in two version: libpf_ogl and libpf_igl, for OpenGL and IRIS GL respectively. | |
| libpr | IRIS Performer high-performance rendering library. Exists in both OpenGL and IRIS GL versions, and is contained within the corresponding |
libpf: libpf_ogl and libpf_igl, for OpenGL and IRIS GL respectively.
| libpfdu | IRIS Performer database library; does file handling, and includes importers for a variety of data formats. Comes in two version: libpfdu_ogl and libpfdu_igl, for OpenGL and IRIS GL respectively. | |
| libpfutil | IRIS Performer utilities library; includes the window-related functions. Comes in two version: libpfutil_ogl and libpfutil_igl, for OpenGL and IRIS GL respectively. | |
| libimage | Image library—required by libpr. | |
| libGLU | OpenGL utilities library—required by libpr with OpenGL. | |
| libGL | OpenGL graphics library; either this or the alternative libgl (the IRIS GL graphics library) is required by libpf and libpr. | |
| libXext |
| |
| libGLw | OpenGL widget library, for using OpenGL with IRIS IM. | |
| libXm | IRIS IM library; used for “Silicon Graphics look” windows. | |
| libXt | X toolkit intrinsics library; used by IRIS IM. | |
| libXmu |
| |
| libX11 | X Window System\xb8 ™ library—required by libgl and libpr. | |
| libfm | IRIS GL font manager—required by libpfutil with IRIS GL. | |
| libm | Math library—required by libpr. | |
| libfpe | Floating point exception library—required by libpr. | |
| libmalloc | Memory allocation library. | |
| libC | C++ library—required by libpf. |
For an IRIS GL application using all of the libraries included with IRIS Performer, the link line should include:
-lpfdu_igl -lpfui -lpfutil_igl -lpf_igl -limage -lgl -lXmu -lX11 -lm -lfpe -lfm -lmalloc -lC |
The corresponding line for an OpenGL application would be:
-lpfdu_ogl -lpfui -lpfutil_ogl -lpf_ogl -limage -lGLU -lGL -lXext -lXmu -lX11 -lm -lfpe -lmalloc -lC |
The standard libraries for IRIS Performer are distributed as dynamic shared objects. Compared with static libraries, DSOs produce smaller applications and allow sharing between multiple executables that are running simultaneously. However, if you build an application using a DSO, that DSO must be present on the target system at run time. The DSOs for IRIS Performer 2.0 are in the performer_eoe subsystem on the IRIS Performer CD-ROM.
IRIS Performer also ships with the its libraries in different forms that might be useful to developers. The debug versions are primarily intended for bug reporting because they contain more symbol table information than the optimized versions. The static versions are for use when distributing an application to customers who may not have performer_eoe installed. If you want to ensure that your customers will have all the libraries they need, you should use static linking. Debug DSO, static optimized and static debug versions of the libraries can be found in optional subsystems and are installed under the directories /usr/lib/Performer/Debug, /usr/lib/Performer/Static and /usr/lib/Performer/StaticDebug, respectively. The “-L” option to cc, CC or ld can be used to link with the static libraries. Use of the standard DSO or debug DSO is determined at run time through the environment variable LD_LIBRARY_PATH.
![]() | Note: See Chapter 9, “Importing Databases,” for information concerning file readers, which are normally accessed as DSOs at run time even when the main IRIS Performer libraries have been statically linked. Also, when linking statically, you should not use the -no_unresolved, option since IRIS Performer may reference symbols such as OpenGL extensions which are not installed on your machine. |
Much of the sample code in this guide, many of the sample applications, and most of the database-importing code are written in ANSI C. They should be compiled using the –ansi flag to the C compiler.
Using –cckr instead of –ansi affects IRIS Performer in the following ways:
Because –cckr doesn't support floating point constants denoted with the f suffix, all constants defined with #define are double precision. The promotion of floating point expressions to double precision can decrease performance for some numerically intensive applications.
Because –cckr doesn't allow a macro to have the same name as a routine, the type-casting macros in pf.h are not available. Thus, when you pass a pointer to a derived type such as pfGroup or pfGeode to a routine that takes a generic type such as a pfNode, that argument must be cast to a pfNode explicitly, as shown in the following example:
pfGeode *geode; pfSwitch *switch; pfAddChild((pfGroup *)switch, (pfNode *)geode); |
If you are running version 6.2 or later of IRIX, you can compile and execute OpenGL-based IRIS Performer applications in 64-bit mode.
To do this, you need to have installed the optional 64-bit versions of the IRIS Performer libraries. All that is required then is to use the “-64” switch to the compiler. This selects the compilation mode and causes libraries to be searched for in /usr/lib64 instead of /usr/lib.
The 64-bit version of IRIS Performer is itself created using -mips3, so that you can compile an application using either the MIPS-3 or MIPS-4 instruction set. MIPS-3 executables can run on R4400-based machines such as Onyx and Indigo2 as well as on R8000-based machines such as PowerOnyx and PowerIndigo2. MIPS-4 executables can only be run on R8000-based (and subsequent) machines.
Under IRIX 6.2 and later, if you want to use the extended MIPS-3 or MIPS-4 instruction set in a 32-bit application, install the optional “new 32-bit” (N32) version of IRIS Performer and use the “-n32” option to the compiler. The old 32-bit, new 32-bit and 64-bit versions of IRIS Performer can all be installed at the same time as each is installed in a separate directory, /usr/lib, /usr/lib32 and /usr/lib64, respectively.
![]() | Note: IRIS GL does not exist in “N32” or 64-bit form; you must use OpenGL. |
IRIS Performer provides C++ bindings for all functions as well as C bindings. Most of this guide does not include code examples in C++; however, all sample programs are provided in the IRIS Performer distribution in both C and C++ versions. The structure of a C++ program is largely identical to that of a C program; for examples of IRIS Performer programs using the C++ API, see the /usr/share/Performer/src/pguide/progs and apps directories for examples of equivalent C and C++ programs.
See Chapter 14, “Programming with C++,” for a discussion of the differences between programming using the C and C++ programming interfaces.