One of the most radical changes that sets UltimateVision graphics apart from any of its predecessors in the Onyx line is the ability to tailor operations of the transform engine and/or the pixel processing engine. In the previous architectures, the graphics pipeline provided fixed functionality that was determined during the chip design phase and frozen in microcode, which was fairly immutable. The old design allowed for multiple configurations selectable by setting OpenGL state. For example, the raster engine could be instructed to choose among several available blending operations or combine multiple textures in one of several possible ways. However, the number of these features was relatively small and if the feature was absent (for example, an esoteric blending function), there was little that could be done (short of implementing it with multipass algorithms, which substantially reduce performance).
An UltimateVision GPU, on the other hand, permits the application developer to extend the standard GPU functionality by writing custom programs which leverage the chip's circuitry in an arbitrary fashion. This allows the programmer to add new functionality (for example, new blending operation) or to implement an enhanced version of an existing operation. For example, see the section “Texture Lookup Tables (TLUTs)” in Chapter 6 for a description on how to implement commonly desirable functionality that was not provided by the designers of the new chip but can be easily engineered using custom programs. In essence, with custom routines the application programmer is limited by the hardware capabilities of the chip but not by the fact that some functionality was not provided at a high-level interface (for example, OpenGL). Complex programs can be created to achieve desirable visual effects like warping of the original geometry, bump mapping, bi-cubic texture filtering, ARB imaging extensions, and life-like cinematic rendering.
An UltimateVision GPU can be programmed at the two different stages of the graphics pipeline:
The geometry engine, which applies transformations and lighting calculations to vertex data
The rendering engine, which operates on rasterized pixels
The associated programs are referred to as vertex and fragment programs, respectively.
In their most basic form, vertex programs and fragment programs are specialized assembly code. Instructions are provided to perform arithmetic and logic operations on the respective engine's ALU (for example, MAD for Multiply and Add, DP3 for Dot Product Vec3) or to sample textures (for example, TEX to fetch a suitably interpolated texture value into a register, LRP to interpolate between two values). Once created, such special purpose programs can be loaded into the GPU circuitry using OpenGL. The programmed behavior will then be applied to any geometry sent down the graphics pipe instead of the standard fixed pipeline operations. Multiple programs can be compiled, loaded into the GPU, and bound when needed.
The obvious downside to writing long, complex programs is that they do impact performance. The more instructions in the vertex program, the lower the transform rate (that is, fewer triangles per seconds). Long vertex programs will negatively affect the transform rate.
The Programmable Transform & Light (T&L) unit allows the application developer to take full control over vertex processing. As each set of vertex data (coordinates, colors, texture coordinates, and the like) passes through the unit, an arbitrary set of manipulations can be applied to it. For example, a simple flat quad-mesh can have its height distorted with a vertex program to contain dynamic sinusoidal “ripples” that simulate wave propagation. In this way, compute-intensive simulations can be offloaded from the CPU by leveraging the heavily pipelined GPU design to get higher overall performance (the host sends the same flat mesh for each frame; all distorting is done by the GPU). Similarly, custom versions of glTexGen*() or mesh generation functionality can be implemented by inserting computed values of texture coordinates to reduce bandwidth requirements (the CPU only needs to send the control vertex coordinates).
The following are typical applications of vertex programs:
The following is a sample of a simple vertex program that explicitly transforms each incoming vertex coordinate from the model to screen space and passes through the vertex color unchanged:
!!ARBvp1.0
# Constant Parameters
PARAM mvp[4] = { state.matrix.mvp }; # modelview + proj
# Per-vertex inputs
ATTRIB inPosition = vertex.position;
ATTRIB inColor = vertex.color;
# Per-vertex outputs
OUTPUT outPosition = result.position;
OUTPUT outColor = result.color;
# Transform the vertex component by component
DP4 outPosition.x, mvp[0], inPosition;
DP4 outPosition.y, mvp[1], inPosition;
DP4 outPosition.z, mvp[2], inPosition;
DP4 outPosition.w, mvp[3], inPosition;
# Use the per-vertex color specified
MOV outColor, inColor;
END
|
The resulting text of the program is stored as a string (an array of Glubytes) and subsequently loaded into the geometry engine:
glGenProgramsARB(1, &vertexProgram); glBindProgramsARB(GL_VERTEX_PROGRAM_ARB, vertexProgram); glProgramStringARB(GL_VERTEX_PROGRAM_ARB, GL_PROGRAM_ASCII_ARB strlen(programString), programString); /* check error output */ glEnable(GL_VERTEX_PROGRAM_ARB); |
The maximum length of a vertex program on UltimateVision systems is 64K instructions, but keep in mind that vertex program execution speed is inversely related to the length of the program. Thus, keep the programs short. Fragment programs and vertex programs are limited to the number of hardware registers provided by the chip designers and temporary storage cannot be virtualized; therefore, effective storage allocation is of critical importance here.
Once the vertices are transformed and lit by the T&L unit, the primitives they represent are rasterized and the resulting pixels together with their attributes (color, depth, texture coordinates, and the like) are passed along to the fragment program as “fragments.” A fragment program takes its input (color and texture coordinates, for example) and produces a single RGBA value as output to be inserted into the framebuffer and other render targets. In a simple case, the color will be simply blended with the textures and will replace the previous value in the framebuffer, but programmability of the unit allows for arbitrarily complex operations to be applied instead. For example, having offset the texture coordinates somewhat to produce a simple motion blur effect, a fragment program can apply the same texture several times.
The following are typical applications of fragment programs:
Fragment programs also allow coarser models to be used compared with older architectures. The programs can do so because the lighting effects can be interpolated across large stretches of primitives without introducing lighting artifacts.
The following code sample implements a very simple fragment program that modulates the incoming color with a texture:
!!ARBfp1.0
# Simple program to show how to code the default texture environment
ATTRIB tex = fragment.texcoord; #first set of texture coordinates
ATTRIB col = fragment.color.primary; #interpolated color
PARAM zero = {0, 0, 0, 0};
OUTPUT outColor = result.color;
TEMP tmp;
TEMP tmp2;
TEMP tmp3;
TXP tmp, tex, texture, 2D; #sample the texture
TXP tmp2, tex, texture, 2D; #sample the texture again
ADD tmp3, tmp, tmp2; #add together = tex *two
MUL outColor, tmp3, col; #perform the modulation
END
|
The following code shows how the program is loaded into the fragment engine:
glGenProgramsARB(1, &fragmentProgram); glBindProgramsARB(GL_FRAGMENT_PROGRAM_ARB, vertexProgram); glProgramStringARB(GL_FRAGMENT_PROGRAM_ARB, GL_PROGRAM_ASCII_ARB strlen(programString), programString); /* check error output */ glEnable(GL_FRAGMENT_PROGRAM_ARB); |
The maximum length of a fragment program on UltimateVision systems is 64 instructions but, just as with vertex programs, performance (specifically, fill rate) generally will be inversely related to the length of the program.