Sue Ledoux
Microsoft Corporation
December 23, 1996
Microsoft® Direct3D consists of two distinct APIs. The higher-level Retained-Mode API offers scene- and object-management services and a built-in geometry engine, freeing application developers from having to maintain and manipulate object databases. The lower-level Immediate-Mode API provides direct access to the hardware and allows experienced 3-D programmers to implement their own rendering and scene management. This supplies both flexibility and performance advantages over Retained Mode.
Most of the documentation for Direct3D addresses the two APIs separately, and many people assume that the APIs are mutually exclusive. In fact, there are several instances in which you may wish to use functionality from both APIs in a single application.
In this article I discuss two situations in which you would use elements of both the Retained-Mode and the Immediate-Mode APIs:
This article assumes that you already have some knowledge of Direct3D. It is not a Direct3D overview or a tutorial. Instead, it addresses a question not covered elsewhere and that you might ask after reading the documentation. For this article to be meaningful, you should have at least read the Direct3D documentation.
Note When discussing Direct3D in general terms, the documentation usually uses the term Direct3D to refer to the entire 3D API, consisting of both Retained-Mode and Immediate-Mode APIs. However, the naming convention of the Direct3D code uses the abbreviation D3D somewhere within the name when referring to Immediate-Mode interfaces or variables, and D3DRM when referring to Retained-Mode entities. Because of this convention, in discussions about sample code or specific interfaces, the term Direct3D sometimes refers to Immediate Mode rather than the entire set of 3D APIs. This article attempts to make the distinction clear, but you should be aware that the sample code and comments within it may use the term Direct3D in both senses.
Applications that employ Direct3D, whether they use Immediate Mode or Retained Mode, usually need to enumerate the drivers that are available at run-time on an end-user's machine. If image quality is more important to the application than rendering speed , the app should choose the driver supporting the highest bit depth and/or resolution. If, on the other hand, high-speed rendering is the key requirement, the application may want to sacrifice some image quality in favor of performance.
Retained Mode does not contain a driver-enumeration method. Instead, all Direct3D applications use the IDirect3D::EnumDevices method. The DirectX 3 SDK has a good explanation and some sample code on how to use this method; check out the Direct3D Retained-Mode Tutorial in Chapter 5: Direct3D Overview. Using this method to enumerate device drivers is not difficult. I will touch on a few points, and you can refer to the documentation for a complete description of how to do it.
When a developer needs to call an Immediate-Mode API from a Retained-Mode application, the obvious question is: "How do I get an interface pointer to Immediate-Mode Direct3D?" Simple: Because the Direct3D COM interface is really an interface to a DirectDraw® object (isn't COM amazing?), you can obtain a Direct3D pointer (remember, Direct3D means Immediate Mode here) through the DirectDraw interface. This can be accomplished in two easy steps:
LPDIRECTDRAW lpDD; LPDIRECT3D lpD3D; HRESULT rval; DirectDrawCreate (NULL, &lpDD, NULL); rval = lpDD->lpVtbl->QueryInterface(lpDD, &IID_IDirect3D, (void**) &lpD3D);
See? What could be easier? Now you have a handy dandy pointer to a Direct3D interface (lpD3D) and you can go along your merry way calling the IDirect3D::EnumDevices method to enumerate the available drivers. From here on, it works just the same as it would from an Immediate-Mode application: you define a enumeration callback routine and pass its address to the IDirect3D::EnumDevices method. Your callback will be called for each driver installed on the system, and the callback can examine the characteristics of each driver to determine which one suits the needs of your application.
So, that covers one case of using methods from both Immediate Mode and Retained Mode in a single application. Now let's move on to a much more interesting case of mixing modes in Direct3D.
Sometimes an application may wish to use an execute buffer (allowing it to perform its own transformation, lighting, or rasterization) while at the same time using the more convenient APIs available in Retained Mode. This can be accomplished by treating the execute buffer as a visual in a Direct3D Retained-Mode scene.
You'll find a sample in the SDK called UVIS (User VISual) that demonstrates this technique. If you're the sort who would rather just read the code and be done with it, and you've barely managed to contain your impatience thus far, you needn't continue. Just go read the sample code and you'll see how to mix the two modes. For those who prefer an explanation of the sample using complete sentences and a proportional font, let's walk through the sample code and discuss some of the major points.
If you've already figured out how to get the DirectX 3 samples to compile, you can skip this section. If you haven't compiled any of the samples yet, you'll have to go through a few steps. Here's what you need to do to compile the UVIS sample for use with the Microsoft Developer Studio:
If you're still a die-hard command-line cowboy, makefiles are provided for your nmake-ing pleasure. You'll just need to set up your INCLUDE and LIB variables to include the directories listed above.
All of the Retained-Mode samples in the Direct3D SDK share a common code base for simplicity; this lets you focus on the purpose of the sample without bothering with the elements which are the same in each sample. The common code is contained in two files--RMMAIN.CPP and RMERROR.C, in the MISC subdirectory of c:\dxsdk\sdk\samples. The sample-specific code for the UVIS sample is in UVIS.CPP in the UVIS subdirectory.
The common code base contains code to create and manage a standard Windows application and perform the basic Retained-Mode initialization and processing. The sample code is expected to assemble the scene it is demonstrating. In the following sections I review the main parts of the UVIS sample. I won't examine each function call in excruciating detail; my purpose is not to explain how to use either Retained Mode or Immediate Mode but rather to show how both can be used in the same application. However, I hope this article will help you understand how the sample code is put together so that you can get through the samples quickly.
Note In the Appendix you'll find a call hierarchy detailing some of the parts of the UVIS sample. This gives a more compact way to view the layout and functionality of the sample code and provides a nice summary of what the sample code and common code harness does. You might want to print it out and refer to it as you read the following discussion. You may also find this useful when looking at any of the other Retained-Mode samples that use the common code found in RMMAIN.CPP.
The code in RMMAIN.CPP forms the framework for a generic Direct3D Retained-Mode application. The bulk of the code is contained within the WinMain() function. Within WinMain(), there are two main sections--the application setup and initialization phase; and the message loop, in which the actual rendering takes place.
The harness code encapsulates most of the initialization code in the InitApp() function. This function sets the application up as your basic garden-variety Retained-Mode application. It begins by going through the usual window class setup, and then initializes some global variables. Following this is a call to OverrideDefaults(), which is one of the two functions the sample code harness expects the individual sample to provide. OverrideDefaults() lets the sample define a few of its own settings by filling in the following structure:
typedef struct Defaultstag {
BOOL bNoTextures;
BOOL bResizingDisabled;
BOOL bConstRenderQuality;
char Name[50];
} Defaults;
Next the window is created in the normal Windows fashion, and the sample goes through a few steps needed to set up the Retained-Mode application: the enumeration of devices, creation of the main D3DRM object, creation of the master scene and camera, setting of render quality, and so on. Just before the window is made visible, InitApp() calls the BuildScene() function in UVIS.CPP. This is where all the excitement is, but I'm going to make you wait just a bit longer to taste the action. First I want to finish discussing how the sample harness works. The application is initialized--now it's time to get into the loop.
Once the application is initialized, WinMain() sets up the standard Windows message loop. Included in this loop is a call to RenderLoop(), which, in the case of these Direct3D Retained-Mode samples, performs most of the actual work of rendering the objects to the screen.
RenderLoop() works by making a series of four calls to three different Retained-Mode interfaces. First, it calls IDirect3DRMFrame::Move() to apply rotations and velocities for all frames. It then calls IDirect3DRMViewport::Clear() to clear current the viewport and set the background color. Next it makes a call to IDirect3DRMViewport::Render() to render the current scene into the current viewport. And finally, IDirect3DRMDevice::Update() copies the rendered image to display.
It is through the IDirect3DRMViewport::Render() call that the work actually gets done. It's here that the system makes calls to each object in the scene, telling the objects to render themselves. Additionally this is how, as you'll see in the next section, we can sneak in an object rendered through Immediate Mode into this Retained-Mode application.
All of what was just described is a standard, albeit simple, Retained-Mode application. Now we'll have a look at what UVIS demonstrates--and this is the focal p oint of this article--the ability to employ user visuals in Retained Mode.
As you saw in RMMAIN.CPP, InitApp() called BuildScene(), defined in UVIS.CPP, to set up the scene particular to this sample application. Here's where it gets cool. Instead of just adding a bunch of Retained-Mode objects as you would in the rest of the Retained-Mode sample applications, I make use of user visual objects to add an object that represents an execute buffer and its creation and rendering routines. A user visual is simply a user-defined visual object that is added to a scene like any of the predefined visuals, with the developer providing the creation and rendering routines. This introduces a high degree of flexibility to the Retained-Mode API.
The BuildScene() function starts out by creating some lights for the scene. It then calls CreateFire(), which actually creates the Immediate-Mode object that will be used as a user visual. Let's look at some actual code snippets to see how this is done.
First, let's look at the Fire structure that is defined in UVIS.CPP:
typedef struct _Fire {
Flame flames[MAX_FLAMES];
LPDIRECT3DRMDEVICE dev;
LPDIRECT3DEXECUTEBUFFER eb;
LPDIRECT3DMATERIAL mat;
} Fire;
CreateFire() creates a Fire structure to contain information about the user visual that we'll create--a "burning" fire consisting of a number of individual flames. The Fire structure contains a data structure for each flame (containing information about the flame's position, velocity, lifetime, etc.), and pointers to the Retained-Mode device, an execute buffer, and a material. This structure is initially empty:
Fire* fire;
fire = (Fire*)malloc(sizeof(Fire));
if (!fire)
goto ret_with_error;
memset(fire, 0, sizeof(Fire));
The IDirect3DRM::CreateUserVisual() function creates a user visual object and passes back the address of this object in the uvis variable. Associated with this object is the application-defined data (in this case the Fire structure) and the callback (called FireCallback() in this sample) that will be invoked whenever the system wants the application to render the user visual.
LPDIRECT3DRMUSERVISUAL uvis = NULL;
if (FAILED(lpD3DRM->CreateUserVisual(FireCallback, (void*) fire, &uvis)))
goto ret_with_error;
The DestroyFire() callback will be called when the user visual needs to be destroyed:
if (FAILED(uvis->AddDestroyCallback(DestroyFire, (void*) fire)))
goto ret_with_error;
After BuildScene() has called CreateFire() to set up the user visual, it then adds this visual to the scene. (Note that the uvis variable below is a different variable but contains the same value as the uvis mentioned above, since this value is returned from the CreateFire() function.)
uvis = CreateFire();
if (!uvis)
goto generic_error;
if (FAILED(frame->AddVisual(uvis)))
goto generic_error;
Now the user visual has been created. At this point it's just a placeholder, an empty structure attached to a frame in a scene. But when the program goes through the render loop, the system tries to render each object in the scene. When it gets to the user visual, it calls FireCallback(), which in turn calls RenderFire() to actually do the job of rendering.
RenderFire() is called each time through the rendering loop. This function consists of several steps that create and maintain the continuously burning "fire" that you see when you run UVIS.EXE. First up is CreateFireObjects(), which is only called the first time through the rendering loop. This function first gets a pointer to the Direct3D device from a Direct3DRM device as follows:
dev->GetDirect3DDevice(&lpD3DDev);
if (!lpD3DDev)
goto generic_error;
if (FAILED(lpD3DDev->GetDirect3D(&lpD3D)))
goto generic_error;
Next, an execute buffer is created and filled, as is typical of a normal Immediate-Mode application: the material, lighting, and shade states are set; triangles are created; and so on.
Each time through the render loop, RenderFire() checks for any flame that is no longer valid because it has "burned out" (meaning its preset lifetime has expired). For each burned-out flame (or all flames the first time around) InitFlame() is called, which sets a lifetime and velocity and assigns a random position to the flame. Next, UpdateFlame() is called each round for each flame to update the position and size of the flame according to the current time.
Finally, now that RenderFire() has set up and updated the flames of the fire, it calls IDirect3DDevice::Execute(). This is the function that actually processes the execute buffer and causes it to be rendered to the screen, letting you see that cozy flickering "fire."
You can have the best of both worlds--an entire Direct3D Retained-Mode application that uses Immediate Mode to create anything via a user visual. Retained Mode can take care of all common, everyday objects, and Immediate Mode can render just about any custom object or effect you might need. Ain't life grand?
This appendix illustrates some of the major function calls in the UVIS sample, a Direct3D Retained-Mode sample that ships with the DirectX 3 SDK. The list is not exhaustive; it is simply a list of the functions I found useful to call out when I was sorting out the layout of the sample Retained-Mode applications.
Sections marked in green are those that are in UVIS.CPP; the rest are in RMMAIN.CPP.
WinMain()