Chapter 14. Tuning the Pipeline
Prev |  | Next

## Chapter 14. Tuning the Pipeline

This chapter discusses tuning the graphics pipeline. It presents a variety of techniques for optimizing the different parts of the pipeline, providing code fragments and examples as appropriate. You learn about

- “CPU Tuning: Basics”

- “CPU Tuning: Immediate Mode Drawing”

- “CPU Tuning: Display Lists”

- “CPU Tuning: Advanced Techniques”

- “Tuning the Geometry Subsystem”

- “Tuning the Raster Subsystem”

- “Tuning the Imaging Pipeline”

## CPU Tuning: Basics

The first stage of the rendering pipeline is traversal of the data and sending of the current rendering data to the rest of the pipeline. In theory, the entire rendering database (scene graph) must be traversed in some fashion for each frame because both scene content and viewer position can be dynamic.

To get the best possible CPU performance, follow these two overall guidelines:

- Compile your application for optimum speed.

  Compile all object files with at least `-O2`. Note that the compiler option for debugging, `–g`, turns off all optimization. If you must run the debugger on optimized code, you can use `–g3` with `-O2` with limited success. If you are not compiling with `-xansi` (the default) or `-ansi`, you may need to include `–float` for faster floating-point operations.

  On certain platforms, other compile-time options (such as `-mips3` or `-mips4`) are available.

- If you aren't concerned about backward compatibility, compile for the n32 abi instead of compiling for o32. The default on IRIX 6.5 is n32.

- Use a simple data structure and a fast traversal method.

  The CPU tuning strategy focuses on developing fast database traversal for drawing with a simple, easily accessed data structure. The fastest rendering is achieved with an inner loop that traverses a completely flattened (non-hierarchical) database. Most applications cannot achieve this level of simplicity for a variety of reasons. For example, some databases occupy too much memory when completely flattened. Note also that you run a greater risk of cache misses if you flatten the data.

When an application is CPU limited, the entire graphics pipeline may be sitting idle for periods of time. The following sections describe techniques for structuring application code so that the CPU doesn't become the bottleneck.

### Immediate Mode Drawing Versus Display Lists

When deciding whether you want to use display list or immediate mode drawing, consider the amount of work you do in constructing your databases and using them for purposes other than graphics. Here are three cases to consider:

- If you create models that never change, and are used only for drawing, then OpenGL display lists are the right representation.

  Display lists can be optimized in hardware-specific ways, loaded into dedicate display list storage in the graphics subsystem, downloaded to on-board dlist RAM, and so on. See “CPU Tuning: Display Lists” for more information on display lists.

- If you create models that are subject to infrequent change, but are rarely used for any purpose other than drawing, then vertex arrays are the right representation.

  Vertex Arrays are relatively compact and have modest impact on cache. Software renderers can process the vertices in batches; hardware renderers can trickle triangles out a few at a time to maximize parallelism. As long as the vertex arrays can be retained from frame to frame, so you do not incur a lot of latency by building them afresh each frame, they are the best solution for this case. See “Using Vertex Arrays” for more information.

- If you create very dynamic models, or if you use the data for heavy computations unrelated to graphics, then the `glVertex()`-style interface (immediate mode drawing) is the best choice.

  Immediate mode drawing allows you to maximize parallelism for hardware renderers and to optimize your database for the other computations you need to perform, and it reduces cache thrashing. Overall, this will result in higher performance than forcing the application to use a graphics-oriented data structure like a vertex array. Use immediate-mode drawing for large databases (which might have to be paged into main memory) and dynamic databases, for example for morphing operations where the number of vertices is subject to change, or for progressive refinement. See “CPU Tuning: Immediate Mode Drawing” for tuning information.

If you are still not sure whether to choose display lists or immediate mode drawing, consider the following advantages and disadvantages of display lists.

Display lists have the following advantages:

- You don't have to optimize traversal of the data yourself; display list traversal is well-tuned and more efficient than user programs.

- Display lists manage their own data storage. This is particularly useful for algorithmically generated objects.

- Display lists are significantly better for remote graphics over a network. The display list can be cached on the remote CPU so that the data for the display list does not have to be re-sent every frame. Furthermore, the remote CPU handles much of the responsibility for traversal.

- Display lists are preferable for direct rendering if they contain enough primitives (a total of about ten) because display lists are stored efficiently. If the lists are short, the setup performance cost is not offset by the more efficient storage or saving in CPU time.

- For information on display lists on Indigo2 IMPACT systems, see “Using Display Lists Effectively”.

Display lists do have drawbacks that may affect some applications:

- The most troublesome drawback of display lists is data expansion. To achieve fast, simple traversal on all systems, all data is copied directly into the display list. Therefore, the display list contains an entire copy of all application data plus additional overhead for each command. If the application has no other need for the data then drawing, it can release the storage for its copy of the data and the penalty is negligible.

- If vertices are shared in structures more complex than the OpenGL primitives (line strip, triangle strip, triangle fan, quad strip), they are stored more than once.

- If the database becomes sufficiently large, paging eventually hinders performance. Therefore, when contemplating the use of OpenGL display lists for really large databases, consider the amount of main memory.

- Compiling display lists may take some time.

## CPU Tuning: Display Lists

In display-list mode, pieces of the database are compiled into static chunks that can then be sent to the graphics pipeline. In this case, the display list is a separate copy of the database that can be stored in main memory in a form optimized for feeding the rest of the pipeline.

For example, suppose you want to apply a transformation to some geometric objects and then draw the result. If the geometric objects are to be transformed in the same way each time, it is better to store the matrix in the display list. The database traversal task is to hand the correct chunks to the graphics pipeline. Display lists can be recreated easily with some additional performance cost.

Tuning for display lists focuses mainly on reducing storage requirements. Performance improves if the data fit in the cache because this avoids cache misses when the data is t traversed again.This section explains how to optimize display lists.

Follow these rules to optimize display lists:

- If possible, compile and execute a display list in two steps instead of using GL_COMPILE_AND_EXECUTE.

- Call `glDeleteLists()` to delete display lists that are no longer needed.

  This frees storage space used by the deleted display lists and expedites the creation of new display lists.

- Avoid duplication of display lists.

  For example, if you have a scene with 100 spheres of different sizes and materials, generate one display list that is a unit sphere centered about the origin. Then for each sphere in the scene, follow these steps:

  1. Set the material for the current sphere.

  2. Issue the necessary scaling and translation commands for sizing and positioning the sphere—watch out for scaling of normals.

  3. Invoke `glCallList()` to draw the unit sphere display list.

  In this way, a reference to the unit sphere display list is stored instead of all of the sphere vertices for each instance of the sphere.

