GPU Gems

GPU Gems is now available, right here, online. You can purchase a beautifully printed version of this book, and others in the series, at a 30% discount courtesy of InformIT and Addison-Wesley.

Please visit our Recent Documents page to see all the latest whitepapers and conference presentations that can help you with your projects.



Chapter 30. The Design of FX Composer

Christopher Maughan
NVIDIA

FX Composer is a tool designed to help developers and artists create Direct3D effects. These effects are stored in .fx files, which contain complete information on how to apply a shader to a given 3D model. FX Composer is essentially an IDE, with a look and feel that is similar to Microsoft's Visual Studio .NET application. In developing FX Composer, we wanted to enable people to interactively edit effects in a friendly environment, and we wanted to build a solid foundation for future expansion into areas such as shader debugging and profiling.

This chapter describes the design of the FX Composer tool and explains the reasons behind the decisions we made along the way. By discussing the problems we faced and the approaches we took to fix them, we hope to give you insight into the complexities of developing a large Direct3D application. Note: The Direct3D 9 SDK help documentation and other supporting materials cover .fx files in detail and explain how to use them in 3D applications. For that reason, this chapter does not explore the specifics of using .fx files.

30.1 Tools Development

The primary job of a tools developer is to give end users an application they can use in a productive way. Thus, the design and evolution of a tool can be quite different from that of a typical game, where the main focus usually is developing a fast engine with a fixed feature set. Tools development is interesting because it allows the tools developer to engineer software without that restrictive mantra "must run at x FPS." In a tool program, functionality and stability take precedence over speed of execution. As such, the primary focus of FX Composer development was to create a useful tool that could be extended to keep up with future Direct3D API changes, with a clean, public interface for future expansion.

30.2 Initial Features and Target Audience

When we began the FX Composer project, these were our major design goals:

  1. A familiar IDE, with a similar look and feel to .NET.
  2. An extensible engine.
  3. A stable environment for creating .fx files and visualizing the results.

The target audience for FX Composer is software developers and technical artists who have some level of programming knowledge. With that in mind, the tool would need to combine .fx file-editing capabilities and a user interface for editing shader properties. To facilitate this functionality, the .NET-like IDE would allow users to add features that could be docked conveniently into window panels and then hidden when necessary. At the outset, the following application features were planned:

What follows is a description of the FX Composer application and engine as they stand at the time of writing.

30.3 Object Design

FX Composer is built almost entirely of interfaces and objects. The model very closely follows the principles of Microsoft's COM specification, with a core runtime implementing the object creation and registration features. Such an interface-based approach has several advantages, and it makes for a very clean design, as we show later. One key result of our design is a clean plug-in model, which pervades the entire FX Composer application. Almost every component in the application is a plug-in, enabling easy integration of new features and externally authored extensions.

A simplified interface from the SDK is shown in Listing 30-1.

Example 30-1. A Sample Interface

class INVImportScene : public INVObject
{
public:
  virtual bool ImportScene(INVScenePtr& pScene,
                           const char* pszFileName) = 0;
  virtual bool GetFileExtension(unsigned int i,
                            const char** pszName,
                            const char** pszExtension) const = 0;
  virtual unsigned int GetNumFileExtensions() const = 0;
};

Any objects in the system that wish to implement INVImportScene can do so by inheriting the pure abstract base class and implementing the methods. All engine interfaces are pure abstract, and all inherit from the base interface, INVObject. INVObject has three methods familiar to any COM programmer: AddRef, Release, and QueryInterface. These enable lifetime control through reference counting, and they allow any instantiated object in the system to be asked if it implements a particular interface. Thus, any object in the system must at a minimum implement the INVObject interface, so that it can always be queried for pointers to other interfaces, and reference-counted.

A typical engine object, implementing the INVImportScene interface, would do so as shown in Listing 30-2.

The additional function CreateNVObject is typically declared using a macro, and it is called by the runtime to create a new instance of this object and return a pointer to the requested interface, if supported.

All plug-ins—in our case, dynamic link libraries (DLLs), are required to export two functions: RegisterNVObjects and UnRegisterNVObjects. At startup, the runtime will load the plug-ins and ask them to register each object type that can be created. As with COM, each object and each interface have globally unique identifiers (GUIDs).

Example 30-2. The XFileImporter Object

class XFileImporter : public INVImportScene
{
public:
  // Implement INVImportScene methods
  . . .

