Chapter 6. Performance Tips

Certain graphics features take advantage of the underlying hardware more effectively than others, and UltimateVision graphics systems are no exception. It is important to be able to pick the optimal alternative among the many available. For example, you can feed your data model to the GPU within a glBegin()/glEnd() sequence, but vertex arrays are likely to result in better performance. The following subsections describe techniques that will improve the performance of your application:

Retained Data Model

If possible, use retained data mode instead of immediate mode. Retained mode avoids the need to repeatedly copy geometry and instructions from the host to the graphics board memory for every frame, thus eliminating the system bus bandwidth bottleneck. In addition, the system virtualizes most retained mode structures and pages them in and out as needed. While using retained mode is always a good practice, it is particularly important with UltimateVision systems, which use PCI as the interconnecting bus. Given that the theoretical bandwidth for the PCI bus is only 266 MB/s (and effective bandwidth lower yet) geometry traffic should be optimized at all costs.

Vertex array objects (VAOs) and display lists are the two most attractive ways of providing vertex data to the graphics engine. Geometry-limited scenes that fit entirely within GPU memory should see around 100 Mtris/s. For details, see the performance benchmarks at http://www.ati.com/developer/ATIVertexArrayObject.pdf.

Given the choice between the VAOs and display lists, VAOs are preferable from the performance standpoint. However, if your application already uses display lists, which, unlike vertex arrays, can store instructions as well as data, they can be converted to vertex arrays completely transparently, as described in the section “Display List Optimizer”.

Applications that can benefit the most from switching to retained mode need to satisfy the following conditions:

  • Geometry is static or changes infrequently.

  • Geometry data fits within the GPU memory.

  • Application is geometry-limited, rather than fill-limited.

For more details, see the document http://www.ati.com/developer/ATIVertexArrayObject.pdf.

A similar rule applies to dealing with textures as well as vertex data: using texture objects will generally improve performance by eliminating repeated texture downloads. Also, large or numerous texture objects will be virtualized—that is, swapped in and out as needed by the application—without the need for explicit texture management on the developer's part. This can be a mixed blessing, though. When texture memory is oversubscribed, the resource manager will make its own decisions as to what and when to swap out; this may not suit your application well.

Display List Optimizer

The display list optimizer will attempt to turn display lists into equivalent representations that can be processed more efficiently by the underlying hardware (for example, VAOs). If the process is successful, the resulting geometry will run optimally; otherwise, performance will be similar to that in the immediate mode.

You can determine if your display lists fell within the fast path by analyzing the debug output of the optimizer. This can be accomplished by setting your environment variable FGL_DLOPT_INFO to 1. The OpenGL driver detects the variable and prints information about the degree of optimization applied to the display list. After setting the environment variable, expect printouts of the following form:

List 2153: DL_OPT_CONV_TO_DRAWARRAY: num_begins = 2128, num_prims = 2128
List 2153: DL_OPT_CONNECT_DRAWARRAYS: num_prims = 532
List 2153: DL_OPT_CONV_TO_HW: num_prims = 532, num_hw_prims = 532, bytes_cached = 1327872

DL_OPT_CONV_TO_DRAWARRAY and DL_OPT_CONNECT_DRAWARRAYS are stages in the optimization process. DL_OPT_CONV_TO_HW indicates the most optimized hardware-accelerated lists that are cached in graphics memory.

The following are two of the operations that the optimizer attempts:

  • Converting glBegin()/glEnd() pairs into vertex array objects

  • Connecting and compressing contiguous arrays of the same primitive type (POINTS, LINES, LINESTRIP, TRIANGLES, and TRIANGLE_STRIP). Note that QUADS are not optimized.

Certain data organizations will prevent the optimizer from doing its job. For example, using vertex attributes (colors, normals, texture coordinates) with layouts that vary within the same display list will inhibit optimizations. Therefore, it is best to select a single one even if it means padding data within some lists by replicating per-primitive data. Obviously, this will only work if the replicated geometry data still fits within GPU memory.

It is important to realize that in certain rare situations the optimizer can actually degrade performance. For example, converting very short glBegin()/glEnd() sequences (for example, containing a single QUAD) into vertex array objects can result in a performance penalty due to costly VAO setup.

Since the optimizer's performance relies heavily on the content of the scene, it may not be possible to realize any gains for a specific application. As a matter of fact, some applications may be slower. If this is the case for your application, you can disable the display list optimizer entirely by setting the environment variable FGL_DEBUG_DLOPT to 0.

In order to use these environment variables, systems running IRIX 6.5.22 or 6.5.23 should have SGI patch 5448 or greater.