- Make the display list as flat as possible, but be sure not to exceed the cache size.

  Avoid using an excessive hierarchy with many invocations of `glCallList()`. Each `glCallList()` invocation results in a lookup operation to find the designated display list. A flat display list requires less memory and yields simpler and faster traversal. It also improves cache coherency.

Display lists are best used for static objects. Do not put dynamic data or operations in display lists. Instead, use a mixture of display lists for static objects and immediate mode for dynamic operations.

| Note: See Chapter 16, “System-Specific Tuning,” for potential display list optimizations on the system you are using.

## CPU Tuning: Immediate Mode Drawing

Immediate mode drawing means that OpenGL commands are executed when they are called, rather than from a display list. This style of drawing provides flexibility and control over both storage management and drawing traversal. The trade-off for the extra control is that you have to write your own optimized subroutines for data traversal. Tuning therefore has two parts:

- “Optimizing the Data Organization”

- “Optimizing Database Rendering Code”

While you may not use each technique in this section, minimize the CPU work done at the per-vertex level and use a simple data structure for rendering traversal.

There is no recipe for writing a peak-performance immediate mode renderer for a specific application. To predict the CPU limitation of your traversal, design potential data structures and traversal loops and write small benchmarks that mimic the memory demands you expect. Experiment with optimizations and benchmark the effects. Experimenting on small examples can save time in the actual implementation.

### Optimizing the Data Organization

It is common for scenes to have hierarchical definitions. Scene management techniques may rely on specific hierarchical information. However, a hierarchical organization of the data raises several performance concerns:

- The time spent traversing pointers to different sections of a hierarchy can create a CPU bottleneck.

  This is partly because of the number of extra instructions executed, but it is also a result of the inefficient use of cache and memory. Overhead data not needed for rendering is brought through the cache and can push out needed data, causing subsequent cache misses.

- Traversing hierarchical structures can cause excessive memory paging.

  Hierarchical structures can be distributed throughout memory. It is difficult to be sure of the exact amount of data you are accessing and of its exact location; traversing hierarchical structures can therefore access a costly number of pages.

- Complex operations may need access to both the geometric data and other scene information, complicating the data structure.

- Caching behavior is often difficult to predict for dynamic hierarchical data structures.

For these reasons, hierarchy should be used with care. In general, store the geometry data used for rendering in static, contiguous buffers, rather than in the hierarchical data structures.

- Do not interlace data used to render frames and infrequently used data in memory. Instead, include a pointer to the infrequently used data and store the data itself elsewhere.

- Flatten your rendering data (minimize the number of levels in the hierarchy) as much as cache and memory considerations and your application constraints permit.

  The appropriate amount of flattening depends on the system on which your application will run.