  // Declare INVObject (addref/release/queryinterface)
  . . .

  // A static creation function
  
   static bool CreateNVObject(INVCreatorPtr,
                             NVGUID& Interface,
                             void** ppObject);
};

Essentially, every object in the FX Composer framework is a plug-in—from complex objects such as file importers to simple ones such as string containers. Loadable DLLs contain any number of such objects, each implementing any number of interfaces. Additionally, all objects have human-readable strings and an optional category, which groups together objects of a particular type, enabling them to be found easily. An object factory implementing the INVCreator interface is declared once for each object type; it can be used to manufacture registered objects as needed. The FX Composer system is an example of a pluggable class factory.

Here is an example. All the shapes you can create in FX Composer are in the category geopipeobject_shape. The following code shows how FX Composer finds the list of shape plug-ins available:

INVCreatorArrayPtr pCreators = GetSYSInterface()->
                          GetCreatorsInCategory("geopipeobject_shape");

The call to GetSYSInterface is global and can be done anywhere to return a pointer to the nv_sys runtime. The returned array of object creators can then be used to create shapes for display in the materials and rendering viewports. It would therefore be a fairly simple process to create a plug-in object capable of generating a unique shape, and making it available for immediate use inside FX Composer. Further, this shape can be saved to the project file and reloaded with all of its associated parameters.

Discussion of the technical implementation of these components is beyond the scope of this chapter, but it boils down to a few simple macros that declare the objects and their interfaces. We took the useful parts of the COM specification and avoided the more complex features, such as aggregation and component registration through the registry. To simplify usage further, we used smart pointers to remove the necessity of calling AddRef/Release, and to make interface querying automatic. For example, the following code fragment checks to see if a scene importer object supports the INVProperties interface:

INVSceneImporterPtr pImporter = GetImporter();
INVPropertiesPtr pProps = pImporter;
if (pProps)
{
  . . .
}

The assignment from the importer pointer to the properties pointer will call an implicit QueryInterface on the importer object, and it will return a valid pointer if successful. All additional references are cleaned up by the reference-counting properties of the smart pointer (signified by the Ptr suffix on the interface name). Every interface in FX Composer is declared with an equivalent smart pointer, and they are rarely referenced without one.

30.3.1 Benefits of the Interface Approach

Not everyone favors an interface-based/COM approach. For FX Composer, it has turned out to be a great feature, one that has made the underlying implementation clean and easy. Using interface-based programming, we have managed to expose FX Composer objects in a very accessible fashion, making extensions and new features easy to add.

The following are some of the advantages of our approach.

Object Querying

In FX Composer, any object can support any interface, giving great extensibility. A good example of this is the IDE's Properties panel. When a material is selected, an "object selection" message arrives, which contains an INVObjectPtr. The Properties panel can now cast it to an INVPropertiesPtr, and if successful, it can walk through a list of property values using this standard interface and enable them to be edited in the GUI. This works for any object that chooses to expose its properties to the system. For example, when we added the shape plug-ins, it was a trivial matter to have them expose their dimensions, tessellation, and so on. When these shapes are loaded and selected, the Properties panel allows the user to change their values. For this reason, many objects in the FX Composer system support the properties interface, and they give useful information in the user interface as a result.

This feature has paid off time and again. When we added the ability to save the scene to an XML file, it was simple to have serialized objects support an INVXMLStreamable interface that could be used to stream the object out to an XML-formatted file. When we wanted to clone an object, we added an INVClone interface, giving objects control over how they were duplicated.

Future ideas for interfaces include an INVScript interface, enabling system objects to expose their scripting functions.

Object Lifetime

Because all components are reference-counted and smart pointers typically point to them, we have had very few memory leaks—they usually end up in pieces of external code. Of course, there are disadvantages to reference counting: there is always a fixed overhead in passing around interface pointers; it can sometimes be difficult to make an object go away when you want it to; and circular references can often be an issue—such as when two objects hold references to each other. When we came across these issues, we worked around them by making "weak" pointers on occasion—that is, holding pointers to objects that were not "smart" and did not have a reference. This approach does feel a little like a workaround, but in practice, it is not often needed. The additional overhead was not a consideration for this tool, as mentioned earlier.

Plug-In Management

