Fastgraph 3D Tutorial

Chapter 5: Taking Advantage of DirectX

Speed Considerations

The biggest advantage of using either DirectDraw or Direct3D with your Fastgraph program is you can take advantage of hardware rendering and blitting using graphics accelerator chips on some modern video cards.

This can make a 3D program really fast.

Unfortunately, it can also give you inconsistent results. For more information about the advantages and disadvantages of using DirectX with Fastgraph, have a look at some benchmark results.

Fastgraph works with Direct3D in what is known as immediate mode. That means a program will access the capabilities of the video card directly, going through the driver only to a very limited extent. Fastgraph uses Direct3D in full screen applications only. Graphics are written directly to video memory as opposed to being written to a surface in RAM and then copied to video memory.

I will discuss the use of Direct3D in a program called TBench, that performs benchmark tests. This is another simple C program, very similar to the one in Chapter 2. If you had any trouble with the discussion of textures in the last chapter, take a look at the way we do textures here. It is a little simpler.

Benchmark Source Code

Here is the source code to TBench.c:



#include 
#include 

//----------------------------------------------------------------------
// Function declarations
LRESULT CALLBACK WindowProc(HWND,UINT,WPARAM,LPARAM);
void Init3D(void);
BOOL InitVirtualBuffers(void);
void LoadTextures(void);
void DrawCube(void);

#define vbWidth  800
#define vbHeight 600
#define maxX     799
#define maxY     599
#define vbDepth  16

#define IRAND(min,max) ((rand()%((max)-(min)+1))+(min))

byte *TextureMap[32];   // there's actually only 6 in the file
int TextureHandle[32];
int TextureCount;

//----------------------------------------------------------------------
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdParam, int nCmdShow)
{
   static char szAppName[] = "FGbench.c";
   HWND        hWnd;
   MSG         msg;
   WNDCLASSEX  wndclass;

   wndclass.cbSize        = sizeof(wndclass);
   wndclass.style         = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
   wndclass.lpfnWndProc   = WindowProc;
   wndclass.cbClsExtra    = 0;
   wndclass.cbWndExtra    = 0;
   wndclass.hInstance     = hInstance;
   wndclass.hIcon         = LoadIcon(NULL,IDI_APPLICATION);
   wndclass.hCursor       = LoadCursor(NULL,IDC_ARROW);
   wndclass.hbrBackground = NULL;
   wndclass.lpszMenuName  = NULL;
   wndclass.lpszClassName = szAppName;
   wndclass.hIconSm       = LoadIcon(NULL,IDI_APPLICATION);
   RegisterClassEx(&wndclass);

   hWnd = CreateWindowEx(WS_EX_TOPMOST,szAppName,"Fastgraph Direct3D benchmark",
      WS_POPUP,0,0,vbWidth,vbHeight,NULL,NULL,hInstance,NULL);

   ShowWindow(hWnd,nCmdShow);
   UpdateWindow(hWnd);

   while (GetMessage(&msg,NULL,0,0))
   {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
   }
   return msg.wParam;
}

//----------------------------------------------------------------------
HDC      hDC;                  // handle of a windows device client
HPALETTE hPal;                 // handle of a windows palette
int      VBuffer;              // handle of a Fastgraph virtual buffer
int      ZBuffer;              // handle of a Fastgraph Z-buffer