- Balance the data hierarchy. This makes application culling (the process of eliminating objects that don't fall within the viewing frustum) more efficient and effective.

### Optimizing Database Rendering Code

This section includes some suggestions for writing peak-performance code for inner rendering loops.

During rendering, an application ideally spends most of its time traversing the database and sending data to the graphics pipeline. Instructions in the display loop are executed many times every frame, creating hot spots. Any extra overhead in a hot spot is greatly magnified by the number of times it is executed.

When using simple, high-performance graphics primitives, the application is even more likely to be CPU limited. The data traversal must be optimized so that it does not become a bottleneck.

During rendering, the sections of code that actually issue graphics commands should be the hot spots in application code. These subroutines should use peak-performance coding methods. Small improvements to a line that is executed for every vertex in a database accumulate to have a noticeable effect when the entire frame is rendered.

The rest of this section looks at examples and techniques for optimizing immediate-mode rendering:

- “Examples for Optimizing Data Structures for Drawing”

- “Examples for Optimizing Program Structure”

- “Using Specialized Drawing Subroutines and Macros”

- “Preprocessing Drawing Data: Introduction”

- “Preprocessing Meshes Into Fixed-Length Strips”

- “Preprocessing Vertex Loops”

#### Examples for Optimizing Data Structures for Drawing

Follow these suggestions for optimizing how your application accesses data:

- One-Dimensional Arrays. Use one-dimensional arrays traversed with a pointer that always holds the address for the current drawing command. Avoid array-element addressing or multidimensional array accesses.

      bad:  glVertex3fv(&data[i][j][k]);
      good: glVertex3fv(dataptr);

- Adjacent structures. Keep all static drawing data for a given object together in a single contiguous array traversed with a single pointer. Keep this data separate from other program data, such as pointers to drawing data, or interpreter flags.

- Flat structures. Use flat data structures and do not use multiple pointer indirection when rendering:

  Good |  | `glVertex3fv(object->data->vert)`;
  OK |  | `glVertex3fv(dataptr->vert);`
  Bad |  | `glVertex3fv(dataptr);`

  The following code fragment is an example of efficient code to draw a single smooth-shaded, lit polygon. Notice that a single data pointer is used. It is updated once at the end of the polygon, after the `glEnd()` call.

      glBegin(GL_QUADS);
      glNormal3fv(ptr);
      glVertex3fv(ptr+3);
      glNormal3fv(ptr+6);
      glVertex3fv(ptr+9);
      glNormal3fv(ptr+12);
      glVertex3fv(ptr+15);
      glNormal3fv(ptr+18);
      glVertex3fv(ptr+21);
      glEnd();
      ptr += 24;

#### Examples for Optimizing Program Structure

- Loop unrolling (1). Avoid short, fixed-length loops, especially around vertices. Instead, unroll these loops:

  Bad |  | `for(i=0; i < 4; i++){ glColor4ubv(poly_colors[i]); glVertex3fv(poly_vert_ptr[i]); }`
  Good |  | `glColor4ubv(poly_colors[0]); glVertex3fv(poly_vert_ptr[0]); glColor4ubv(poly_colors[1]); glVertex3fv(poly_vert_ptr[1]); glColor4ubv(poly_colors[2]); glVertex3fv(poly_vert_ptr[2]); glColor4ubv(poly_colors[3]); glVertex3fv(poly_vert_ptr[3]);`

- Loop unrolling (2). Minimize the work done in a loop to maintain and update variables and pointers. Unrolling can often assist in this:

  Bad |  | `glNormal3fv(*(ptr++)); glVertex3fv(*(ptr++)); or glNormal3fv(ptr); ptr += 4; glVertex3fv(ptr); ptr += 4;`
  Good |  | `glNormal3fv(*(ptr)); glVertex3fv(*(ptr+1)); glNormal3fv(*(ptr+2)); glVertex3fv(*(ptr+3));` `or` `glNormal3fv(ptr); glVertex3fv(ptr+4); glNormal3fv(ptr+8); glVertex3fv(ptr+12);` ``

  | Note: On some processors, such as the R8000 and R10000, loop unrolling may hurt performance more than it helps, so use it with caution. In fact, unrolling too far hurts on any processor because the loop may use an excessive portion of the cache. If it uses a large enough portion of the cache, it may interfere with itself; that is, the whole loop won't fit (not likely) or it may conflict with the instructions of one of the subroutines it calls.

- Loops accessing buffers. Minimize the number of different buffers accessed in a loop:

  Bad |  | `glNormal3fv(normaldata); glTexCoord2fv(texdata); glVertex3fv(vertdata);`
  Good |  | `glNormal3fv(dataptr); glTexCoord2fv(dataptr+3); glVertex3fv(dataptr+5);`

- Loop end conditions. Make end conditions on loops as trivial as possible; for example, compare the loop variable to a constant, preferably zero. Decrementing loops are often more efficient than their incrementing counterparts:

  Bad

      for (i = 0; i < (end-beginning)/size; i++)
          {...}

  Better

      for (i = beginning; i < end; i += size)
          {...}

  Good

      for (i = total; i > 0; i--)
          {...}

- Conditional statements.

  - Use `switch` statements instead of multiple `if-else-if` control structures.

  - Avoid `if` tests around vertices; use duplicate code instead.

- Subroutine prototyping. Prototype subroutines in ANSI C style to avoid runtime typecasting of parameters:

      void drawit(float f, int count)
      {
        .......
      }

- Multiple primitives. Send multiple primitives between `glBegin()/glEnd()` whenever possible:

      glBegin(GL_TRIANGLES)
      ....
      ..../* many triangles */
      ....
      glEnd

      glBegin(GL_QUADS)
      ....
      ..../* many quads */
      ....
      glEnd

#### Using Specialized Drawing Subroutines and Macros

This section looks at several ways to improve performance by making appropriate choices about display modes, geometry, and so on.

- Geometry display choices. Make decisions about which geometry to display and which modes to use at the highest possible level in the program organization.

  The drawing subroutines should be highly specialized leaves in the program's call tree. Decisions made too far down the tree can be redundant. For example, consider a program that switches back and forth between flat-shaded and smooth-shaded drawing. Once this choice has been made for a frame, the decision is fixed and the flag is set. For example, the following code is inefficient:

      /* Inefficient way to toggle modes */
      draw_object(float *data, int npolys, int smooth)  {
      int i;
      glBegin(GL_QUADS);
      for (i = npolys; i > 0; i--) {
          if (smooth) glColor3fv(data);
          glVertex3fv(data + 4);
          if (smooth) glColor3fv(data + 8);
          glVertex3fv(data + 12);
          if (smooth) glColor3fv(data + 16);
          glVertex3fv(data + 20);
          if (smooth) glColor3fv(data + 24);
          glVertex3fv(data + 28);
      }
      glEnd();

  Even though the program chooses the drawing mode before entering the `draw_object()` routine, the flag is checked for every vertex in the scene. A simple `if` test may seem innocuous; however, when done on a per-vertex basis, it can accumulate a noticeable amount of overhead.

  Compare the number of instructions in the disassembled code for a call to `glColor3fv()`, first without, and then with, the `if` test.

  Assembly code for a call without `if` test (six instructions):

      lw a0,32(sp)
      lw t9,glColor3fv
      addiu a0,a0,32
      jalr ra,t9
      nop
      lw gp,24(sp)

  Assembly code for a call with an `if` test (eight instructions):

      lw t7,40(sp)
      beql t7,zero,0x78
      nop
      lw t9,glColor3fv
      lw a0,32(sp)
      jalr ra,t9
      addiu a0,a0,32
      lw gp,24(sp)

  Notice the two extra instructions required to implement the `if` test. The extra `if` test per vertex increases the number of instructions executed for this otherwise optimal code by 33%. These effects may not be visible if the code is used only to render objects that are always graphics limited. However, if the process is CPU-limited, then moving decision operations such as this `if` test higher up in the program structure improves performance.

#### Preprocessing Drawing Data: Introduction

Putting some extra effort into generating a simpler database makes a significant difference when traversing that data for display. A common tendency is to leave the data in a format that is good for loading or generating the object, but not optimal for actually displaying it. For peak performance, do as much of the work as possible before rendering.

Preprocessing turns a difficult database into a database that is easy to render quickly. This is typically done at initialization or when changing from a modeling to a fast-rendering mode. This section discusses “Preprocessing Meshes Into Fixed-Length Strips” and “Preprocessing Vertex Loops” to illustrate this point.

#### Preprocessing Meshes Into Fixed-Length Strips

Preprocessing can be used to turn general meshes into fixed-length strips.

The following sample code shows a commonly used, but inefficient, way to write a triangle strip render loop:

    float* dataptr;
    ...
    while (!done) switch(*dataptr) {
        case BEGINSTRIP:
            glBegin(GL_TRIANGLE_STRIP);
            dataptr++;
            break;
        case ENDSTRIP:
            glEnd();
            dataptr++;
            break;
        case EXIT:
            done = 1;
            break;
        default: /* have a vertex !!! */
            glNormal3fv(dataptr);
            glVertex3fv(dataptr + 4);
            dataptr += 8;
    }

This traversal method incurs a significant amount of per-vertex overhead. The loop is evaluated for every vertex and every vertex must also be checked to make sure that it is not a flag. These checks waste time and also bring all of the object data through the cache, reducing the performance advantage of triangle strips. Any variation of this code that has per-vertex overhead is likely to be CPU limited for most types of simple graphics operations.

#### Preprocessing Vertex Loops

Preprocessing is also possible for vertex loops:

    glBegin(GL_TRIANGLE_STRIP);
    for (i=num_verts; i > 0; i--) {
        glNormal3fv(dataptr);
        glVertex3fv(dataptr+4);
        dataptr += 8;
        }
    glEnd();

For peak immediate mode performance, precompile strips into specialized primitives of fixed length. Only a few fixed lengths are needed. For example, use strips that consist of 12, 8, and 2 primitives.

| Note: The optimal strip length may vary depending on the hardware the program runs on. For more information, see Chapter 16, “System-Specific Tuning.”

The specialized strips are sorted by size, resulting in the efficient loop shown in this sample code:

    /* dump out N 8-triangle strips */
    for (i=N; i > 0; i--) {
        glBegin(GL_TRIANGLE_STRIP);
        glNormal3fv(dataptr);
        glVertex3fv(dataptr+4);
        glNormal3fv(dataptr+8);
        glVertex3fv(dataptr+12);
        glNormal3fv(dataptr+16);
        glVertex3fv(dataptr+20);
        glNormal3fv(dataptr+24);
        glVertex3fv(datatpr+28);
        ...
        glEnd();
        dataptr += 64;
    }

A mesh of length 12 is about the maximum for unrolling. Unrolling helps to reduce the overall cost-per-loop overhead, but after a point, it produces no further gain.

Over-unrolling eventually hurts performance by increasing code size and reducing effectiveness of the instruction cache. The degree of unrolling depends on the processor; run some benchmarks to understand the optimal program structure on your system.

## Optimizing Cache and Memory Use

This section first provides some background information about the structure of the cache and about memory lookup. It then gives some tips for optimizing cache and memory use.

### Memory Organization

On most systems, memory is structured as a hierarchy that contains a small amount of faster, more expensive memory at the top and a large amount of slower memory at the base. The hierarchy is organized from registers in the CPU at the top down to the disks at the bottom. As memory locations are referenced, they are automatically copied into higher levels of the hierarchy, so data that is referenced most often migrates to the fastest memory locations.

Here are the areas you should be most concerned about:

- The cache feeds data to the CPU, and cache misses can slow down your program.

  Each processor has instruction caches and data caches. The purpose of the caches is to feed data and instructions to the CPU at maximum speed. When data is not found in the cache, a cache miss occurs and a performance penalty is incurred as data is brought into the cache.

- The translation-lookaside buffer (TLB) keeps track of the location of frequently used pages of memory. If a page translation is not found in the TLB, a delay is incurred while the system looks up the page and enters its translation.

The goal of machine designers and programmers is to maximize the chance of finding data as high up in the memory hierarchy as possible. To achieve this goal, algorithms for maintaining the hierarchy, embodied in the hardware and the operating system, assume that programs have locality of reference in both time and space; that is, programs keep frequently accessed locations close together. Performance increases if you respect the degree of locality required by each level in the memory hierarchy.

Even applications that appear not to be memory intensive, in terms of total number of memory locations accessed, may suffer unnecessary performance penalties for inefficient allocation of these resources. An excess of cache misses, especially misses on read operations, can force the most optimized code to be CPU limited. Memory paging causes almost any application to be severely CPU limited.

### Minimizing Paging

This section provides some guidelines for minimizing memory paging. You learn about:

- “Minimizing Lookup”

- “Minimizing Cache Misses”

- “Measuring Cache-Miss and Page-Fault Overhead”

#### Minimizing Lookup

To minimize page lookup, follow these guidelines:

- Keep frequently used data within a minimal number of pages. Starting with IRIX 6.5, each page consists of 16 KB. In earlier versions of IRIX, each page consists of 4 KB (16 KB in high-end systems). Minimize the number of pages referenced in your program by keeping data structures within as few pages as possible. Use `osview` to verify that no TLB misses are occurring.

- Store and access data in flat, sequential data structures, particularly for frequently referenced data. Every pointer indirection could result in the reading of a new page. This is guaranteed to cause performance problems with CPUs like R10000 that try to do instructions in parallel.

- In large applications (which cause memory swapping), use `mpin()` to lock important memory into RAM.

#### Minimizing Cache Misses

Each processor may have first-level instruction and data caches on chip and have second-level cache(s) that are bigger but somewhat slower. The sizes of these caches vary; you can use the `hinv` command to determine the sizes on your system. The first-level data cache is always a subset of the data in the second-level cache.

Memory access is much faster if the data is already loaded into the first-level cache. When your program accesses data that is not in one of the caches, a cache miss results. This causes a cache line of several bytes, including the data you just accessed, to be read from memory and stored in the cache. The size of this transaction varies from machine to machine. Caches are broken down into lines, typically 32-128 bytes. When a cache miss occurs, the corresponding line is loaded from the next level down in the hierarchy.

Because cache misses are costly, try to minimize them by following these steps:

- Keep frequently accessed data together. Store and access frequently used data in flat, sequential files and structures and avoid pointer indirection. This way, the most frequently accessed data remains in the first-level cache wherever possible.

- Access data sequentially. If you are accessing words sequentially, each cache miss brings in 32 or more words of needed data; if you are accessing every 32nd word, each cache miss brings in one needed word and 31 unneeded words, degrading performance by up to a factor of 32.

- Avoid simultaneously traversing several large independent buffers of data, such as an array of vertex coordinates and an array of colors within a loop. There can be cache conflicts between the buffers. Instead, pack the contents into one interleaved buffer when possible. If this packing forces a big increase in the size of the data, it may not be the right optimization for that program. If you are using vertex arrays, try using interleaved arrays.

Second-level data cache misses also increase bus traffic, which can be a problem in a multi-processing application. This can happen with multiple processes traversing very large data sets. See “Immediate Mode Drawing Versus Display Lists” for additional information.

#### Measuring Cache-Miss and Page-Fault Overhead

To find out if cache and memory usage are a significant part of your CPU limitation, follow these guidelines:

- Use `osview` to monitor your application.

- A more rigorous way to estimate the time spent on memory access is to compare the execution profiling results collected with PC sampling with those of basic block counting, performing each test with and without calls to `glVertex3fv()`.

  - PC sampling in Speedshop gives a real-time estimate of the time spent in different sections of the code.

  - Basic block counting, from Speedshop, gives an ideal estimate of how much time should be spent, not including memory references.

  See the `speedshop` reference page or the Speedshop User's Guide for more information.

  PC sampling includes time for system overhead, so it always predicts longer execution than basic block counting. However, your PC sample time should not be more than 1.5 times the time predicted by Speedshop.

  The CASEVision/WorkShop tools, in particular the performance analyzer, can also help with those measurements. The WorkShop Overview introduces the tools.

## CPU Tuning: Advanced Techniques

After you have applied the techniques discussed in the previous sections, consider using these advanced techniques to tune CPU-limited applications:

- “Mixing Computation With Graphics”

- “Examining Assembly Code”

- “Using Additional Processors for Complex Scene Management”

- “Modeling to the Graphics Pipeline”

### Mixing Computation With Graphics

When you are fine-tuning an application, interleaving computation and graphics can make it better balanced and therefore more efficient. Key places for interleaving are after `glXSwapBuffers()`, `glClear()`, and drawing operations that are known to be fill limited (such as drawing a backdrop or a ground plane or any other large polygon).

A `glXSwapBuffers()` call creates a special situation. After calling `glXSwapBuffers()`, an application may be forced to wait for the next vertical retrace (in the worst case, up to 16.7 msecs) before it can issue more graphics calls. For a program drawing 10 frames per second, 15% of the time (worst case) can be spent waiting for the buffer swap to occur.

In contrast, non-graphic computation is not forced to wait for a vertical retrace. Therefore, if there is a section of computation that must be done every frame that includes no graphics calls, it can be done after the `glXSwapBuffers()` instead of causing a CPU limitation during drawing.

Clearing the screen is a time-consuming operation. Doing non-graphics computation immediately after the clear is more efficient than sending additional graphics requests down the pipeline and being forced to wait when the pipeline's input queue overflows.

Experimentation is required to

- determine where the application is reliably graphics limited

- ensure that inserting the computation does not create a new bottleneck

For example, if a new computation references a large section of data that is not in the data cache, the data for drawing may be swapped out for the computation, then swapped back in for drawing, resulting in worse performance than the original organization.

### Examining Assembly Code

When tuning inner rendering loops, examining assembly code can be helpful. Use `dis` to disassemble optimized code for a given procedure, and correlate assembly code lines with line numbers from the source code file. This correlation is especially helpful for examining optimized code. The `-S` option to `cc` produces a `.s` file of assembly output, complete with your original comments.

You need not be an expert in MIPS assembly code to interpret the results. Just looking at the number of extra instructions required for an apparently innocuous operation is informative. Knowing some basics about MIPS assembly code can be helpful for finding performance bugs in inner loops. See MIPS RISC Architecture, by Gerry Kane, listed in “Background Reading” for additional information.

### Using Additional Processors for Complex Scene Management

If your application is running on systems with multiple processors, consider supplying an option for doing scene management on additional processors to relieve the rendering processor from the burden of expensive computation.

Using additional processors may also reduce the amount of data rendered for a given frame. Simplifying or reducing rendering for a given scene can help reduce bottlenecks in all parts of the pipeline, as well as the CPU. One example is removing unseen or backfacing objects. Another common technique is to use an additional processor to determine when objects are going to appear very far away and use a simpler model with fewer polygons and less expensive modes for distant objects.

### Modeling to the Graphics Pipeline

The modeling of the database directly affects the rendering performance of the resulting application and therefore has to match the performance characteristics of the graphics pipeline and make trade-offs with the database traversals. Graphics pipelines that support connected primitives, such as triangle meshes, benefit from having long meshes in the database. However, the length of the meshes affects the resulting database hierarchy, and long strips through the database do not cull well with simple bounding geometry.

Model objects with an understanding of inherent bottlenecks in the graphics pipeline:

- Pipelines that are severely fill limited benefit from having objects modeled with cut polygons and more vertices and fewer overlapping parts, which decreases depth complexity.

- Pipelines that are easily geometry- or host-limited benefit from modeling with fewer polygons.

There are several other modeling tricks that can reduce database complexity:

- Use textured polygons to simulate complex geometry. This is especially useful if the graphics subsystem supports the use of textures where the alpha component of the texture marks the transparency of the object. Textures can be used as cut-outs for objects like fences and trees.

- Use textures for simulating particles, such as smoke.

- Use textured polygons as single-polygon billboards. Billboards are polygons that are fixed at a point and rotated about an axis, or about a point, so that the polygon always faces the viewer. Billboards are useful for symmetric objects such as light posts and trees, and also for volume objects such as smoke. Billboards can also be used for distant objects to save geometry. However, the managing of billboard transformations can be expensive and affect both the cull and the draw processes.

  In OpenGL 1.1, the sprite extension can be used for billboards on certain platforms; see “SGIX_sprite—The Sprite Extension”.

## Tuning the Geometry Subsystem

The geometry subsystem is the part of the pipeline in which per-polygon operations, such as coordinate transformations, lighting, texture coordinate generation, and clipping are performed. The geometry hardware may also be used for operations that are not strictly transform operations, such as convolution.

This section presents techniques that you can use to tune the geometry subsystem, discussing the following topics:

- “Using Peak Performance Primitives for Drawing”

- “Using Vertex Arrays”

- “Using Display Lists as Appropriate”

- “Optimizing Transformations”

- “Optimizing Lighting Performance”

- “Choosing Modes Wisely”

- “Advanced Transform-Limited Tuning Techniques”

### Using Peak Performance Primitives for Drawing

This section describes how to draw geometry with optimal primitives. Consider these guidelines to optimize drawing:

- Use connected primitives (line strips, triangle strips, triangle fans, and quad strips). Put at least 8 primitives in a sequence, 12 to 16 if possible.

  Connected primitives are desirable because they reduce the amount of data sent to the graphics subsystem and the amount of per-polygon work done in the pipeline. Typically, about 12 vertices per `glBegin()/glEnd()` are required to achieve peak rates (but this can vary depending on the hardware you are running on). For lines and points, it is especially beneficial to put as many vertices as possible in a `glBegin()/glEnd()` sequence. For information on the most efficient vertex numbers for the system you are using, see Chapter 16, “System-Specific Tuning.”

- Use “well-behaved” polygons—convex and planar, with only three or four vertices.

  If you use concave and self-intersecting polygons, they are broken down into triangles by OpenGL. For high-quality rendering, you must pass the polygons to GLU to be tessellated. This can make them prohibitively expensive. Nonplanar polygons and polygons with large numbers of vertices are more likely to exhibit shading artifacts.

  If your database has polygons that are not well-behaved, perform an initial one-time pass over the database to transform the troublemakers into well-behaved polygons and use the new database for rendering. Using connected primitives results in additional gains.

- Minimize the data sent per vertex.

  Polygon rates can be affected directly by the number of normals or colors sent per polygon. Setting a color or normal per vertex, regardless of the `glShadeModel()` used, may be slower than setting only a color per polygon, because of the time spent sending the extra data and resetting the current color. The number of normals and colors per polygon also directly affects the size of a display list containing the object.

- Group like primitives and minimize state changes to reduce pipeline revalidation.

### Using Vertex Arrays

Vertex arrays are available in OpenGL 1.1. They offer the following benefits:

- The OpenGL implementation can take advantage of uniform data formats.

- The `glInterleavedArrays()` call lets you specify packed vertex data easily. Packed vertex formats are typically faster for OpenGL to process.

- The `glDrawArrays()` call reduces subroutine call overhead.

- The `glDrawElements()` call reduces subroutine call overhead and also reduces per-vertex calculations because vertices are reused.

### Using Display Lists as Appropriate

You can often improve geometry performance by storing frequently-used commands in a display list. If you plan to redraw the same geometry multiple times, or if you have a set of state changes that are applied multiple times, consider using display lists. Display lists allow you to define the geometry or state changes once and execute them multiple times. Some graphics hardware stores display lists in dedicated memory or stores data in an optimized form for rendering (see also “CPU Tuning: Display Lists”).

### Storing Data Efficiently

Putting some extra effort into generating a more efficient database makes a significant difference when traversing the data for display. A common tendency is to leave the data in a format that is good for loading or generating the object, but not optimal for actually displaying the data. For peak performance, do as much work as possible before rendering. Preprocessing of data is typically performed at initialization time or when changing from a modeling mode to a fast rendering mode.

### Minimizing State Changes

Your program will almost always benefit if you reduce the number of state changes. A good way to do this is to sort your scene data according to what state is set and render primitives with the same state settings together. Primitives should be sorted by the most expensive state settings first. Typically it is expensive to change texture binding, material parameters, fog parameters, texture filter modes, and the lighting model. However, some experimentation will be required to determine which state settings are most expensive on the system you are running on. For example, on systems that accelerate rasterization, it may not be very expensive to disable or enable depth testing or to change rasterization controls such as the depth test function. But if you are running on a system with software rasterization, this may cause the graphics pipeline to be revalidated.

It is also important to avoid redundant state changes. If your data is stored in a hierarchical database, make decisions about which geometry to display and which modes to use at the highest possible level. Decisions that are made too far down the tree can be redundant.

### Optimizing Transformations

OpenGL implementations are often able to optimize transform operations if the matrix type is known. Follow these guidelines to achieve optimal transform rates:

- Call `glLoadIdentity()` to initialize a matrix rather than loading your own copy of the identity matrix.

- Use specific matrix calls such as `glRotate*()`, `glTranslate*()`, and `glScale*()` rather than composing your own rotation, translation, or scale matrices and calling `glLoadMatrix()` or `glMultMatrix()`.

- If possible, use single precision such as `glRotatef()`, `glTranslatef()`, and `glScalef().` (On most systems, this may not be critical because the CPU converts doubles to floats).

### Optimizing Lighting Performance

OpenGL offers a large selection of lighting features: Some are virtually “free” in terms of computational time, others offer sophisticated effects with some performance penalty. For some features, the penalties may vary depending on the hardware the application is running on. Be prepared to experiment with the lighting configuration.

As a general rule, use the simplest possible lighting model, a single infinite light with an infinite viewer. For some local effects, try replacing local lights with infinite lights and a local viewer.

You normally won't notice a performance degradation when using one infinite light, unless you use lit textures or color index lighting.

Use the following settings for peak performance lighting:

- Single infinite light.

  - GL_LIGHT_MODEL_LOCAL_VIEWER set to GL_FALSE in `glLightModel()` (the default).

  - GL_LIGHT_MODEL_TWO_SIDE set to GL_FALSE in `glLightModel()` (the default).

  - Local lights are noticeably more expensive than infinite lights. Avoid lighting where the fourth component of GL_LIGHT_POSITION is nonzero.

  - There may be a sharp drop in lighting performance when switching from one light to two lights, but the drop for additional lights is likely to be more gradual.

- RGB mode.

- GL_COLOR_MATERIAL disabled.

- GL_NORMALIZE disabled—Because this is usually necessary when the model-view matrix includes a scaling transformation, consider preprocessing the scene to eliminate scaling.

#### Lighting Operations With Noticeable Performance Costs

Follow these additional guidelines to achieve peak lighting performance:

- Don't change material parameters frequently.

  Changing material parameters can be expensive. If you need to change the material parameters many times per frame, consider rearranging the scene traversal to minimize material changes. Also consider using `glColorMaterial()` to change specific parameters automatically, rather than using `glMaterial()` to change parameters explicitly.

  The following code fragment illustrates how to change ambient and diffuse material parameters at every polygon or at every vertex:

      glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
      glEnable(GL_COLOR_MATERIAL);
      /* Draw triangles: */
      glBegin(GL_TRIANGLES);
      /* Set ambient and diffuse material parameters: */
      glColor4f(red, green, blue, alpha);
      glVertex3fv(...);glVertex3fv(...);glVertex3fv(...);
      glColor4f(red, green, blue, alpha);
      glVertex3fv(...);glVertex3fv(...);glVertex3fv(...);
      ...
      glEnd();

- Disable two-sided lighting unless your application requires it.

  Two-sided lighting illuminates both sides of a polygon. This is much faster than the alternative of drawing polygons twice. However, using two-sided lighting is significantly slower than one-sided lighting for a single rendering object.

- Disable GL_NORMALIZE.

  If possible, provide unit-length normals and don't call `glScale*()` to avoid the overhead of GL_NORMALIZE. On some OpenGL implementations it may be faster to simply rescale the normal, instead of renormalizing it, when the modelview matrix contains a uniform scale matrix.

- Avoid scaling operations if possible.

- Avoid changing the GL_SHININESS material parameter if possible. Setting a new GL_SHININESS value requires significant computation each time.

### Choosing Modes Wisely

OpenGL offers many features that create sophisticated effects with excellent performance. For each feature, consider the trade-off between effects, performance, and quality.

- Turn off features when they are not required.

  Once a feature has been turned on, it can slow the transform rate even when it has no visible effect.

  For example, the use of fog can slow the transform rate of polygons even when the polygons are too close to show fog, and even when the fog density is set to zero. For these conditions, turn off fog explicitly with

      glDisable(GL_FOG)

- Minimize expensive mode changes and sort operations by the most expensive mode. Specifically, consider these tips:

  - Use small numbers of texture maps to avoid the cost of switching between textures. If you have many small textures, consider combining them into a single larger, tiled texture. Rather than switching to a new texture before drawing a textured polygon, choose texture coordinates that select the appropriate small texture tile within the large texture.

  - Avoid changing the projection matrix or changing `glDepthRange()` parameters.

  - When fog is enabled, avoid changing fog parameters.

  - Turn fog off for rendering with a different projection (for example, orthographic) and turn it back on when returning to the normal projection.

- Use flat shading whenever possible. This reduces the number of lighting computations from one per vertex to one per primitive, and also reduces the amount of data that must be passed from the CPU through the graphics pipeline for each primitive. This is particularly important for high-performance line drawing.

- Beware of excessive mode changes, even mode changes considered cheap, such as changes to shade model, depth buffering, and blending function.

### Advanced Transform-Limited Tuning Techniques

This section describes advanced techniques for tuning transform-limited drawing. Follow these guidelines to draw objects with complex surface characteristics:

- Use textures to replace complex geometry.

  Textured polygons can be significantly slower than their non-textured counterparts. However, texture can be used instead of extra polygons to add detail to a geometric object. This can greatly simplify geometry, resulting in a net speed increase and an improved picture, as long as it does not cause the program to become fill limited. Texturing performance varies across the product line, so this technique might not be equally effective on all systems. Experimentation is usually necessary.

- Use `glAlphaFunc()` in conjunction with one or more textures to give the effect of rather complex geometry on a single polygon.

  Consider drawing an image of a complex object by texturing it onto a single polygon. Set alpha values to zero in the texture outside the image of the object. (The edges of the object can be antialiased by using alpha values between zero and one.) Orient the polygon to face the viewer. To prevent pixels with zero alpha values in the textured polygon from being drawn, call

      glAlphaFunc(GL_NOTEQUAL, 0.0)

  This effect is often used to create objects like trees that have complex edges or many holes through which the background should be visible (or both).

- Eliminate objects or polygons that will be out of sight or too small.

- Use fog to increase visual detail without drawing small background objects.

- Use culling on a separate processor to eliminate objects or polygons that will be out of sight or too small to see.

- Use occlusion culling: draw large objects that are in front first, then read back the depth buffer and use it to avoid drawing objects that are hidden.

## Tuning the Raster Subsystem

In the raster system, per-pixel and per-fragment operations take place. The operations include writing color values into the framebuffer or more complex operations like depth buffering, alpha blending, and texture mapping.

An explosion of both data and operations is required to rasterize a polygon as individual pixels. Typically, the operations include depth comparison, Gouraud shading, color blending, logical operations, texture mapping, and possibly antialiasing. This section discusses the following techniques for tuning fill-limited drawing:

- “Using Backface/Frontface Removal”

- “Minimizing Per-Pixel Calculations”

- “Using Clear Operations”

- “Optimizing Texture Mapping”

### Using Backface/Frontface Removal

To reduce fill-limited drawing, use backface and frontface removal. For example, if you are drawing a sphere, half of its polygons are backfacing at any given time. Backface and frontface removal is done after transformation calculations but before per-fragment operations. This means that backface removal may make transform-limited polygons somewhat slower, but make fill-limited polygons significantly faster. You can turn on backface removal when you are drawing an object with many backfacing polygons, then turn it off again when drawing is completed.

### Minimizing Per-Pixel Calculations

One way to improve fill-limited drawing is to reduce the work required to render fragments. This section discusses several ways you can do this:

- “Avoiding Unnecessary Per-Fragment Operations”

- “Using Expensive Per-Fragment Operations Efficiently”

- “Using Depth-Buffering Efficiently”

- “Balancing Polygon Size and Pixel Operations”

- “Other Considerations”

#### Avoiding Unnecessary Per-Fragment Operations

Turn off per-fragment operations for objects that do not require them, and structure the drawing process to minimize their use without causing excessive toggling of modes.

For example, if you are using alpha blending to draw some partially transparent objects, make sure that you disable blending when drawing the opaque objects. Also, if you enable alpha testing to render textures with holes through which the background can be seen, be sure to disable alpha testing when rendering textures or objects with no holes. It also helps to sort primitives so that primitives that require alpha blending or alpha testing to be enabled are drawn at the same time. Finally, you may find it faster to render polygons such as terrain data in back-to-front order.

#### Organizing Drawing to Minimize Computation

Organizing drawing to minimize per-pixel computation can significantly enhance performance. For example, to minimize depth buffer requirements, disable depth buffering when drawing large background polygons, then draw more complex depth-buffered objects.

#### Using Expensive Per-Fragment Operations Efficiently

Use expensive per-fragment operations with care. Per-fragment operations, in rough order of increasing cost (with flat-shading being the least expensive and multisampling the most expensive) are as follows:

1. flat-shading

2. Gouraud shading

3. depth buffering

4. alpha blending

5. texturing

6. multisampling

| Note: The actual order depends on the system you are running on.

Each operation can independently slow down the pixel fill rate of a polygon, although depth buffering can help reduce the cost of alpha blending or multisampling for hidden polygons.

Some of this information depends on the particular system the program is running on:

- Texturing is less expensive than alpha blending on new-generation hardware only.

- Alpha blending is less expensive than depth buffering on Indy systems.

- Beware of fill operations that are executed on the host for your graphics platform (for example, texturing on Extreme or Elan graphics).

#### Using Depth-Buffering Efficiently

Any rendering operation can become fill limited for large polygons. Clever structuring of drawing can eliminate the need for certain fill operations. For example, if large backgrounds are drawn first, they do not need to be depth buffered. It is better to disable depth buffering for the backgrounds and then enable it for other objects where it is needed.

For example, flight simulators use this technique. Depth buffering is disabled and the sky and ground, then the polygons lying flat on the ground (runway and grid) are drawn without suffering a performance penalty. Then depth buffering is enabled for drawing the mountains and airplanes.

There are other special cases in which depth buffering might not be required. For example, terrain, ocean waves, and 3D function plots are often represented as height fields (X-Y grids with one height value at each lattice point). It is straightforward to draw height fields in back-to-front order by determining which edge of the field is furthest away from the viewer, then drawing strips of triangles or quadrilaterals parallel to that starting edge and working forward. The entire height field can be drawn without depth testing provided it doesn't intersect any piece of previously-drawn geometry. Depth values need not be written at all, unless subsequently-drawn depth buffered geometry might intersect the height field; in that case, depth values for the height field should be written, but the depth test can be avoided by calling

    glDepthFunc(GL_ALWAYS)

#### Balancing Polygon Size and Pixel Operations

The pipeline is generally optimized for polygons that are 10 pixels on a side. However, you may need to work with polygons larger or smaller than that depending on the other operations going on in the pipeline:

- If the polygons are too large for the fill rate to keep up with the rest of the pipeline, the application is fill-rate limited. Smaller polygons balance the pipeline and increase the polygon rate.

- If the polygons are too small for the rest of the pipeline to keep up with filling, then the application is transform limited. Larger and fewer polygons, or fewer vertices, balance the pipeline and increase the fill rate.

If you are drawing very large polygons such as backgrounds, performance will improve if you use simple fill algorithms. For example, don't set `glShadeModel()` to GL_SMOOTH if smooth shading is not required. Also, disable per-fragment operations such as depth buffering, if possible. If you need to texture the background polygons, consider using GL_REPLACE as the texture environment.

#### Other Considerations

- Use alpha blending with discretion.

  Alpha blending is an expensive operation. A common use of alpha blending is for transparency, where the alpha value denotes the opacity of the object. For fully opaque objects, disable alpha blending with `glDisable(GL_BLEND)`.

- Avoid unnecessary per-fragment operations.

  Turn off per-fragment operations for objects that do not require them, and structure the drawing process to minimize their use without causing excessive toggling of modes.

### Using Clear Operations

When considering clear operations, consider these points:

- If possible, avoid clear operations. For example, you can avoid clearing the depth buffer by setting the depth test to GL_ALWAYS.

- Avoid clearing the color and depth buffers independently.

  The most basic per-frame operations are clearing the color and depth buffers. On some systems, there are optimizations for common special cases of these operations.

  Whenever you need to clear both the color and depth buffers, don't clear each buffer independently. Instead call:

      glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

- Be sure to disable dithering before clearing.

### Optimizing Texture Mapping

Follow these guidelines when rendering textured objects:

- Avoid frequent switching between texture maps. If you have many small textures, consider combining them into a single larger, mosaic texture. Rather than switching to a new texture before drawing a textured polygon, choose texture coordinates that select the appropriate small texture tile within the large texture.

- Use texture objects to encapsulate texture data. Place all the `glTexImage*()` calls (including mipmaps) required to completely specify a texture and the associated `glTexParameter*()` calls (which set texture properties) into a texture object and bind this texture object to the rendering context. This allows the implementation to compile the texture into a format that is optimal for rendering and, if the system accelerates texturing, to efficiently manage textures on the graphics adapter.

- When using texture objects, call `glAreTexturesResident()` to make sure that all texture objects are resident during rendering. (On systems where texturing is done on the host, `glAreTexturesResident()` always returns GL_TRUE.) If necessary, reduce the size or internal format resolution of your textures until they all fit into memory. If such a reduction creates intolerably fuzzy textured objects, you may give some textures lower priority.

- If possible, use `glTexSubImage*D()` to replace all or part of an existing texture image rather than the more costly operations of deleting and creating an entire new image.

- Avoid expensive texture filter modes. On some systems, trilinear filtering is much more expensive than nearest or linear filtering.

## Tuning the Imaging Pipeline

This section briefly lists some ways in which you can improve pixel processing. Example 15-1 provides a code fragment that shows how to set the OpenGL state so that subsequent calls to `glDrawPixels()` or `glCopyPixels()` will be fast.

To improve performance in the imaging pipeline, follow these guidelines:

- Disable all per-fragment operations.

- Define images in the native hardware format so type conversion is not necessary.

- For texture download operations, match the internal format of the texture with that on the host.

- Byte-sized components, particularly unsigned byte components, are fast. Use pixel formats where each of the components (red, green, blue, alpha, luminance, or intensity) is 8 bits long.

- Use fewer components, for example, use GL_LUMINANCE_ALPHA or GL_LUMINANCE.

- Use color matrix and color mask to store four luminance values in the RGBA framebuffer. Use color matrix and color mask to work with one component at a time If one component is being processed, convolution is much more efficient. Then process all four images in parallel. Processing four images together is usually faster than processing them individually as single-component images.

  The following code fragment uses the green component as the data source and writes the result of the operation into some (possibly all) of the other components:

      /* Matrix is in column major order */
      GLfloat smearGreenMat[16] = {
          0, 0, 0, 0,
          1, 1, 1, 1,
          0, 0, 0, 0,
          0, 0, 0, 0,
      };
      /* The variables update R/G/B/A indicate whether the
      * corresponding component would be updated.
      */
      GLboolean updateR, updateG, updateB, updateA;

      ...

      /* Check for availability of the color matrix extension */

      /* Set proper color matrix and mask */
      glMatrixMode(GL_COLOR);
      glLoadMatrixf(smearGreenMat);
      glColorMask(updateR, updateG, updateB, updateA);

      /* Perform the imaging operation */
      glEnable(GL_SEPARABLE_2D_EXT);
      glCopyTexSubImage2DEXT(...);
      /* Restore an identity color matrix.  Not needed when the same
      * smear operation is to used over and over
      */
      glLoadIdentity();

      /* Restore previous matrix mode (assuming it is modelview) */
      glMatrixMode(GL_MODELVIEW);
      ...

- Load the identity matrix into the color matrix to turn the color matrix off.

  When using the color matrix to broadcast one component into all others, avoid manipulating the color matrix with transformation calls such as `glRotate()`. Instead, load the matrix explicitly using `glLoadMatrix().`

- Know where the bottleneck is.

  Similar to polygon drawing, there can be a pixel-drawing bottleneck due to overload in host bandwidth, processing, or rasterizing. When all modes are off, the path is most likely limited by host bandwidth, and a wise choice of host pixel format and type pays off tremendously. This is also why byte components are sometimes faster. For example, use packed pixel format GL_RGB5_A1 to load texture with an GL_RGB5_A1 internal format.

  When either many processing modes or a several expensive modes such as convolution are on, the processing stage is the bottleneck. Such cases benefit from one-component processing, which is much faster than multicomponent processing.

  Zooming up pixels may create a raster bottleneck.

- A big pixel rectangle has a higher throughput (that is, pixels per second) than a small rectangle. Because the imaging pipeline is tuned to trade off a relatively large setup time with a high pixel transfer efficiency, a large rectangle amortizes the setup cost over many pixels, resulting in higher throughput.

- Having no mode changes between pixel operations results in higher throughput. New high-end hardware detects pixel mode changes between pixel operations: When there is no mode change between pixel operations, the setup operation is drastically reduced. This is done to optimize for image tiling where an image is painted on the screen by drawing many small tiles.

- On most systems, `glCopyPixels()` is faster than `glDrawPixels()`.

- Tightly packing data in memory (for example row length=0, alignment=1) is slightly more efficient for host transfer.

Prev | Table of Contents | Next
Chapter 13. Tuning Graphics Applications: Fundamentals |  | Chapter 15. Tuning Graphics Applications: Examples