Plug-ins in FX Composer essentially come for free with the object model we are using. Each object registers itself with the system and can be used by any interested party at a later time. Creating a new plug-in is as simple as declaring a new interface, assigning an associated category, and writing simple code to load the objects in that category and use them. The core runtime takes care of tracking objects, and in fact it always has an internal list of every instantiated object in the system, as well as all the loaded plug-in modules. This can be a useful debug feature, because it's possible to analyze the runtime system at any time. An additional debugging dialog in FX Composer presents a tree view of the current system objects, which has been used on many occasions to analyze behavior and track down bugs.

Our approach has one drawback: it does require a little understanding to use it well, especially if a developer is not familiar with this kind of programming. To make this adjustment process easier, we added several macros to make the creation of objects and interfaces as simple as possible; we also included a heavily documented sample plug-in.

Connection Parameters

Right from the start, we decided that FX Composer should handle the animation of scenes, skinned characters, and parameters in materials. To that end, we designed a connection parameter type, closely matched to D3DX effect parameter types, with full support for animation. The result is that just about any parameter in FX Composer can be animated with time keys and interpolators of various types, to generate intermediate values between two separate keys. For example, it's possible to apply an effect to a skinned mesh and then animate a value such as "bump height" in the effect. The character will animate, and the effect will change in real time. Connection parameters have several additional properties, such as semantics, annotations, and names, just as in .fx files, making them a useful generic holder for any item of data in an object.

The connection parameters FX Composer supports have been useful in several ways:

Message Senders and Receivers

With a complex system such as the FX Composer engine, good communication between different components is essential. We took a two-pronged approach to this problem. At first, we developed a global message-broadcasting system that allowed any object to send messages to all interested parties in a "fire-and-forget" fashion. The NewScene message is a good example of this, where many components may wish to know that a new scene has been created.

This scheme worked well at first, but it quickly became bloated due to the generic nature of the broadcast. To enhance the system, an additional messaging protocol was developed that enabled a close link between two interested parties. In the system, any object can implement the INVMessageReceiver interface, and any object can implement the INVMessageSender interface. Interested parties can then register themselves with objects and find out about events they wish to know. The usual issues with such a scheme come up, and it is essential to carefully maintain both ends of the communication link. Care must be taken so that the participants do not get out of sync and find themselves with references to objects that either no longer exist or no longer matter to them. The message sender/receiver system is an example of the "Publish/Subscribe" pattern described in the book Design Patterns (Gamma et al. 1995).

30.4 File Format

We decided that FX Composer needed its own file format in order to save and restore the current scene, the material palette, the file paths, and optionally the current media set. This file would hold the entire workspace and allow the user to package textures, .fx files, and other items into one file—a bonus for our Developer Relations group, who often want to send self-contained examples to developers. The file extension of this workspace format is ".fxcomposer".

The first file format we considered was one consisting entirely of XML data, but this was rejected due to the large amount of binary data in a given scene; such data does not encode well into XML. Instead, we implemented a hybrid approach that offered the best of both worlds. The FX Composer project file is a compound format, in which all the data is actually stored inside a single .zip file (albeit with the .fxcomposer extension). The .zip file contains an XML scene description with some references to external binary data, in the form of offsets into a binary chunk. The binary chunk is also encoded in the same .zip file. To further simplify the design, objects in the system can support an XML streaming interface, and they are capable of writing out their data in XML format. The interface they use to do this also allows them to add data to the zipped binary file, returning an offset for later retrieval. Saving the entire workspace is simply a matter of walking the object hierarchy and building the XML for each object. Reloading is almost as simple: each object reloads itself based on the XML data. As a further abstraction, each element in the XML has an ObjectID, which can be retrieved from the end of the file. In this way, FX Composer can check ahead of time that the necessary plug-ins are loaded and that it can support the objects being streamed from the workspace file. This step protects against such problems as saving out a scene containing a "teapot shape" plug-in, for example, and trying to reload it on a version of FX Composer without that plug-in installed. In such a case, the program can inform the user that a plug-in is missing and then try to load the rest of the scene.

An interesting side effect of the new file format is its efficiency. Because common data is compressed into a binary chunk without the interleaved scene data, the file sizes have turned out to be quite compact, resulting in storage requirements smaller than the original mesh file when compressed. This compactness is impressive, given that the .fxcomposer file contains not only the mesh, but also the materials in the palette. In the case of an imported .x file, the gains are the greatest when compared to our internally developed .nvb file format, and the saved file actually includes more mesh data in the form of tangent and binormal vectors.