//----------------------------------------------------------------------
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam,
LPARAM lParam)
{
   HGLOBAL hmem;
   BOOL rc;
   int i;

   switch (message)
   {
      case WM_CREATE:
         ShowWindow(hWnd,SW_SHOWNORMAL);
#ifdef DIRECT3D
#ifdef SOFTRENDER
         fg_ddsetup(vbWidth,vbHeight,vbDepth,FG_DX_FLIP|FG_DX_RENDER_SW|FG_DX_ZBUFFER);
#else
         fg_ddsetup(vbWidth,vbHeight,vbDepth,FG_DX_FLIP|FG_DX_RENDER_HW|FG_DX_ZBUFFER);
#endif
#else
         fg_modeset(vbWidth,vbHeight,fg_colors(),1);
#endif
         hDC = GetDC(hWnd);    // find the handle of the device context
         fg_setdc(hDC);        // tell Fastgraph the device context handle
         hPal = fg_defpal();   // define a windows palette
         fg_realize(hPal);     // realize the palette

         rc = InitVirtualBuffers(); // initialize the virtual buffers
         if (!rc)
         {
            MessageBox(hWnd,"DirectX not available","Error",MB_OK);
            DestroyWindow(hWnd);
            return 0;
         }
         Init3D();             // initialize the 3D geometry
         LoadTextures();       // get texture data from a file
         srand(fg_getclock()); // launch random number generator
         DrawCube();           // draw a cube in the virtual buffer
         return 0;

      case WM_KEYDOWN:
         switch(wParam)
         {
            case VK_ESCAPE:
               DestroyWindow(hWnd);
               break;
         }
         return 0;

      case WM_SETFOCUS:
         fg_realize(hPal);    // realize the palette
         InvalidateRect(hWnd,NULL,TRUE);
         return 0;

      case WM_DESTROY:
         for (i=0; i < TextureCount; i++) // free the textures
         {
            hmem = GlobalHandle(TextureMap);
            GlobalUnlock(hmem);
            GlobalFree(hmem);
         }
         fg_tmfree(-1);
         fg_zbfree(ZBuffer);  // free the Z-buffer
         fg_vbclose();        // close the virtual buffer
#ifdef DIRECT3D
         fg_vbfin();          // shut down virtual buffers
#else
         fg_vbfree(VBuffer);  // free the virtual buffer
         fg_vbfin();          // shut down virtual buffers
         fg_modeset(0,0,0,0); // restore original display resolution
#endif
         DeleteObject(hPal);  // free the palette
         ReleaseDC(hWnd,hDC); // free the device context
         PostQuitMessage(0);
         return 0;
   }
   return DefWindowProc(hWnd,message,wParam,lParam);
}

//----------------------------------------------------------------------
BOOL InitVirtualBuffers(void)
{
   int status;

   // initialize the virtual buffer system
   status = fg_vbinit();
   if (status < 0) return FALSE;

   // define the virtual buffer color depth
   fg_vbdepth(vbDepth);

   // the DirectDraw back buffer uses virtual buffer handle 0
#ifdef DIRECT3D
   VBuffer = 0;
#else
   VBuffer = fg_vballoc(vbWidth,vbHeight);
#endif

   // open the virtual buffer
   fg_vbopen(VBuffer);

   // fill the virtual buffer with white pixels and display it
   fg_setcolor(fg_maprgb(255,255,255));
   fg_fillpage();
#ifdef DIRECT3D
   fg_ddflip();
   fg_fillpage();
#else
   fg_vbpaste(0,maxX,0,maxY,0,maxY);
#endif
   return TRUE;
}
//----------------------------------------------------------------------
void Init3D(void)
{
   // define 3D viewport
   fg_3Dviewport(0,maxX,0,maxY,1.0);

   // create and open the Z-buffer
   ZBuffer = fg_zballoc(vbWidth,vbHeight);
   fg_zbopen(ZBuffer);

   // set the Z clipping limits
   fg_3Dsetzclip(1.0,1000.0);
}

