This chapter provides some basic guidelines to follow when tuning your application for optimal performance. It also describes how to use debugging tools like pixie, prof, gldebug, and glprof to debug and tune your applications. It concludes with some specific notes on tuning applications on systems with RealityEngine graphics.
This section contains some general performance-tuning principles. Some of these issues are discussed in more detail later in this chapter.
Remember that high performance doesn't come by accident. You must design your programs with speed in mind for optimal results.
Tuning graphical applications, particularly IRIS Performer applications, requires a pipeline-based approach. You can think of the graphics pipeline as comprising three separate stages; the pipeline runs only as fast as the slowest stage, so improving the performance of the pipeline requires improving the slowest stage's performance. The three stages are:
The host (or CPU) stage, in which routines are called and general processing is done by the application. This stage can be thought of as a software pipeline, sometimes called the rendering pipeline, itself comprising up to three sub-stages—the application, cull, and draw stages—as discussed at length throughout this guide.
The transformation stage, in which transformation matrices are applied to objects to position them in space (this includes matrix operations, setting graphics modes, transforming vertices, handling lighting, and clipping polygons).
The fill stage, which includes clearing the screen and then drawing and filling polygons (with operations such as Gouraud shading, z-buffering, and texture mapping).
You can estimate your expected frame rate based on the makeup of the scene to be drawn and graphics speeds for the hardware you're using. Be sure to include fill rates, polygon transform rates, and time for mode changes, matrix operations, and screen clear in your calculations.
Measure both the performance of complex scenes in your database and of individual objects and drawing primitive to verify your understanding of the performance requirements.
Use the IRIS Performer diagnostic statistics to evaluate how long each stage takes and how much it does. See Chapter 12, “Statistics,” for more information. These statistics are referred to frequently in this chapter.
Use system tools to help profile and analyze your application. The IRIS GL glprof utility (described in “Using glprof to Find Performance Bottlenecks”) will profile the rendering of an IRIS GL programs and show what was drawn and help figure out what stage of the graphics hardware pipeline is the significant bottleneck.
Tuning an application is an incremental process. As you improve one stage's performance, bottlenecks in other stages may become more noticeable. Also, don't be discouraged if you apply tuning techniques and find that your frame rate doesn't change—frame rates only change by a field at a time (which is to say in increments of 16.67 milliseconds for a 60 Hz display), while tuning may provide speed increases of finer granularity than that. To see performance improvements that don't actually increase frame rate, look at the times reported by IRIS Performer statistics on the cull and draw processes (see Chapter 12 for more information).
See the graphics library books listed in the “Bibliography” for information about how to get peak performance from your graphics hardware, beyond what IRIS Performer does for you.
IRIS Performer uses many techniques to increase application performance. Knowing about what IRIS Performer is doing and how it affects the various pipeline stages may help you write more efficient code. This section lists some of the things IRIS Performer can do for you.
During drawing, IRIS Performer
Sets up an internal record of what routines and rendering methods are fastest for the current graphics platform. This information can be queried in any process with pfQueryFeature(). You can use this information at run time when setting state properties on your pfGeoStates.
Has machine-dependent fast options for commands that are very performance sensitive. Use the _ON and _FAST mode settings whenever possible to get machine-dependent high-performance defaults. Some examples include:
Sets up default modes for drawing, multiprocessing, statistics, and other areas, that are chosen to provide high scene quality and performance. Some rendering defaults differ from GL defaults: backface elimination is enabled by default (pfCullFace(PFCF_BACK)) and lighting materials use lmcolor() in IRIS GL and glColorMaterial() in OpenGL to minimize the number of materials required in a database (pfMtlColorMode(mtl, side, PFMTL_CMODE_AD)).
Uses a large number of specialized routines for rendering different kinds of objects extremely quickly. There's a specialized drawing routine for each possible pfGeoSet configuration (each choice of primitive type, attribute bindings, and index selection). Each time you change from one pfGeoSet to another, one of these specialized routines is called. However, this happens even if the new pfGeoSet has the same configuration as the old one, so larger pfGeoSets are more efficient than smaller ones—the function-call overhead for drawing many small pfGeoSets can reduce performance. As a rule of thumb, a pfGeoSet should contain at least 4 triangles, and preferably between 8 and 64. If the pfGeoSet is too large, it can reduce the efficiency of other parts of the process pipeline.
Caches state changes, because applying state changes is costly in the draw stage. IRIS Performer accumulates state changes until you call one of the pfApply*() functions, at which point it applies all the changes at once. Note that this differs from the graphics libraries, in which state changes are immediate. If you have several state changes to make in IRIS Performer, set up all the changes before applying them, rather than using the one-change-at-a-time approach (with each change followed by an apply operation) that you might expect if you're used to graphics library programming.
Evaluates state changes lazily—that is, it avoids making any redundant changes. When you apply a state change, IRIS Performer compares the new graphics state to the previous one to see if they're different. If they are, it checks whether the new state sets any modes. If it does, IRIS Performer checks each mode being set to see whether it's different from the previous setting. To take advantage of this feature, share pfGeoStates and inherit states from the global state wherever possible. Set all the settings you can at the global level and let other nodes inherit those settings, rather than setting each node's attributes redundantly. To do this within a database, you can set up pfGeoStates with your desired global state and apply them to the pfChannel or pfScene with pfChanGState() or pfSceneGState(). You can do this through the database loader utilities in libpfdu trivially for a given scene with pfdMakeSharedScene(), or have more control over the process with pfdDefaultGState(), pfdMakeSceneGState(), and pfdOptimizeGStateList().
Provides an optimized immediate mode rendering display list data type, pfDispList, in libpr. The pfDispList type reduces host overhead in the drawing process and requires much less memory than a graphics library display list. libpf uses pfDispLists to pass results from the cull process to the draw process when the PFMP_CULL_DL_DRAW mode is turned on as part of the multiprocessing model. For more information about display lists, see “Display Lists” in Chapter 10; for more information about multiprocessing, see “Successful Multiprocessing With IRIS Performer” in Chapter 7.
To help optimize culling and intersection, IRIS Performer
Provides pfFlatten() to resolve instancing (via cloning) and static matrix transformations (by pre-transforming the cloned geometry). It can be especially important to flatten small pfGeoSets; otherwise matrix transformations must be applied to each small pfGeoSet at significant computational expense. Note that flattening resolves only static coordinate systems, not dynamic ones, but that where desired, pfDCS nodes can be converted to pfSCS nodes automatically using the IRIS Performer utility function pfdFreezeTransforms(), which allows for subsequent flattening. Using pfFlatten(), of course, increases performance at the cost of greater memory use. Further, the function pfdCleanTree() can be used to remove needless nodes: identity matrix pfSCS nodes, single child pfGroup nodes, and the like.
Uses bounding spheres around nodes for fast culling—the intersection test for spheres is much faster than that for bounding boxes. If a node's bounding sphere doesn't intersect the viewing frustum, there's no need to descend further into that node. There are bounding boxes around pfGeoSets; the intersection test is more expensive but provides greater accuracy at that level.
Provides the pfPartition node type to partition geometry for fast intersection testing. Use a pfPartition node as the parent for anything that needs intersection testing.
Provides level-of-detail (LOD) capabilities in order to draw simpler (and thus cheaper) versions of objects when they're too far away for the user to discern small details.
Allows intersection performance enhancement via precomputation of polygon plane equations within pfGeoSets. This pre-computation is in the form of a traversal that is nearly always appropriate—only in cases of non-intersectable or dynamically changing geometry might these cached plane equations be disadvantageous. This optimization is performed by pfuCollideSetup() using the PFTRAV_IS_CACHE bit mask value.
Sorts pfGeoSets by graphics state in the cull process, in order to minimize state changes and flatten matrix transformations, when libpf creates display lists to send to the draw process (as occurs in the PFMP_CULL_DL_DRAW multiprocessing mode). This procedure takes extra time in the cull stage, but can greatly improve performance when rendering a complex scene that uses many pfGeoStates. The sorting is enabled by default; it can be turned off and on by calling the function pfChanTravMode(chan, PFTRAV_CULL, mode) and including or excluding the PFCULL_SORT token. See “pfChannel Traversal Modes” in Chapter 6 and “Sorting the Scene” in Chapter 6 for more information on sorting.
During the application stage, IRIS Performer
Divides the application process into two parts: the latency-critical section (which includes everything between pfSync() and pfFrame()), where last-minute latency-critical operations are performed before the cull of the current frame can start; and the noncritical portion, after pfFrame() and before pfSync(). The critical section is displayed in the channel statistics graph drawn with pfDrawChanStats()
Provides an efficient mechanism to automatically propagate database changes down the process pipeline, and provides pfPassChanData() for passing custom application data down the process pipeline.
Minimizes overhead copying database changes to the cull process by accumulating unique changes and updating the cull once inside pfFrame(). This updated period is displayed in the MP statistics of the channel statistics graph drawn with pfDrawChanStats().
Provides a mechanism for performing database intersections in a forked process: pass the PFMP_FORK_ISECT flag to pfMultiprocess() and declare an intersection callback with pfIsectFunc().
Provides a mechanism for performing database loading and editing operations in a forked process, such as the DBASE process: pass the PFMP_FORK_DBASE flag to pfMultiprocess() and declare an intersection callback with pfDBaseFunc().
While IRIS Performer provides inherent performance optimization, there are specific techniques you can use to increase performance even more. This section contains some guidelines and advice pertaining to database organization, code structure and style, managing system resources, and rules for using IRIS Performer.
Tuning the graphics pipeline requires identifying and removing bottlenecks in the pipeline. You can remove a bottleneck either by minimizing the amount of work being done in the stage that has the bottleneck or, in some cases, by reorganizing your rendering to more effectively distribute the workload over the pipeline stages. This section contains specific tips for finding and removing bottlenecks in each stage of the graphics pipeline. For more information on this topic, refer to the graphics library documentation (see “The IRIS GL and OpenGL Graphics Libraries” for information on ordering these books).
Here are some ways to minimize the time spent in the host stage of the graphics pipeline:
Function calls, loops, and other programming constructs require a certain amount of overhead. To make such constructs cost-effective, make sure they do as much work as possible with each invocation. For instance, drawing a pfGeoSet of triangle strips involves a nested loop, iterating on strips within the set and triangles within each strip; it therefore makes sense to have several triangles in each strip and several strips in each set. If you put only two triangles in a pfGeoSet, you'll spend all that loop overhead on drawing those two triangles, when you could be drawing many more with little extra cost. The channel statistics can display (as part of the graphics statistics) a histogram showing the percentage of your database that is drawn in triangle strips of certain lengths.
Only bind vertex attributes that are actually in use. For example, if you bind per-vertex colors on a set of flat-shaded quads, the software will waste work by sending those colors to the graphics pipeline, which will ignore them. Similarly, it's pointless to bind normals on an unlit pfGeoSet.
Nonindexed drawing has less host overhead than indexed drawing because indexed drawing requires an extra memory reference to get the vertex data to the graphics pipeline. This is most significant for primitives that are easily host-limited, such as independent polygons or very short triangle strips. However, indexed drawing can be very helpful in reducing the memory requirements of a very large database.
Enable state sorting for pfChannels (this is the default). By sorting, the CPU does not need to examine as many pfGeoStates. The graphics channel statistics can be used to report the pfGeoSet-to-pfGeoState drawing ratio.
A transform bottleneck can arise from expensive vertex operations, from a scene that's typically drawn with many very tiny polygons, from a scene modeled with inefficient primitive types, or from excessive mode or transform operations. Here are some tips on reducing such bottlenecks:
Connected primitives will have better vertex rates than independent primitives, and quadrilaterals are typically much more efficient in vertex operations than independent triangles are.
The expensive vertex operations are generally lighting calculations. The fastest lighting configuration is always one infinite light. Multiple lights, local viewing models, and local lights have (in that order) dramatically increasing cost. Two-sided lighting also incurs some additional transform cost. On some graphics platforms, texturing and fog can add further significant cost to per-vertex transformation. The channel graphics statistics will tell you what kinds of lights and light models are being used in the scene.
Matrix transforms are also fairly expensive, and definitely more costly than one or two explicit scale, translate, or rotate operations. When possible, flatten matrix operations into the database with pfFlatten().
The most frequent causes of mode changes are shademodel() (or glShadeModel()), textures, and object materials. The speed of these changes depends on the graphics hardware; however, material changes do tend to be expensive. Sharing materials among different objects can be increased with the use of pfMtlColorMode() that is PFMTL_CMODE_AD by default. However, on some older graphics platforms (such as the Elan, Extreme, and VGX), the use of pfMtlColorMode() (which actually calls the IRIS GL function lmcolor() or the OpenGL function glColorMaterial()) has some associated per-vertex cost and should be used with some caution.
If you cull stage is not a bottleneck, make sure your pfChannels sort the scene by graphics state. Even if you are running in single process mode, the extra time taken to sort the database is often more than offset by the savings in rendering time. See “Sorting the Scene” in Chapter 6 for more details on how to configure sorting.
Here are some methods of dealing with fill-stage bottlenecks:
One technique to hide the cost of expensive fill operations is to fill the pipeline from the back forward so that no part is sitting idle waiting for an upstream stage to finish. The last stage of the pipeline is the fill stage, so by drawing backgrounds or clearing the screen via pfClearChan() first, before pfDraw(), you can keep the fill stage busy. In addition, if you have a couple of large objects that reliably occlude much of the scene, drawing them very early on can both fill up the back-end stage and also reduce future fill work, because the occluded parts of the scene will fail a z-buffer test and will not have to write z values to the z-buffer or go on to more complex fill operations.
Use the pfStats fill statistics (available for display through the channel statistics) to visualize your depth complexity and get a count of how many pixels are being computed each frame.
Be aware of the cost of any fill operations you use and their relative cost on the relevant graphics hardware configuration. Quick experiments you can do to test for a fill limitation include:
If either of these tests causes a significant reduction in the time to draw the scene, then a significant fill bottleneck probably exists.
The cost of specific fill operations can vary greatly depending on the graphics hardware configuration. As a rule of thumb, flat shading is much faster than Gouraud shading because it reduces both fill work and the host overhead of specifying per-vertex colors. Z-buffering is typically next in cost, and then stencil and blending. On a RealityEngine, the use of multisampling can add to the cost of some of these operations, specifically z-buffering and stenciling. See “Multisampling” for more information. Texturing is considered free on RealityEngine and Impact systems but is relatively expensive on a VGX and is actually done in the host stage on lower-end graphics platforms, such as Extreme and XZ. Some of the low-end graphics platforms also implement z-buffering on the host.
You may not be able to achieve benchmark-level performance in all cases for all features. For instance, if you frequently change modes and you use short triangle strips, you get much less than the peak triangle mesh performance quoted for the machine. Fill rates are sensitive to both modes, mode changes, and polygon sizes. As a general rule of thumb, assume that fill rates average around 70% of peak on general scenes to account for polygon size and position as well as for pipeline efficiency.
These simple tips will help you optimize your IRIS Performer process pipeline:
Use pfMultiprocess() to set the appropriate process model for the current machine.
You usually shouldn't specify more processes with pfMultiprocess() than there are CPUs on the system. The default multiprocess mode (PFMP_DEFAULT) attempts an optimal configuration for the number of unrestricted CPUs. However, if there are fewer processors than significant tasks (consider APP, CULL, DRAW, ISECT, DBASE) you will want experiment with the different two-process models to find the one that will yield the best overall frame rate. Use of pfDrawChanStats(), described in Chapter 12, “Statistics,” will greatly help with this task.
Put only latency-critical tasks between the pfSync() and pfFrame() calls. For example, put latency-critical updates, like changes to the viewpoint, after pfSync() but before pfFrame(). Put time-consuming tasks, such as intersection tests and system dynamics, after pfFrame().
You will also want to refer to the IRIX REACT™ documentation for setting up a real-time system.
For maximum performance, use the IRIS Performer utilities in libpfutil for setting non-degrading priorities and isolating CPUs (pfuPrioritizeProcs(), pfuLockDownProc(), pfuLockDownApp(), pfuLockDownCull(), pfuLockDownDraw()). These facilities require that the application runs with root permissions. The source code for these utilities is in /usr/share/Performer/src/lib/libpfutil/lockcpu.c. For an example of there use, see the sample source code in /usr/share/Performer/src/pguide/libpf/C/bench.c. For more information about priority scheduling and real-time programming, see the chapter of the IRIX System Programming Guide entitled “Using Real-Time Programming Features.” and the IRX REACT™ technical report.
Make sure you aren't generating any floating-point exceptions. Floating-point exceptions can cause an individual computation to incur tens of times its normal cost. Furthermore, a single floating point exception can lead to many further exceptions in computations that use the exceptional result and can even propagate performance degradation down the process pipeline. IRIS Performer will detect and tell you about the existence of floating point exceptions if your pfNotifyLevel() is set to PFNFY_INFO or PFNFY_DEBUG. You can then run your application in dbx and your application will stop when it encounters an exception, enabling you to trace the cause.
Minimize the amount of channel data allocated by pfAllocChanData() and call pfPassChanData() only when necessary to reduce the overhead of copying the data. Copying pointers instead of data is often sufficient.
Here are a couple of suggestions for tuning the cull process:
The default channel culling mode enables all types of culling. If your cull process is your most expensive task, you may want to consider doing less culling operations. When doing database culling, always use view-frustum culling (PFCULL_VIEW), and usually use graphics library mode database sorting (PFCULL_SORT) and pfGeoSet culling (PFCULL_GSET) as well:
pfChanTravMode(chan, root,
PFCULL_VIEW | PFCULL_GSET | PFCULL_SORT);
|
A cull-limited application might realize a quick gain from turning off pfGeoSet culling. If you think your database has few textures and materials, you might turn off sorting. However, if possible it would be better to try improving cull performance by improving database construction. “Efficient Intersection and Traversals” discusses optimizing cull traversals in more detail.
Look at the channel culling statistics for:
A large amount of the database being traversed by the culling process and being trivially rejected as not being in the viewing frustum. This can be improved with better spatial organization of the database.
A large number of database nodes being non-trivially culled. This can be improved with better spatial organization and breakup of large pfGeodes and pfGeoSets.
A surprising number of LODs in their fade state (the fade computations can be expensive, particularly if channel stress management has been enabled).
Balance the database hierarchy with the scene complexity: the depth of the hierarchy, the number of pfGeoStates, and the depth of culling. See “Balancing Cull and Draw Processing with Database Hierarchy” for details.
pfNodes that have significant evaluation in the cull stage include pfBillboards, pfLightPoints, pfLightSources, and pfLODs.
Here are some suggestions specific to the draw process:
Minimize host work done in the draw process before the call to pfDraw(). Time spent before the call to pfDraw() is time that the graphics pipeline is idle. Any graphics library (or X) input processing or mode changes should be done after pfDraw() to take effect the following frame.
Use only one pfPipe per hardware graphics pipeline and preferably one pfPipeWindow per pfPipe. Use multiple channels within that pfPipeWindow to manage multiple views or scenes. It's fairly expensive to simultaneously render to multiple graphics windows on a single hardware graphics pipeline and is not advisable for a real-time application.
Pre-define and pre-load all of the textures in the database into hardware texture memory by using a pfApplyTex() function on each pfTexture. You can do this texture-applying in the pfConfigStage() draw callback or (for multipipe applications to allow parallelism) the pfConfigPWin() callback. This approach avoids the huge performance impact that results when textures are encountered for the first time while drawing the database and must then be downloaded to texture memory. Utilities are provided in libpfutil to apply textures appropriately; see the pfuDownloadTexList() routine in the distributed source code file /usr/share/Performer/src/lib/libpfutil/tex.c. The perfly application demonstrates this; see the perfly source file generic.c in either the C-language (/usr/share/Performer/src/sample/C/common) or C++ language (/usr/share/Performer/src/sample/C++/common) versions of perfly.
Minimize the use of pfSCSs and pfDCSs and nodes with draw callbacks in the database since aggressive state sorting is kept local to subtrees under these nodes.
Don't do any graphics library input handling in the draw process. Instead, use X input handling in an asynchronous process. IRIS Performer provides utilities for asynchronous input handling in libpfutil with source code provided in /usr/share/Performer/src/lib/libpfutil/input.c. For a demonstration of asynchronous X input handling, see provided sample applications, such as perfly, and also the distributed sample programs /usr/share/Performer/src/pguide/libpf/C/motif.c and /usr/share/Performer/src/pguide/libpfui/C/motifxformer.c.
Here are some tips on optimizing intersections and traversals:
Use pfPartition nodes on pieces of the database that will be handed to intersection traversal. These nodes impose spatial partitioning on the subgraph beneath them, which can dramatically improve the performance of intersection traversals.
![]() | Note: Subgraphs under pfDCS, pfLOD, pfSwitch, and pfSequence nodes are not partitioned so intersection traversals of these subgraphs will not be affected. |
Use intersection caching. For static objects, enable intersection caching at initialization—first call pfNodeTravMask(), specifying intersection traversal (PFTRAV_ISECT), and then include PFTRAV_IS_CACHE in the mode for intersections. You can turn this mode on and off for dynamic objects as appropriate.
Use intersection masks on nodes to eliminate large sections of the database when doing intersection tests. Note that intersections are sproc()-safe in the current version of IRIS Performer; you can check intersections in more than one process.
Bundle segments for intersections with bounding cylinders. You can pass as many as 32 segments to each intersection request. If the request contains more than a few segments and if the segments are close together, the traversal will run faster if you place a bounding cylinder around the segments using pfCylAroundSegs() and pass that bounding cylinder to pfNodeIsectSegs(). The intersection traversal will use the cylinder rather than each segment when testing the segments against the bounding volumes of nodes and pfGeoSets.
Optimizing your databases can provide large performance increases.
The following tips will help you achieve peak performance when using libpr:
Minimize the number of pfGeoStates by sharing as much as possible.
Initialize each mode in the global state to match the majority of the database, in order to set as little local state for individual pfGeoStates as possible.
Use triangle strips wherever possible; they produce the largest number of polygons from a given number of vertices, so they use the least memory and are drawn the fastest of the primitive types.
Use the simplest possible attribute bindings and use flat-shaded primitives wherever possible. If you're not going to need an object's attributes, don't bind them—anything you bind will have to be sent to the pipeline with the object.
Flat-shaded primitives and simple attribute bindings reduce the transformation and lighting requirements for the polygon. Note that the flat-shaded triangle-strip primitive renders faster than a regular triangle strip, but you have to change the index by two to get the colors right (that is, you need to ignore the first two vertices when coloring) See “Attribute Bindings” in Chapter 10 for more information.
Use nonindexed drawing wherever possible, especially for independent polygon primitives and short triangle strips.
When building the database, avoid fragmentation in memory of data to be rendered. Minimize the number of separate data and index arrays. Keep the data and index arrays for pfGeoSets contiguous and try to keep separate pfGeoSets contiguous to avoid small, fragmented pfMalloc() memory allocations.
The ideal size of a pfGeoSet (and of each triangle strip within the pfGeoSet) depends a great deal on the specific CPU and system architecture involved; you may have to do benchmarks to find out what's best for your machine. For a general rule of thumb, use at least 4 triangles per strip on any machine, and 8 on most. Use 5 to 10 strips in each pfGeoSet, or a total of 24 to 100 triangles per pfGeoSet.
When you're using libpf, the following tips can improve the performance of database tasks:
Use pfFlatten(), especially when a pfScene contains many small instanced objects and billboards. Use pfdCleanTree() and (if application considerations permit) pfdFreezeTransforms() to minimize the cull traversal processing time and maximize state sorting scope.
Initialize each mode in the scene pfGeoState to match the majority of the database, in order to set as little local state for individual pfGeoStates as possible. The utility function pfdMakeSharedScene() provides an easy to use mechanism for this task.
Minimize the number of very small pfGeoSets (that is, those containing four or fewer total triangles). Each tiny pfGeoSet means another bounding box to test against if you're culling down to the pfGeoSet level (that is, when PFCULL_GSET is set with pfChanTravMode()) as well as another item to sort during culling. (If your pfGeoSets are large, on the other hand, you should definitely cull down to the pfGeoSet level.)
Be sparing in the use of pfLayers. Layers imply that pixels are being filled with geometry that is not visible. If fill performance is a concern, this should be minimized in the modeling process by cutting layers into their bases when possible. However, this will produce more polygons which require more transform and host processing so it should only be done if it will not greatly increase database size.
Make the hierarchy of the database spatially coherent so that culling will be more accurate and geometry that is outside the viewing frustum will not be drawn. (See Figure 6-3 for an example of a spatially organized database.)
Construct your database to minimize the draw-process time spent traversing and rendering the culled part of the database without the cull-process time becoming the limiting performance factor. This process involves making tradeoffs as a simpler cull means a less efficient draw stage. This section describes these tradeoffs and some good rules to follow to give you a good start.
If the cull and draw processes are performed in parallel, the goal is to minimize the larger of the culling and drawing times. In this case, an application can spend approximately the same amount of time on each task. However, if both culling and drawing are performed in the same process, the goal is to optimize the sum of these two times, and both processes must be streamlined to minimize the total frame time. Important parameters in this optimization include the number of pfGeoSets, the average branching factor of the database hierarchy, and the enabled channel culling traversal modes. The pfDrawChanStats() function (see Chapter 12, “Statistics”) can easily provide diagnostic information to aid in this tuning.
The average number of immediate children per node can directly affect the culling process. If most nodes have a high number of children, the bounding spheres are more likely to intersect the viewing frustum and all those nodes will have to be tested for visibility. At the other extreme, a very low number of children per node will mean that each bounding sphere test can only eliminate a small part of the database and so many nodes may still have to be traversed. A good place to start is with a quad-tree type organization where each node has about four children and the bounding geometry of sibling nodes are adjacent but have minimal intersection. In the ideal case, projected to a two-dimensional plane on the ground, the spatial extent of each node and its parents would form a hierarchy of boxes.
The transition from pfGeodes to pfGeoSets is an important point in the database structure. If there are many very small pfGeoSets within a single pfGeode, not culling down to pfGeoSets can actually improve overall frame time because the cost of drawing the tiny pfGeoSets may be small relative to the time spent culling them. Adding more pfGeodes to break up the pfGeoSets can help by providing a slightly more accurate cull at less cost than considering each pfGeoSet individually. In addition, pfGeodes are culled by their bounding spheres, which is faster than the culling of pfGeoSets which are culled by their bounding boxes.
The size (both spatial extend and number of triangles) can also directly impact culling and drawing performance. If pfGeoSets are relatively large, there will be fewer to cull so pfGeoSet culling can probably be afforded. pfGeoSets with more triangles will draw faster. However, pfGeoSets with larger spatial extent are more likely to have geometry that is being drawn that is outside of the viewing frustum which wastes time in the graphics stage. Breaking up some of the large pfGeoSets can improve graphics performance by allowing a more accurate cull.
With some added cost to the culling task, the use of Level-of-Detail nodes (pfLODs) can make a tremendous difference in graphics performance and image quality. LODs allow objects to be automatically drawn with a simplified version when they are in a state that yields little contribution to the scene (such as being far from the eyepoint). This allows you to have many more objects in your scene than if you always were drawing all objects at full complexity. However, you do not want the cull to be testing all LODs of an object every frame when only one will be used. Again, proper use of hierarchy can help. pfLODs (non-fading) can be inserted into the hierarchy with actual object pfLODs grouped beneath them. If the parent LOD is out of range for the current viewpoint, the child LODs will never be tested. The pfLODs of each object can be placed together under a pfGroup so that no LOD tests for the object will be done if the object is outside of the viewing frustum.
Calling pfFlatten(), pfdFreezeTransforms(), or pfdCleanTree() to remove extraneous nodes can often help culling performance. Use pfFlatten() to de-instance and apply pfSCS node transformations to leaf geometry—resulting in less work during the cull traversal. This will allow both better database sorting for the draw, and also better caching of matrix and bounding information which can speed up culling. When these scene graph modifications are not acceptable, you may reduce cull time by turning off culling of pfGeoSets but this will directly impact rendering performance by forcing the rendering of geometry that is outside the viewing frustum.
The code fragment in Example 13-1, taken from the sample program /usr/share/Performer/src/pguide/libpf/C/bench.c, makes an IRIS GL object and then temporarily draws that instead of calling pfDraw().
Example 13-1. Drawing an Object Without Calling pfDraw()
if (SharedFlags->glObject == MAKE_GL_OBJECT)
{
static have_obj = 0;
fprintf(stderr, "Making object\n");
if (have_obj)
delobj(1);
makeobj(1); /* OpenGL: glNewList() */
pfDraw();
closeobj(); /* OpenGL: glEndList() */
SharedFlags->glObject = DRAW_GL_OBJECT;
have_obj = 1;
}
else if (SharedFlags->glObject == DRAW_GL_OBJECT)
callobj(1); /* OpenGL: glCallList() */
else if (SharedFlags->glObject == PERF_DRAW)
pfDraw();
|
On machines with fast texture mapping, texture should be used to replace complex geometry. Complex objects, such as trees, signs, and building fronts, can be effectively and efficiently represented by textured images on single polygons in combination with applying pfAlphaFunc() to remove parts of the polygon that don't appear in the image. Using texture to reduce polygonal complexity can often give both an improved picture and improved performance. This is because
The image texture provides scene complexity, and the texture hardware handles scaling of the image with MIP-map interpolation functions for minification (and, on RealityEngine systems, Sharpen and DetailTexture functions for magnification).
Using just a few textured polygons rather than a complex model containing many individual polygons reduces system load.
In order to represent a tree or other 3D object as a single textured polygon, IRIS Performer can rotate polygons to always face the eyepoint. An object of this type is known as a billboard and is represented by a pfBillboard node. As the viewer moves around the object, the textured polygon rotates so that the object appears three-dimensional. For more information on billboards, see “pfBillboard Nodes” in Chapter 5.
To determine if the current graphics platform has fast texture mapping, look for a PFQFTR_FAST return value from:
pfFeature(PFQFTR_TEXTURE, &ret);
|
pfAlphaFunc() with a function PFAF_GEQUAL and a reference value greater than zero can be used whenever transparency is used to remove pixels of low contribution and avoid their expensive processing phase.
For maximum performance, routines that make extensive use of the IRIS Performer linear algebra routines should use the macros defined in prmath.h to allow the compiler to in-line these operations.
Use single- rather than double-precision arithmetic where possible and avoid the use of short-integer data in arithmetic expressions. Append an `f' to all floating point constants used in arithmetic expressions.
| BAD | In this example, values in both of the expressions involving the floating point variable x are promoted to double precision when evaluated:
| ||
| GOOD | In this example, both of the expressions involving the floating point variable x remain in single-precision mode, because there is an `f' appended to the floating point constants:
|
Performance measurement tools can help you track the progress of your application by gathering statistics on certain operations. IRIS Performer provides run-time profiling of the time spent in parts of the graphics pipeline for a given frame. The pfDrawChanStats() function displays application, cull, and draw time in the form of a graph drawn in a channel; see Chapter 12, “Statistics,” for more information on that and related functions. There are advanced debugging and tuning tools available from Silicon Graphics that can be of great assistance. WorkShop product in the CASEVision™ tools provides a complete development environment for debugging and tuning of host tasks. The Performance Co-Pilot™ helps you to tune host code in a real-time environment. There is also the WindView™ product from WindRiver that works with IRIX REACT to do full system profiling in a real-time environment. However, progress can be made with the basic tools that are in the IRIX development environment: prof, pixie, and glprof. The IRIS GL debugging utility, gldebug, can also be used to aid in performance tuning, as can the OpenGL equivalent, ogldebug. This section briefly discusses getting started with these tools.
You can use the IRIX performance analysis utilities pixie and prof to tune the application process. Use pixie for basic-block counting and use prof for program counter (PC) sampling. PC sampling gives run-time estimation of where significant amounts of time are spent, whereas basic-block counting will report the number of times a given instruction is executed.
To isolate statistics for the application process, even in single-process models, run the application through pixie or prof in APP_CULL_DRAW mode to separate out the process of interest. Both pixie and prof can generate statistics for an individual process.
When using IRIS Performer DSO libraries with prof you may want to provide the -dso option to prof with the full pathname of the library of interest to have IRIS Performer routines included in the analysis. When using pixie you will need to have the.pixie versions of the DSO libraries in your LD_LIBRARY_PATH. Additionally, you will need a.pixie version of the loader DSO for your database in your LD_LIBRARY_PATH. You may have to pixie the loader DSO separately since pixie will not find it automatically if your executable was not linked with it. When using prof to do PC sampling, link with unshared libraries exclusively and use the –p option to ld. Then set the environment variable PROFDIR to indicate the directory in which to put profiling data files.
When profiling, run the program for a while so that the initialization will not be significant in the profiling output. When running a program for profiling, run a set number of frames and then use the automatic exit described below.
You can use the graphics utilities gldebug (IRIS GL) and ogldebug (OpenGL) to both debug and tune IRIS Performer applications. The application must be run in single-process mode in order to use gldebug but ogldebug can handling multiprocessed programs.
Show which graphics calls are being issued
Look for frequent mode changes, or unnecessary mode settings that can be caused if your initialization of the global state doesn't match the majority of the database
Look for unnecessary vertex bindings such as unneeded per-vertex colors, or normals for a flat-shaded object
Follow these steps to examine one frame of the application in a gldebug session:
Start up profiler of choice:
IRIS% gldebug -i ignore -s -F your_prog_name prog_options |
OPEN% ogldebug your_prog_name prog_options
Turn off output and breakpoints from the control panel.
Set a breakpoint at swapbuffers() or glXSwapBuffers().
Click the “Continue” button and go to the frame of interest.
Turn on breakpoints.
Execution stops at swapbuffers() (or glXSwapBuffers()).
Turn on all trace output.
Click the “Continue” button.
Execution stops at the next swapbuffers(), outputting one full scene to GLdebug.history (or progname.pid.trace for ogldebug).
Quit and examine the output.
![]() | Note: Since IRIS Performer avoids unnecessary mode settings, recording one frame shows modes that are set during that frame, but it doesn't reflect modes that were set previously. It's therefore best to have a program that can come up in the desired location and with the desired modes, then grab the first two frames: one for initialization, and one for continued drawing. |
You can use the IRIS GL graphics-profiling utility glprof to estimate where there are graphics bottlenecks so that you can tune the database. You don't have to relink the program or run it in single-process mode to use glprof.
Use glprof to:
See if certain views or individual objects are inherently fill- or transform-limited
If a scene or object is fill-limited, there can be more complex geometry in the LOD(s).
If a scene or object is transform-limited, you need to simplify or add LOD(s), especially for small objects.
See significant mode changes
Get additional scene graphics state and fill statistics such as the number of pixels fixed with polygons of different sizes and different modes
Note that since only glprof simulates the graphics pipeline, it may not always be entirely accurate in predicting performance.
You can use predraw callbacks on nodes to output glprof_object tags that will appear in a glprof trace. This method has the disadvantage of turning off sorting, which may increase the number of mode changes and also the balance of bottlenecks in the scene because it may change the drawing order. However, it has the advantage of giving per-object drawing statistics, and it indicates whether a specific object is fill- or transform-limited.
Example 13-2 shows sample code for a traversal that installs and removes the glprof object tags taken from trav.c. The example demonstrates general-user traversal code, then uses this traversal to install and remove glprof callbacks. This code is simplified from that found in /usr/src/Performer/src/lib/libpfutil/trav.c.
Example 13-2. General Traversal
void
InitMyTraverser(MyTraverser *trav)
{
trav->preFunc = NULL;
trav->postFunc = NULL;
trav->mstack = NULL;
trav->data = NULL;
trav->node = NULL;
trav->depth = 0;
}
/* handle return value for pruning or terminating */
#define PFU_DO_RET(_ret) \
switch (_ret) \
{ \
case PFTRAV_PRUNE: \
trav->node = prevNode; \
if (needPop) \
pfPopMStack(trav->mstack); \
return PFTRAV_CONT; \
case PFTRAV_TERM: \
trav->node = prevNode; \
if (needPop) \
pfPopMStack(trav->mstack); \
return PFTRAV_TERM; \
}
int
MyTraverse(pfNode *node, MyTraverser *trav)
{
int i;
int numChild = 1;
int ret = PFTRAV_CONT, needPop = 0;
pfNode *prevNode = trav->node;
if (node == NULL)
{
pfNotify(PFNFY_WARN, PFNFY_USAGE,
“MyTraverse() Null node”);
return PFTRAV_CONT;
}
/*
* for SCS and DCS push the transform on the stack
*/
if (pfIsOfType(node, pfGetSCSClassType()) &&
trav->mstack)
{
pfMatrix mat;
pfGetSCSMat((pfSCS *)trav->node, mat);
pfPushMStack(trav->mstack);
pfPreMultMStack(trav->mstack, mat);
needPop = 1;
}
/* call pre-traversal callback */
trav->node = (pfNode *)node;
if (trav->preFunc)
ret = (*trav->preFunc)(trav);
PFU_DO_RET(ret);
/* after preFunc, in case topology changed */
if (pfIsOfType(node, pfGetGroupClassType()))
{
numChild = pfGetNumChildren(node);
if (pfIsOfType(node, pfGetGroupClassType()))
for (i = 0 ; i < numChild ; i++)
{
trav->depth++;
ret = MyTraverse((pfNode*)pfGetChild(group, i), trav);
trav->depth--;
PFU_DO_RET(ret);
}
}
PFU_DO_RET(ret);
/* call post traversal callback */
trav->node = node;
if (trav->postFunc)
ret = (*trav->postFunc)(trav);
PFU_DO_RET(ret);
if (needPop)
pfPopMStack(trav->mstack);
return PFTRAV_CONT;
}
/***********************************************************
* Traversals and callbacks for installing and removing
* pre-draw callbacks (pfNodeTravFuncs) for generating GL * Prof tags during drawing
* WARNING: removes any existing pre or post-draw callbacks
***********************************************************/
/*
* glprof pre-draw traversal callback
* issues glprof_object calls
*/
static int
cbGLProf(pfTraverser *trav, void *data)
{
const pfNode *node = pfGetTravNode(trav);
const char *nn;
static char name[80];
if (node != NULL &&
(nn = pfGetNodeName(node))
{
/* if it exists, use the node name for the tag */
strncpy(name, nn, 79);
}
else
{
/* otherwise, use the node type string for the tag */
strncpy(name, pfGetTypeName((pfObject *)node), 79);
}
glprof_object(name);
return 0;
}
/*
* callback for placing glprof callbacks as
* pre-draw callbacks on nodes
*/
static int
cbPutGLProfTag(MyTraverser *trav)
{
pfNode *node = trav->node;
if (node != NULL &&
((pfIsOfType(node, pfGetGeodeClassType()))
pfNodeTravFuncs(node, PFTRAV_DRAW, cbGLProf, NULL);
return PFTRAV_CONT;
}
/*
* callback to remove the pre-draw glprof tag callbacks
*/
static int
cbRmGLProfTag(MyTraverser *trav)
{
pfNode *node = trav->node;
if (node != NULL &&
(pfIsOfType(node, pfGetGeodeClassType()) ||
pfIsOfType(node, pfGetGroupClassType()))
pfNodeTravFuncs(node, PFTRAV_DRAW, NULL, NULL);
return PFTRAV_CONT;
}
/* glprof object tag traversal */
void
DoGLProfTraversal(pfNode *node, int mode)
{
MyTraverser trav;
InitMyTraverser(&trav);
pfNotify(PFNFY_INFO, PFNFY_PRINT,
“doing travGLProf: mode = %d”, mode);
if (mode) /* place glprof tag callbacks */
trav.preFunc = cbPutGLProfTag;
else /* remove the callbacks */
trav.preFunc = cbRmGLProfTag;
MyTraverse(node, &trav);
}
|
Use this traverser in a program with a code fragment like that in Example 13-3.
Example 13-3. Using the Traverser
{
/* mode = 1 - install glprof tag node callbacks
* mode = 0 - remove glprof tag node callbacks
*/
DoGLProfTraversal((pfNode *)scene, DBGT_GLPROF, mode);
}
|
This section lists some general guidelines to keep in mind when debugging IRIS Performer applications.
Because malloc() doesn't allocate memory until that memory is used, core dumps may occur when arenas don't find enough disk space for paging. The IRIX kernel can be configured to actually allocate space upon calling malloc(), but this change is pervasive and has performance ramifications for fork() and exec(). Reconfiguring the kernel is not recommended, so be aware that unexplained core dumps can result from inadequate disk space.
Be sure to initialize pointers to shared memory and all other nonshared global values before IRIS Performer creates the additional processes in the call to pfConfig(). Values put in global variables initialized after pfConfig() will only be visible to the process that set them.
For detailed information about other aspects of shared memory, see “Memory Allocation” in Chapter 10.
When debugging an application that uses a multiprocess model, first use a single-process model to verify the basic paths of execution in the program. You don't have to restructure your code; simply select single-process operation by calling pfMultiprocess(PFMP_APPCULLDRAW) to force all tasks to initiate execution sequentially on a frame-by-frame basis.
If an application fails to run in multiprocess mode but runs smoothly in single-process mode, you may have a problem with the initialization and use of data that's shared among processes.
If you need to debug one of multiple processes, use
IRIS% dbx -p progname |
while the process is running. This will show the related processes and allow you to choose a process to trace. The application process will always be the process with the lowest process id. In order after that will be the (default) clock process, then the cull process, and then the draw.
Once the program works, experiment with the different multiprocess models to achieve the best overall frame rate for a given machine. Don't specify more processes than CPUs. Use pfDrawChanStats() to compare the frame timings of the different stages and frame times for the different process models.
Arrange error handling for floating-point operations. To see floating-point errors, turn debug messages on and enable floating-point traps. Set pfNotifyLevel(PFNFY_DEBUG).
The goal is to have no NaN (Not a Number), INF (infinite value), or floating-point exceptions resulting from numerical computations.
This section contains some specific notes on performance tuning with RealityEngine graphics.
Multisampling provides full-scene antialiasing with performance sufficient for a real-time visual simulation application. However, it isn't free and it adds to the cost of some other fill operations. With RealityEngine graphics, most other modes are free until you add multisampling— multisampling requires some fill operations to be performed on every subpixel. This is most noticeable with z-buffering and stenciling operations, but also applies to blendfunction() and glBlendFunc(). Texturing is an example of a fill operation that can be free on a RealityEngine and isn't affected by the use of multisampling.
The multisampling hardware reduces the cost of subpixel operations by optimizing for pixels that are fully opaque. Pixels that have several primitives contributing to their result are thus more expensive to evaluate and are called complex pixels. Scenes usually end up having a very low ratio of complex pixels.
Multisampling offers an additional performance optimization that helps balance its cost: a virtually free screen clear. Technically, it doesn't really clear the screen but rather allow you to set the z values in the framebuffer to be undefined. Therefore, use of this clear requires that every pixel on the screen be rendered every frame. This clear is invoked with a pfEarthSky using the PFES_TAG option to pfESkyMode(). Refer to the pfEarthSky(3pf) reference page for more detailed information.
There are two ways of achieving transparency on a RealityEngine: blending, and screen-door transparency with multisampling.
Blended transparency, using the IRIS GL blendfunction() routine (or the OpenGL glBlendFunc() equivalent), can be used with or without multisampling. Blending doesn't increase the number of complex pixels, but is expensive for complex pixels.
To reduce the number of pixels with very low alpha, one can use a pfAlphaFunc() that ignores pixels of low alpha, such as alpha less than 3 or 4. This will slightly improve fill performance and probably not have a noticeable effect on scene quality. Many scenes can use values as high as 60 or 70 without suffering degradation in image quality. In fact, for a scene with very little actual transparency, this can reduce the fuzzy edges on textures that simulate geometry (such as trees and fences) that arise from MIP-mapping.
Screen-door transparency gives order-independent transparent effects and is used for achieving the fade-LOD effect. It's a common misperception that screen-door transparency on RealityEngine gives you n levels of transparency for n multisamples. In fact, n samples gives you 4n levels of transparency, because RealityEngine uses 2-pixel by 2-pixel dithering. However, screen-door transparency causes a dramatic increase in the number of complex pixels in a scene, which can affect fill performance.
Texturing is free on a RealityEngine if you use a 16-bit texel internal texture format. There are 16-bit texel formats for each number of components. These formats are used by default by IRIS Performer but can be set on a pfTexture with pfTexFormat(). Using a 32-bit texel format will yield half the fill rate of the 16-bit texel formats.
Do not use huge ranges of texture coordinates on individual triangles. This can incur both an image quality degradation and a severe performance degradation. Keep the maximum texture coordinate range for a given component on a single triangle under (1 << (13-log2(TexCSize)) where TexCSize is the size in the dimension of that component.
The use of Detail Texture and Sharpen can greatly improve image quality. Minimize the number of different detail and sharpen splines (or just use the internal default splines). Applying the same detail texture to many base textures can incur a noticeable cost when base textures are changed. Detail textures are intended to be derived from a high-resolution image that corresponds to that of the base texture.