Sol's dirty DX7 tutorial

VertexBuffer

Although you may feel tempted to leap on to the next chapter (which covers texture mapping), you should consider reading this chapter nevertheless, especially if you wish to do something more complex with d3d in the future.

I originally meant to call this chapter 'optimizing a bit', as wrapping the vertex array inside a vertexbuffer gave me nice 50% speedup on the older TNT drivers; with the detonator drivers vertexbuffers are actually a bit slower. I assume that the original TNT drivers took local copy of the vertex arrays to make sure that the data is still there even after the DrawPrimitive call. I don't know how the detonator drivers could cope without this, but here we go nevertheless. In any case, due to the additional functionality and promised average performance, we'll use vertex buffers.

#define WIN32_LEAN_AND_MEAN		
#include <windows.h> // windows stuff
#include <stdio.h>   // standard IO
#include <stdlib.h>  // standard C lib
#include <ddraw.h>   // DirectDraw
#include <d3d.h>     // Direct3D
#include <d3dx.h>    // Direct3DX

/* 
 * Griddy2
 * d3dx7 single threaded app
 * sol/trauma 1999
 */

char progname[]="Griddy2 - Sol";
HWND mainhWnd;
HINSTANCE mainhInst;
LPD3DXCONTEXT dxctx;
LPDIRECT3DDEVICE7 d3dd;
LPDIRECT3D7 d3d;

LRESULT CALLBACK WindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam) {
  if (uMsg==WM_DESTROY) {
    d3d->Release();
    d3dd->Release();
    D3DXUninitialize();
    exit(wParam);
  }
  return DefWindowProc(hwnd,uMsg,wParam,lParam);
}

#define GRID_X 64
#define GRID_Y 48
#define XMUL (640.0f/(GRID_X))
#define YMUL (480.0f/(GRID_Y))

WORD idx[2*(GRID_X+1)*(GRID_Y+1)];
LPDIRECT3DVERTEXBUFFER7 vbuf;

void init(void) 
{
  D3DTLVERTEX *buf;
  D3DVERTEXBUFFERDESC vbd;
  vbd.dwSize=sizeof(vbd);
  vbd.dwCaps=D3DVBCAPS_WRITEONLY;
  vbd.dwFVF=D3DFVF_TLVERTEX;
  vbd.dwNumVertices=(GRID_X+1)*(GRID_Y+1);        
  d3d->CreateVertexBuffer(&vbd, &vbuf,0);

  vbuf->Lock(DDLOCK_WAIT|DDLOCK_WRITEONLY|DDLOCK_NOOVERWRITE,(void**)&buf,NULL);
    
  for (int y=0;y<(GRID_Y+1);y++)
    for (int x=0;x<GRID_X+1;x++) {
      
      idx[y*(GRID_X+1)*2+x*2+1]=(WORD)((y)*(GRID_X+1)+x);
      idx[y*(GRID_X+1)*2+x*2]=(WORD)((y+1)*(GRID_X+1)+x);
      
      buf[y*(GRID_X+1)+x].sx=x*XMUL;
      buf[y*(GRID_X+1)+x].sy=y*YMUL;
      buf[y*(GRID_X+1)+x].sz=0;
      buf[y*(GRID_X+1)+x].rhw=1;
      buf[y*(GRID_X+1)+x].color=0;
    }
  vbuf->Unlock();
}

void setup(void)
{
  D3DTLVERTEX *buf;

  vbuf->Lock(DDLOCK_WAIT|DDLOCK_WRITEONLY|DDLOCK_NOOVERWRITE,(void**)&buf,NULL);

  float i=GetTickCount()*0.02f;
  for (int y=0,c=0;y<GRID_Y+1;y++)
    for (int x=0;x<GRID_X+1;x++,c++) {     
      buf[c].color=RGB((int)(
                       sin((y+i)*0.2251474)*44+
                       cos((y-i)*0.2354128)*
                       cos((x+i)*0.3434913)*84
                       )+128,
                       (int)(
                       cos((y-i)*0.2252474)*44+
                       cos((x-i)*0.3334923)*84
                       )+128,
                       (int)(
                       sin((y-i)*0.1453474)*44+
                       sin((y+i)*0.2354328)*
                       cos((x-i)*0.3234933)*84
                       )+128);
    }
  vbuf->Unlock();
}