//----------------------------------------------------------------------
void LoadTextures(void)
{
   FILE *TextureFile;
   int ImageWidth, ImageHeight;
   int ReferenceNumber;
   HGLOBAL hmem;
   int i;
   int hcDepth;

   // open a binary file containing texture data (output from Fred)
   TextureFile = fopen("textures.dat","rb");
   if (TextureFile == NULL)
   {
      MessageBox(GetActiveWindow(),"Could not find data file: textures.dat",
      "Missing data file",MB_OK);
      DestroyWindow(GetActiveWindow());
      return;
   }

   // how many textures are in the file?
   fread(&TextureCount,sizeof(int),1,TextureFile);
   fg_tminit(TextureCount);

   // determine the RGB pixel format
   hcDepth = fg_gethcbpp();

   for (i = 0; i < TextureCount; i++)
   {
      // get the width, height and reference number
      fread(&ImageWidth,sizeof(int),1,TextureFile);
      fread(&ImageHeight,sizeof(int),1,TextureFile);
      fread(&ReferenceNumber,sizeof(int),1,TextureFile);

      // allocate memory for this texture
      hmem = GlobalAlloc(GMEM_MOVEABLE,ImageWidth*ImageHeight*2);
      TextureMap[i] = (byte *)GlobalLock(hmem);

      // read the texture data
      fread(TextureMap[i],sizeof(byte),ImageWidth*ImageHeight*2,TextureFile);

      // if this is a 5/5/5 pixel format, translate it
      if (hcDepth == 15)
         fg_transdcb(TextureMap[i],TextureMap[i],16,15,ImageWidth*ImageHeight);

      TextureHandle[i] = fg_tmdefine(TextureMap[i],ImageWidth,ImageHeight);
   }
   fclose(TextureFile);
}

//----------------------------------------------------------------------
double CubeData[6][12]=
{
   {-1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  1.0, 1.0,-1.0, -1.0, 1.0,-1.0}, // top
   {-1.0, 1.0,-1.0,  1.0, 1.0,-1.0,  1.0,-1.0,-1.0, -1.0,-1.0,-1.0}, // front
   {-1.0, 1.0, 1.0, -1.0, 1.0,-1.0, -1.0,-1.0,-1.0, -1.0,-1.0, 1.0}, // left
   { 1.0, 1.0,-1.0,  1.0, 1.0, 1.0,  1.0,-1.0, 1.0,  1.0,-1.0,-1.0}, // right
   {-1.0,-1.0,-1.0,  1.0,-1.0,-1.0,  1.0,-1.0, 1.0, -1.0,-1.0, 1.0}, // bottom
   { 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0,-1.0, 1.0,  1.0,-1.0, 1.0} // back
};

//----------------------------------------------------------------------
void DrawCube()
{
   static int r[] = {102,102,102,255,255,255};
   static int g[] = {102,255,255,102,102,255};
   static int b[] = {255,102,255,102,255,102};
   HCURSOR hCursor;
   int i, j;
   int x, y ,z;
   int xRot, yRot, zRot;
   int time1, time2;
   char string[512];
   static int TextureSource[] = {0,63, 0,0, 63,0, 63,63};

   // set the render state to z buffering, z clipping, and perspective texture mapping
   fg_3Drenderstate(FG_ZBUFFER|FG_ZCLIP|FG_PERSPECTIVE_TM);

   // set the point of view: at the origin looking down the z axis
   fg_3Dpov(4.0,4.0,-15.0, 0,0,0);

   #ifdef DIRECT3D
      fg_ddframe(0);      // tells D3D this is a new frame
   #endif

   // display the mouse cursor wait icon
   hCursor = SetCursor(LoadCursor(NULL,IDC_WAIT));
   ShowCursor(TRUE);

   fg_waitfor(1);
   time1 = fg_getclock();
   for (j = 0; j < 1000; j++)
   {
      // prepare the Z-buffer for the next frame
      fg_zbframe();

      // calculate random cube position and orientation
      x = IRAND(-10,10);
      y = IRAND(-10,10);
      z = IRAND(0,100);
      xRot = IRAND(-1800,1800);
      yRot = IRAND(-1800,1800);
      zRot = IRAND(-1800,1800);

      // put the cube at that position and orientation
      fg_3Dsetobject(x,y,z,xRot,yRot,zRot);

      // draw all the faces
      for (i = 0; i < 6; i++)
      {
         // set the texturemap object
         fg_tmselect(TextureHandle[i]);

         // draw the face as a textured polygon
         fg_3Dtexturemap(CubeData[i],(int*)TextureSource,4);
         fg_3Dtexturemapobject(CubeData[i],(int*)TextureSource,4);
      }
   }
   time2 = fg_getclock();

   // restore the normal mouse cursor arrow icon
   ShowCursor(FALSE);
   SetCursor(hCursor);

   #ifdef DIRECT3D
      fg_ddframe(1);      // done with frame
      fg_ddflip();        // flips the DirectDraw page
      #ifdef SOFTRENDER
         wsprintf(string,"         Priority set to:\n\
         Direct3D Software Rendering\n\n\
         Elapsed time = %d\n\n\
         Actual rendering method = %d\n\
         0=Fastgraph\n\
         1=D3D Software\n\
         2=D3D Hardware",
         time2-time1,fg_ddusage());
      #else
         wsprintf(string,"       Priority set to:\n\
         Direct3D Hardware Rendering\n\nElapsed time = %d\n\n\
         Actual rendering method = %d\n\
         0=Fastgraph\n\
         1=D3D Software\n\
         2=D3D Hardware",
         time2-time1,fg_ddusage());
      #endif
   #else
      fg_vbpaste(0,maxX,0,maxY,0,maxY);
      wsprintf(string,"Fastgraph Software Rendering\n\nElapsed time = %d", time2-time1);
   #endif

   MessageBox(GetActiveWindow(),string,"Fastgraph benchmark",MB_OK);
}