Table 30-1 shows example file sizes in bytes, for an .nvb and an .x imported file, saved as .fxcomposer files. The savings shown represent the percentage savings over the original file in compressed form (.zip file). Note that the compressed original files would not usually be stored on the user's hard drive, so the savings in this case would be greater.

Table 30-1. File Sizes Compared

File Name

File Size

Original File Size

Original File Size, Compressed

Savings

Gooch_alien.fxcomposer

906,486

1,561,000 (.nvb)

968,696

62,210 (6%)

Tiny.fxcomposer

211,819

1,533,978 (.x)

338,252

126,433 (62%)

We have also done some experimentation with XSLT transforms. This declarative language enables the display of XML files by transforming the data into a human-readable Web page. In the future, this will enable us to make our project files self-describing, by packaging the .xslt file with the .fxcomposer project. Adding an XML schema is another interesting option, which would enable validation of our XML format, again enclosed within the compound .zip file.

30.5 User Interface

Typically, a tools project involves a great deal of user-interface work. Such work can be tedious and, if not done well, can often lead to incomplete tools with difficult-to-use interfaces. The outcome often depends on the windows-programming experience of the developer. We knew at the start that FX Composer would need a complex UI, and we wanted to support floating and docking windows to save space and to give users a familiar development environment.

We considered writing our own MFC code, but such a complex task requires a dedicated project of its own. Another possibility was to write an extension to VC.NET, but we weren't absolutely sure that the different requirements of a real-time 3D app would be met by the .NET IDE—and we would have had to climb a steep learning curve just to answer that preliminary question.

In the end, BCGSoft (www.bcgsoft.com) came to our rescue with its BCGControlBar Professional MFC extension library. This robust component implements many of the features found in advanced IDEs such as Visual Studio .NET. It can handle floating and docking windows, and as a bonus, it comes with an editor supporting bookmarks, undo/redo, and even IntelliSense. So far, we've been really pleased with the results. BCGControlBar is so big, in fact, that we have not turned on all the available features in the toolkit—we want to contain the complexity until we need it. For example, the SDK supports a fully customizable menu and hot-key layout, which users can modify to create a look and feel that suits them, just as in .NET.

A side effect of using such a complex toolkit, aside from the additional application complexity, is the testing burden. With so many windows arranged in different ways—and with each one potentially open, closed, or hidden at any time, including at application startup—a thorough testing methodology is needed to ensure that all combinations of layout will work. We are approaching this problem in several ways, through quality assurance, manual application modification to uncover potential problems, and automated tests. One plan we wish to implement is a unit-testing framework, at the object level. With a simple interface, such as INVUnitTest, it should be possible to have components of the system run automated tests in a clean way. FX Composer can then periodically test all the objects in the system for integrity on debug builds, or in response to a key press.

30.6 Direct3D Graphics Implementation

FX Composer is essentially a graphics application and, with its multiple device windows, a fairly complex one. It is also responsible for managing a list of .fx files and all associated media. At any one time on the current system, up to four Direct3D devices may be employed: the Media panel, the Textures panel, the Render panel, and the Shader Perf bar—a feature that enables the disassembly of .fx files and the display of profiling information for NVIDIA GPUs.

30.6.1 Device Windows

Understanding the need to simplify the implementation of the separate components, we decided to use unique Direct3D devices for each panel in the application. Although this approach may seem resource hungry, it does allow users to "tear off"any single panel in the application and run it full screen on a second monitor, for example. It also makes it easy to run the reference rasterizer alongside hardware-accelerated device windows, for validation of effects or for shaders not supported on current hardware.

The cost of separate devices shows up in the Materials and Render window panels. These two windows might contain duplicates of textures from shared materials. For example, loading the DiffTex.fx file into the Materials panel will load the texture onto the Materials panel device. If that material is then applied to a mesh in the scene, the texture will also be instantiated on the Render window. On top of this, the texture will be created in the Textures panel if the material is selected. It may also appear in the Shader Perf panel when it loads the material to analyze the effect—though it is not strictly necessary to load the texture to do this, or to hold on to the material for any length of time. The Shader Perf panel and the Textures panel show only one material at a time, so this is not a major problem.