Vertex Array Objects (VAOs)

Traditionally, vertex arrays are allocated in the client memory and are copied to the server memory whenever they are needed. This substantial overhead can be eliminated by placing such data directly in the persistent server-side memory with help of ATI_vertex_array_object and ATI_element_array OpenGL extensions.

Object buffers can be of any standard type (for example, GL_NORMAL_ARRAY) and are allocated with a  glNewObjectBufferATI() call, which returns a handle to the created buffer for subsequent use. If your vertex array data changes during the course of the application, you can update it with a call to  glUpdateObjectBufferATI(), but if it is done frequently, you should explicitly declare such an object as dynamic with the help of glArrayObjectATI() with the parameter usage set to GL_DYNAMIC_ATI.

VAOs can be rendered in the same way as standard OpenGL vertex arrays: glDrawElements(), glDrawArrays(), and glDrawElement() are all available, even though the first option is generally the best choice. It is best to create one VAO per entire model part; that is, allocate one object, store, and set up all the vertex components for a part within that object, as shown in the following code:

struct Vertex {
float c[4];
float n[3];
float v[3];
    };

    Vertex *vertices;
    int vertexCount;
    unsigned int vobj;

    vertices = new Vertex[vertexCount];
    /* fill in vertex data */

    vobj = glNewObjectBufferATI(sizeof(Vertex) * vertexCount,
            (void *)vertices, GL_STATIC_ATI);

    glEnableClientState(GL_VERTEX_ARRAY);
    glArrayObjectATI(GL_VERTEX_ARRAY, 3, GL_FLOAT, sizeof(Vertex),
                        vobj, offsetof(Vertex, v));
    glEnableClientState(GL_NORMAL_ARRAY);
    glArrayObjectATI(GL_NORMAL_ARRAY, 3, GL_FLOAT, sizeof(Vertex),
                        vobj, offsetof(Vertex, n));
    glEnableClientState(GL_COLOR_ARRAY);
    glArrayObjectATI(GL_COLOR_ARRAY, 4, GL_FLOAT, sizeof(Vertex), vobj,
                        offsetof(Vertex, c));

    glDrawArrays(GL_TRIANGLES, 0, vertexCount);

Element Arrays

Another way to improve performance for geometry-limited applications is to collect as many vertices into a single VAO and provide an additional array of indices that reference it. In this way, any vertices that occur multiple times (for example, shared by adjacent primitives) are only transformed once and cached by the GPU.

UltimateVision graphics systems provide a mechanism for treating a vertex array as a set of indices into another vertex array using the ATI_element_array OpenGL extension. Such an array of indices behaves like any other vertex array in just about all respects. In particular, an element array can be created in the persistent server-side memory as a VAO. The only difference in treatment of element arrays is that there is a dedicated means of rendering indexed objects using glDrawElementArrayATI() or glDrawElementArrayRangeATI(), instead of the traditional glDrawElements().

For more information and code samples, see the document http://www.ati.com/developer/ATIVertexArrayObject.pdf.

Vertex Caches

A GPU on UltimateVision graphics systems has the ability to cache a small number of recently transformed vertices. Applications can leverage this feature by suitably rearranging the input geometry. For example, drawing a non-indexed grid of 120x120 quads will explicitly transform each vertex up to four times (as each internal vertex belongs to four different quads). You will get a significant improvement in performance by using indexed quadstrips and splitting the grid into short swaths that can be cached between adjacent rows, thus requiring them to be transformed only once.

For this technique to work, the following two conditions must be satisfied:

  • Vertices need to be specified in an indexed VAO.

  • Strips need to be short enough so that they are still in the cache when they are needed again.

For example, a 120x120 indexed quadstrip requires sending 120 vertices for a single row. By the time the second row reaches the vertex processor, the previous row will already have been flushed from the cache. Splitting the grid into short spans of 6 quads (12 vertices) assures that the bottom vertices of the first row are still in the vertex cache when the second row is sent, avoiding costly vertex cache misses.

Strip Consolidation

C onsolidating multiple, short, adjacent triangle strips into a single longer strip can also result in improved performance. Converting each strip to a VAO individually replicates the setup overhead for indexed objects. Therefore, reducing the number of strips improves performance.

Suppose you have two strips with the following vertices:

1, 2, 3, 4, 5 and 6, 7, 8, 9

The merged strip looks like the following:

1, 2, 3, 4, 5, 5, 6, 6, 7, 8, 9