void reindeer(void) 
{
  static int inited=0;

  if (!inited) {
    init();
    inited++;
  }
  setup();

  //dxctx->Clear(D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER);

  d3dd->BeginScene();

  for (int y=0;y<GRID_Y;y++)
    d3dd->DrawIndexedPrimitiveVB(D3DPT_TRIANGLESTRIP, vbuf, 0, (GRID_X+1)*(GRID_Y+1), 
                                 (idx+y*(GRID_X+1)*2), (GRID_X+1)*2,0);

  d3dd->EndScene();
}

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow) 
{
  WNDCLASSEX winclass;
  HWND hWnd;
  MSG msg;

  int fs=0;
  if (MessageBox(NULL,"Shall we do it in fullscreen mode?",
                 "Silly question",MB_YESNO)==IDYES) fs=1;

  lpCmdLine=lpCmdLine;
  hPrevInstance=hPrevInstance;

  if (FAILED(D3DXInitialize())) return 0;

  mainhInst=hInstance;

  winclass.cbSize=sizeof(WNDCLASSEX);
  winclass.style=CS_DBLCLKS;
  winclass.lpfnWndProc=&WindowProc;
  winclass.cbClsExtra=0;
  winclass.cbWndExtra=0;
  winclass.hInstance=hInstance;
  winclass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
  winclass.hCursor=LoadCursor(NULL,IDC_ARROW);
  winclass.hbrBackground=GetSysColorBrush(COLOR_APPWORKSPACE);
  winclass.lpszMenuName=NULL;
  winclass.lpszClassName=progname;
  winclass.hIconSm=NULL;

  if (!RegisterClassEx(&winclass))
    return 0;

  hWnd=CreateWindow(
    progname,
    progname,
    WS_SYSMENU|WS_CAPTION|WS_BORDER|WS_OVERLAPPED|WS_VISIBLE|WS_MINIMIZEBOX,
    CW_USEDEFAULT,
    0,
    640,
    480,
    NULL,
    NULL,
    hInstance,
    NULL);

  mainhWnd=hWnd;

  if (FAILED(D3DXCreateContext(  
              D3DX_DEFAULT,  
              D3DX_CONTEXT_FULLSCREEN*fs,  //windowed = 0
              hWnd,
              D3DX_DEFAULT,  
              D3DX_DEFAULT,  
              &dxctx))) return 0;
  d3dd=dxctx->GetD3DDevice();
  d3d=dxctx->GetD3D();
  d3dd->SetRenderState(D3DRENDERSTATE_CULLMODE,D3DCULL_NONE ); // no cull
  d3dd->SetRenderState(D3DRENDERSTATE_DITHERENABLE,TRUE); // dither on
  d3dd->SetRenderState(D3DRENDERSTATE_ZENABLE,D3DZB_FALSE); // no zbuf

  d3dd->SetRenderState(D3DRENDERSTATE_CLIPPING,FALSE);
  d3dd->SetRenderState(D3DRENDERSTATE_LIGHTING,FALSE); 

  ShowWindow(hWnd,nCmdShow);
  int frame=0;
  int starttime;
  starttime=GetTickCount();;
  char str[200];
  while (1) {
    reindeer();
    frame++;
    int sec=GetTickCount()-starttime;
    if (sec>0) sprintf(str,"frame:%05d sec:%05d.%03d fps:%3.3f (alt-F4 quits)",
                       frame,sec/1000,sec%1000,(frame*1000.0)/sec);
    dxctx->DrawDebugText((float)(2/640.0),(float)(2/480.0),0xffffff,str); 
      
    dxctx->UpdateFrame(0);

    if(PeekMessage(&msg,hWnd,0,0,PM_REMOVE)) {
      switch(msg.message) {
      case WM_QUIT:
      case WM_DESTROY:
        d3d->Release();
        d3dd->Release();
        D3DXUninitialize();
        return (msg.wParam);
      default: 
        DefWindowProc(hWnd,msg.message,msg.wParam,msg.lParam);
      }          
    }  
  }    
}

Let's do things in logical order for a change.

LPDIRECT3DVERTEXBUFFER7 vbuf;

This is where we make the vertex buffer variable.

D3DVERTEXBUFFERDESC vbd;
vbd.dwSize=sizeof(vbd);
vbd.dwCaps=D3DVBCAPS_WRITEONLY;
vbd.dwFVF=D3DFVF_TLVERTEX;
vbd.dwNumVertices=(GRID_X+1)*(GRID_Y+1);        
d3d->CreateVertexBuffer(&vbd, &vbuf,0);

And here is where we create the vertex buffer. This, like so many other windows things means we fill an array with the specifications of what we need and then call the creation function. First we set the size of this array (just like with all those other windows calls), then we define the capabilities of this vertex buffer. We'll only be writing to this buffer so the drivers may optimize their calls knowing this. We could force the vertex buffer to system memory (instead of, say, t&l card's memory) by setting _SYSTEMMEMORY flag on. There's also _DONOTCLIP flag but it crashes the system for some reason although we don't need any clipping information here. FVF is the same flexible vertex format info we used in the drawprimitive call earlier.

vbuf->Lock(DDLOCK_WAIT|DDLOCK_WRITEONLY|DDLOCK_NOOVERWRITE,(void**)&buf,NULL);
vbuf->Unlock();

Before trying to mess with the contents of the buffer we must lock it, and unlock it afterwards so that the hardware (or directx itself) can touch it. First parameter is again flags, where we tell the system to wait until we can lock the buffer if it's being used, and that we'll only write to it and we will not overwrite any data written this frame. Latter two are there, again, so that drivers may optimize their behavior. Next parameter is the pointer to our pointer to the buffer, which we can use in the exactly same way as we did the vertex array earlier. The last parameter would be a pointer to dword which would receive the size of this vertex buffer, but as we already know it, we'll just set this to NULL.

The last changed bit is the rendering call:

d3dd->DrawIndexedPrimitiveVB(D3DPT_TRIANGLESTRIP, vbuf, 0, (GRID_X+1)*(GRID_Y+1), 
                              (idx+y*(GRID_X+1)*2), (GRID_X+1)*2,0);

As you can see, the vertex type info is missing compared to the non-VB version. The extra 0 after vbuf is the first vertex in the vertex buffer to use. Otherwise the calls are identical.

So why use vertexbuffers? First, it's a handy wrapper. Second, the drivers can trust that the memory used by them is still there after the calls. Third, this data can exist in non-system memory, and those new t&l cards may be used to calculate transformations and stuff, using the ProcessVertices call of the vertexbuffer object.

In the last chapter (so far) we'll add some textures..