Notes on the Source Code

The benchmark program is a good one to look at because the Fastgraph DirectX functions are highlighted within the defined symbol DIRECT3D. I did it this way so I could compile the program three different ways: using Fastgraph only, using Fastgraph with Direct3D hardware rendering, and using Fastgraph with Direct 3D software rendering. By generating three different programs from one source code file, I make my benchmark test as accurate as possible.

Here is a batch file to compile the program all three ways, using Visual C++ 6.0:


Build.bat

CL /FoTBenchFG -c -Gs -J -Ow -W3 -Zp -Tp tbench.c
CL /FoTBenchDHW -DDIRECT3D -c -Gs -J -Ow -W3 -Zp -Tp tbench.c
CL /FoTBenchDSW -DDIRECT3D -DSOFTRENDER -c -Gs -J -Ow -W3 -Zp -Tp tbench.c
LINK TBenchFG  GDI32.LIB USER32.LIB FGWVC32.LIB
LINK TBenchDHW GDI32.LIB USER32.LIB DDRAW.LIB FGWVC32D.LIB
LINK TBenchDSW GDI32.LIB USER32.LIB DDRAW.LIB FGWVC32D.LIB

Notice when you link a DirectX program, you need to link with a different Fastgraph library. In this case we link with FGWVC32.LIB for the non-DirectX version, and FGWVC32D.LIB for the DirectX version. You also need to link with DDRAW.LIB.

Most of the Fastgraph functions behave exactly the same way whether you are using DirectDraw, Direct3D or Fastgraph by itself. There are just a few functions that are specific to Fastgraph or DirectX. Most of those have to do with initialization and blitting.

Initializing DirectX

Let's look at the lines that initialize DirectX.

#ifdef DIRECT3D
#ifdef SOFTRENDER
         fg_ddsetup(vbWidth,vbHeight,vbDepth,FG_DX_FLIP|FG_DX_RENDER_SW|FG_DX_ZBUFFER);
#else
         fg_ddsetup(vbWidth,vbHeight,vbDepth,FG_DX_FLIP|FG_DX_RENDER_HW|FG_DX_ZBUFFER);
#endif
#else
         fg_modeset(vbWidth,vbHeight,fg_colors(),1);
#endif
To initialize DirectX for blitting and rendering, we call Fastgraph's fg_ddsetup() function. This function call must be the first thing that happens in the create handler. We call fg_ddsetup() two different ways, depending on our preference.