The Materials panel and the Render panel are therefore the main places where resources are potentially duplicated—in particular, textures. In practice, this has not been a problem, and it would take a rather large scene for it to become an issue. In addition, most textures, except for render targets, are managed; they are not necessarily instantiated on the device until they are used, and they can be paged out if necessary. In the future, we may investigate sharing textures across devices—Direct3D allows you to do this—or enabling a device to be shared across windows. This would add a certain level of inflexibility and complication to the design, which we currently see as unnecessary—especially because FX Composer is intended for developers and artists running development machines with reasonably modern hardware, containing plenty of video memory.

30.6.2 Direct3D Effects

FX Composer's main work is managing .fx files. D3DX provides a rich API for working with .fx files, which FX Composer uses to good effect. The key concept in D3DX is the ability to load an effect in two steps. Although that is not the only way to do it, FX Composer takes this approach.

First, FX Composer creates a material. We think of a material as an instantiation of an .fx file and associated parameters unique to that material. At any one time, we may have several materials created from the same .fx file, each of which has a different set of connection parameters. For example, assume that we have two different cartoon materials, both referencing a file named cartoon.fx. Each material may have a different color in the diffuse parameter of the .fx file, to give a different look to a cartoon character. This mimics the way an artist works in a typical CAD package, first selecting a material type, then tweaking the parameters to get the right look. The material at this point has no connection to any particular Direct3D device, and no matter how many windows reference it, there is only one object representing a material.

30.6.3 ID3DXEffectCompiler

Once we have the material, we load the .fx file using the ID3DXEffectCompiler interface. This enables us to compile the effect file and check it for errors without instantiating the effect on a device. The effect compiler creates a compact representation of the effect, which can be used later to create a device-specific effect. But there is a catch here: because we want one set of material parameters for the effect, we must store a collection of connection parameters for the .fx file. This becomes our master set of parameters, which all parts of the engine modify. We must do this because once we have added the effect to a particular device, a new set of parameters will be created for that device, and we need to map from our current set to the device set.

To simplify the rendering of material on a device, we added an API to our material interface, called ApplyToDevice. This API is called just before a material is used on a device window. If at that time the material has not been used on the window, the material will use the compiled effect from the ID3DXEffectCompiler and create an effect for the device. At this point, D3DX will convert the compact effect representation to an ID3DXEffect interface, which we can use to render the effect. Because we previously compiled the .fx file, this step is effectively faster than going straight to an ID3DXEffect from a .fx file; it also means that we compile once, no matter how many windows are rendering the effect.

30.6.4 ID3DXEffect

We now have a device-specific interface, with several additional APIs. Here we can look at the effect as it applies to the current device and check that it can render with the capabilities of that device. FX Composer will render the object in blue wireframe if the device capabilities required by the effect are not available; if the previous compile step failed, the result will be red wireframe. D3DX effects are split up into techniques, each of which may have different resource requirements. For this reason, we also keep a list of possible techniques for the current device, so that we can present them in the user interface and potentially pick a different one if the current selection is not available.

30.7 Scene Management

FX Composer contains its own scene graph, which is implemented in a fairly straightforward way. Additional importers enable files in the .nvb and .x file formats to be imported into the scene graph; in the future, we will likely offer more formats, depending on developer demand. The scene is essentially a "node soup," containing geometry, cameras, lights, and so on. Connecting the nodes are transform links, which enable the traversal of the scene graph. The system is flexible enough that in the future we could traverse the nodes in different ways, depending on need. For example, we might want to walk the scene from the point of view of a light, front to back. The current scene graph does not preclude this, because nodes and links are separate entities.

Nodes and node links also have the standard animated connection parameters, so they can be animated at different key frames, enabling scene animation effects, skinning, camera pans, and more.

30.7.1 Geometry Pipes

D3DX effects completely specify the interface from the application to the vertex shader input through semantics. Using an API called D3DXGetShaderInputSemantics, it is possible to find out the vertex buffer layout that an effect requires. Given this, it seemed appropriate that FX Composer could handle complex geometry streams and map them to the current .fx file. To facilitate doing so, FX Composer implements the concept of a geometry pipe node. A geometry pipe node consists of a number of geometry pipe objects, each of which can modify a geometry pipe bundle. The bundle is a collection of geometry data, with one stream object for each data type. A typical bundle might contain a position stream, a normal stream, and a texture coordinate stream. As the geometry flows up the pipeline, geometry pipe objects can modify the bundle by adding data, modifying it, or removing it. The resulting bundle at the top of the pipeline is then converted to a Direct3D vertex buffer, which can be made to match the effect file being used on that geometry pipe. To assist in the brokering between the geometry bundle and the .fx file input format, the geometry bundle streams have semantic and usage indices.