The new strip contains some degenerate triangles (for example—5, 5, 6), that the GPU skips. Overall, the gain from avoiding multiple VAO setups is larger than the cost of skipping the degenerate triangles.

Texture Lookup Tables (TLUTs)

Unlike InfiniteReality graphics, the UltimateVision GPU does not support the SGI_texture_color_table extension and, thus, texture lookup tables are not implemented intrinsically. However, if your application relies on this functionality, you can still achieve the same effect by writing a simple fragment program that implements dependent texture lookups. Specifically, you need to do the following:

  1. Initialize and bind a 1D texture, A, containing the lookup table values.

  2. Initialize and bind the original data texture, B.

    For volume data sets, this will likely be a 3D GL_LUMINANCE texture.

  3. Create and bind a fragment program that will use the interpolated value of B to index into the lookup texture A.

The following code shows a simple ARB_fragment_program for dependent texture lookup:

!!ARBfp1.0
TEMP volume;
TEX volume, fragment.texcoord[1], texture[1], 3D;
TEX result.color, volume, texture[0], 1D;
END

 In the preceding code, a temporary register, volume, is used to store the interpolated value of texture B (which is bound to texture unit 1). Subsequently, the 1D texture A (bound to texture unit 0) is sampled using the value stored in volume.

Fragment Program Tips

While fragment programs provide great flexibility, they can affect performance. Long, complex programs can slow things down considerably, particularly for applications that are fill-limited (like volume rendering). The most critical issue to good performance of a custom routine is limiting the total number of instructions.

When writing performance-critical fragment programs, use the following guidelines:

  • Minimize the number of operations.

  • Minimize the number of constants and temporaries.

  • The MOV operation is not needed and should be avoided.

  • Reorder operations to interleave arithmetic operations with texture sampling to hide the latency of texture access.

  • Wherever possible, try accessing textures in a cache-friendly fashion.

  • Unused texture samples are ignored by the driver. So, issuing a TEX operation and ignoring the results will not actually sample the texture.

The driver will attempt to collapse and optimize the code (for example, temporary registers) but it is always safer to start with a good initial code.

Hyper-Z Depth Test

Fill-limited applications pose significant demands on the bandwidth between the framebufffer, depth buffer, and the fragment processor. Not only does each pixel have to be read and written, but also, occlusion calculations need to be performed and require data to be read and written from the depth buffer. Hyper-Z circuitry eliminates processing overhead for fragments which are known to be occluded by looking at the depth coordinates of 8x8 blocks of the depth buffer. In addition, the rendering engine applies early Z termination to fragments that fail the block-wide Z test. None of such fragments are rendered at all. This may result in substantial savings, particularly if texturing or complex custom shading operations are being applied. While such conditional evaluation tends to stall the pipeline by introducing an additional overhead of flushing the pipeline, the approach may be justified as the shader complexity increases.

As a result, fill rate for a scene that is drawn from front to back can be much higher (by as much as 10x) than that for back-to-front or in random order. Performance-sensitive developers may choose to pre-sort their geometry to take advantage of this feature. This approach can be particularly effective if the viewpoint does not change dramatically from frame to frame so that visibility sorting can only be repeated periodically (obviously, the scene does not have to be perfectly sorted for each view, but the more the better). It may be best to update the sort in a separate asynchronous thread to eliminate dependency of each frame on the sorting time. Even a coarse, object-level sort may be very effective in taking advantage of Hyper-Z.

Timing

On UltimateVision systems, the first operation to touch the framebuffer followed by a glXSwapBuffers() and then a glFlush() call is where the pipe blocks for the vertical retrace (if sync to vblank is enabled), not at a glXSwapBuffers() call. Therefore, if you are timing a frame, use the following calls after glXSwapBuffers() and before the timing check:

glNormal3f(0, 0, 1); glFlush();

The call glNormal3f(0, 0, 1) is not enough alone.

Pixel Formats

Transferring pixel data (for example, textures) to and from the video memory can have a substantial impact on the performance of your application. The following are some rules that will help you avoid many pitfalls:

  • For GL_UNSIGNED_BYTE reading, the GL_RGB and GL_RGBA formats are the current fastest modes.

  • For GL_UNSIGNED_BYTE writing, the fastest modes are GL_RGBA followed by GL_RGB.

  • For GL_DEPTH_COMPONENT writing, the GL_UNSIGNED_SHORT type is the fastest type.

  • When downloading GL_RGB and GL_RGBA GL_UNSIGNED_BYTE data to a texture, it is best to download to a GL_RGBA internal format.

 For performance numbers, see Appendix A, “Performance Benchmarks”.