In the first case, we set the flags FG_DX_FLIP to indicate we will use hardware page flipping and FG_DX_RENDER_SW to indicate we want to do software rendering. In the second case, we set the flag FG_DX_RENDER_HW to indicate we want to do hardware rendering.

In both cases, we are indicating the preferred method of handling rendering. If the system can not support the prefered method, the program will use Fastgraph's software blitting functions instead. This makes the software more reliable on computers that for one reason or another can't support the preferred method.

We also set the FG_DX_ZBUFFER flag to indicate we want Direct3D to handle the z buffering.

If you are not using DirectX, you can use the fg_modeset() function to set the system to a full-screen video mode (as opposed to a windowed mode), and force a screen resolution (for example, 640x480 pixels).

Checking System Capabilities

But how do you know if DirectDraw will work on the user's system? The fg_vbinit() must be called once in every program. If you are linking with the DirectX libraries, and DirectX is not available on the user's system, then fg_vbinit() will return a negative value. In that case, you have an error. You need to do something else, like exit with an error message, or possibly spawn a Fastgraph native-mode program (more about that later). Here is the check to see if DirectX is available onthe user's system:

status = fg_vbinit();
if (status < 0) return FALSE;
The fg_ddusage() function will return 1 if Direct3D is available for software rendering. It will return 2 if Direct3D is available for hardware rendering. It will return 0 if Direct3D is not available.

Now let's look at the code to allocate the virtual buffer:


#ifdef DIRECT3D
   VBuffer = 0;
#else
   VBuffer = fg_vballoc(vbWidth,vbHeight);
#endif
With DirectDraw flipping, you don't need to allocate any memory for the virtual buffer because the virtual buffer is actually a hardware page on the video card. If you ever did any DOS game programming, you will recognize the concept of hidden pages on the video card. (Those were the good old days, weren't they?). The DirectDraw "back buffer" is just the video card's hidden page. It always has a handle of 0. When you do a page flip, the handle flips too, so the hidden page still has a handle of 0.

Here is how you do a page flip:


#ifdef DIRECT3D
   fg_ddflip();
#else
   fg_vbpaste(0,maxX,0,maxY,0,maxY);
#endif
The DirectDraw page flip is going to be much faster than the Fastgraph paste. That's because when you paste from system RAM to video memory, you have to move a full page of data across the bus. That's fast, but not as fast as doing a page flip. Using DirectDraw, you can do a hardware page flip, which is about as fast as the monitor's vertical retrace -- very fast indeed.

There are just a few other things you have to remember about DirectX. The order in which things happen is important. Be sure to call fg_ddsetup() first thing in your program. Be sure to call fg_vbinit() before you call any Fastgraph functions they depend upon DirectX being initialized, such as fg_tmdefine().

Before you begin and end a rendering frame, you need to call the Fastgraph function fg_ddframe(), passing a 0 at the beginning of the frame and a 1 at the end of the frame. These functions handle the calls to Direct3D's BeginScene() and EndScene() methods. I don't really know why Direct3D requires methods at the beginning and end of each frame. It's one of those DirectX mysteries. Maybe it locks and unlocks the DirectDraw surface. One of my books mentions flushing a cache.

Making High Color RGB Pixel Formats Compatible

RGB pixel formats at a 16-bit resolution can use either a 5/5/5 or 5/6/5 strategy. On some video cards, the DirectX driver will set up the high color RGB pixel format at 5/5/5. Fastgraph virtual buffers will match the DirectX surface. In this program we are reading textures from a file, and we know they were made with a 5/6/5 RGB pixel format (the default, when you use Fastgraph without DirectX). We check to see if the texture format matches the current DirectDraw surface with fg_gethcbpp(). If they don't match, we translate the textures using fg_transdcb(). If you don't do this step, you may notice odd-looking colors on some systems.