This approach allows plug-ins to be created to satisfy the current effect's usage scenario. For example, it should be possible to create a plug-in that adds "fins" to a fur shader. The author of the plug-in might also add a geometry stream to the bundle to indicate the presence of a fin, or a stream of triangles to render it.

A special kind of geometry pipe plug-in is always placed at the bottom of the stack. This plug-in is either a "shape" or a "mesh." It is the starting point for the geometry bundle before it flows up the stack. Shape plug-ins are very compact, containing simple information about how to build the shape. Mesh plug-ins are typically created by importers from the preloaded geometry data, though there is nothing to stop a plug-in author from creating a procedural mesh object, for example.

Skinning

One common problem with .fx files is how to handle skinning. FX Composer takes one of three approaches:

  1. If the current geometry bundle contains bones and weights but the assigned .fx file does not take them as inputs, FX Composer will pre-skin the data on the host before passing it to the effect.
  2. If the current .fx file has bone weights but the current geometry stream does not, FX Composer will set the bone weights to 1 and supply the current transform in the bone matrix palette—hence ensuring that a nonskinned character works on a skinned effect.
  3. If the current .fx file contains bone weights and the model also has the same number of bone weights, FX Composer will send the data directly to the effect, assuming that the current hardware is capable of handling the bone palette size.

30.8 Conclusion

With FX Composer, we hope to have built a tool that developers can use to create great effects for their apps. We have strived to design an application that will grow with future APIs and GPUs, and enable us to continue to offer a useful tool. The plug-in features will give developers the power to integrate their workflow with FX Composer and get their assets into it in a simple way.

We hope that this chapter has given you insight into our approach to writing this complex Direct3D tool and inspired some interesting approaches to implementations using .fx files. Direct3D effects provide powerful effect-authoring features, and tools such as FX Composer should help encourage their widespread use in 3D applications. With its rich IDE for building effects and its solid framework for future developer tools, we believe that FX Composer has achieved its goals.

30.9 References

Gamma, E., R. Helm, R. Johnson, and J. Vlissides. 1995. Design Patterns. Addison-Wesley.

Rogerson, Dale. 1997. Inside COM. Microsoft Press.

Tennison, Jeni. 2002. Beginning XSLT. Wrox Press.

Wyke, R. Allen, Sultan Rehman, and Brad Leupen. 2002. XML Programming. Microsoft Press.


Copyright

Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and Addison-Wesley was aware of a trademark claim, the designations have been printed with initial capital letters or in all capitals.

The authors and publisher have taken care in the preparation of this book, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein.

The publisher offers discounts on this book when ordered in quantity for bulk purchases and special sales. For more information, please contact:

      U.S. Corporate and Government Sales
      (800) 382-3419
      
corpsales@pearsontechgroup.com

For sales outside of the U.S., please contact:

      International Sales
      international@pearsoned.com

Visit Addison-Wesley on the Web: www.awprofessional.com

Library of Congress Control Number: 2004100582

GeForce™ and NVIDIA Quadro® are trademarks or registered trademarks of NVIDIA Corporation.
RenderMan® is a registered trademark of Pixar Animation Studios.
"Shadow Map Antialiasing" © 2003 NVIDIA Corporation and Pixar Animation Studios.
"Cinematic Lighting" © 2003 Pixar Animation Studios.
Dawn images © 2002 NVIDIA Corporation. Vulcan images © 2003 NVIDIA Corporation.

Copyright © 2004 by NVIDIA Corporation.

All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form, or by any means, electronic, mechanical, photocopying, recording, or otherwise, without the prior consent of the publisher. Printed in the United States of America. Published simultaneously in Canada.

For information on obtaining permission for use of material from this work, please submit a written request to:

      Pearson Education, Inc.
      Rights and Contracts Department
      One Lake Street
      Upper Saddle River, NJ 07458

Text printed on recycled and acid-free paper.

5 6 7 8 9 10 QWT 09 08 07

5th Printing September 2007

Developer Site Homepage

Developer News Homepage



Developer Login

Become a
Registered Developer




Developer Tools

Documentation

DirectX

OpenGL

GPU Computing

Handheld

Events Calendar



Newsletter Sign-Up

Drivers

Jobs (1)

Contact

Legal Information



Site Feedback