// determine the RGB pixel format
hcDepth = fg_gethcbpp();
 ...
// if this is a 5/5/5 pixel format, translate the bitmap
if (hcDepth == 15)
   fg_transdcb(TextureMap[i],TextureMap[i],16,15,ImageWidth*ImageHeight);
Those are the only DirectX specific functions in the TBench program. There are some other DirectDraw functions in Fastgraph. You will find them in the Fastgraph manual. If you use DirectDraw without Direct3D, you can write windowed programs, and you can blit from system RAM or write directly to video memory. So there are a few other things you can do with DirectX and Fastgraph, but the most dramatic results are to be found in the hardware rendering functions in Direct3D.

Do I Have to Write My Program Two Times?

I am afraid Direct3D is not reliable on all systems. For that matter, all of DirectX may be unreliable. If you want your game to be reliable, you should consider releasing both a DirectX version and a Fastgraph only version. You can build both exe files from the same source code file if you use the conditional compilation strategy shown above. Then you can detect DirectX during the installation process and select which program to install. Or, you can write a small "stub" program that uses fg_vbinit() to detect DirectX on the system and then calls WinExec() to launch either the DirectX version or the Fastgraph version, as appropriate.

Your DirectX version will support DirectDraw page flipping, and Direct3D hardware rendering if it is available. If 3D hardware rendering is not available, that program will default to Fastgraph rendering, but still use DirectDraw page flipping. If DirectDraw is not available, you must use the other program, the one compiled to not use any DirectX.

For example, if we write the TDemo program from chapter 4 to support both Fastgraph native mode, and DirectX, we might end up with 2 programs: Tdemo.exe and TdemoDD.exe. You would run the TdemoDD program first, and if it determines DirectX was not available, you would spawn the Tdemo program like this:


#ifdef DIRECT3D // code for the TdemoDD.exe version
   int status = fg_vbinit();
   // is DirectDraw available for page flipping?
   if (status < 0)
   {
      // DirectX not available on this system
      MessageBox(hWnd,"DirectX not available\nLaunching Fastgraph version.",
      "System Info",MB_OK);

      // try running the non-DirectX version
      DestroyWindow(hWnd);
      WinExec("Tdemo.exe",SW_SHOWNORMAL);
      return;
   }
#else // code for the Tdemo.exe version
   fg_vbinit();
#endif
Note: Fastgraph without DirectX is extremely reliable. It works very well on every system we have tested it on -- and we have done some rigorous testing. It is only when you introduce DirectX into your programs that you have a variety of unreliability problems.

I would not bother with Direct3D software rendering at all. I can't see any point in it -- it is both slow and unreliable and Fastgraph can replace it seemlessly.

Review

Using Fastgraph with DirectX has both advantages and disadvantages. If you decide to use Fastgraph with DirectX, you will need to add a few simple functions to your program. You will need to link with different libraries.

Functions to initialize DirectX with Fastgraph need to be done in order. First call fg_ddsetup(), then call fg_vbinit(). The fg_ddusage() function will tell you if 3D hardware rendering is available.

Most of Fastgraph's functions are called the same way whether or not you are using DirectDraw or Direct3D. That makes it easy to write a program one time, and use conditional compilation to create versions for various rendering and blitting schemes. I recommend releasing two programs, one that supports DirectX and one that uses Fastgraph without DirectX, and then detect DirectX on the user's system to decide which program to run.

That about wraps it up for the discussion of DirectX with Fastgraph. There is more specific information in the Fastgraph manual. Now take a little break before we move on to our discussion of image processing.


 

Introduction
Chapter 1 | Chapter 2 | Chapter 3
Chapter 4 | Chapter 5 | Chapter 6 | Chapter 7
Appendix 1 | Appendix 2 | Appendix 3
Benchmarks
Fastgraph Home Page

 

become a computer game developer

copyright © 2007 Ted Gruber Software inc. all rights reserved.
This page written by Diana Gruber.