scorpioncity.com
scorpioncity.com · This is/was my old 'personal website' (~1997 to 2010-ish); my new 'personal website' is at djoffe.com »
As of Oct 2016 I have decided to add some of the old content back again. Note that some of the info on the site is now outdated.
- David Joffe
Game programming with DirectX
[ Home | Chapter 1 | Chapter 2 | Chapter 3 | Chapter 4 | Chapter 5 | Chapter 6 ]

Chapter 5 - Miscellaneous unsorted new stuff

1. About conv3ds.exe

conv3ds -X: save templates. Nothing to worry about, part of the nature of x-files. Useful though if you want to see what the various parameters of things (like materials) mean.

conv3ds -m: merge all objects into one single mesh. Useful if you don't need separate meshes, and can speed up loading.

conv3ds -x: Save as text file: Definitely not a bad idea, especially when debugging. It helps to have a look at the files etc

conv3ds -T: Create a top-level frame with all objects as child frames. This is useful since the pFrame->Load loads only the first frame it finds in the file.

conv3ds -v : Verbose mode.

Possible bug (possibly not in v5, only in v3) .. seems to need XForms reset in 3dstudio or strange things happen. Will maybe check this out.

When using a pFrame object to load a mesh from a mesh file, and you want to retain (more coming)

Coming:

Plan to learn OpenGL: Comparison etc, as unbiased as I can muster up.

Enumerating device drivers

Callbacks

RM vs IM

A decent sample with D/L'able code.

Comments on MS samples

How to compile with NMAKE

faq: main: linker error

3D math

Fog + some other frame rate optimization techniques

Loading and displaying a .bmp (or .png :)

Execute buffers vs DrawPrimitive

2. Drawing a pixel with DirectDraw

Microsoft did not provide a "PutPixel"-type function with DirectDraw to draw a pixel onto a DirectDraw surface. Granted, no real game is going to draw its graphics using a PutPixel routine, since it would be too slow - but it would have been nice to have such a "convenience function", and it could also be useful for testing out stuff. Some applications that might not necessarily need speed could also benefit from the convenience of a PutPixel routine. But, DirectX was not designed with convenience in mind.

Anyway, there are a number of ways to go about writing a PutPixel routine. One way would be to use GDI functions on the surface, using GetDC(). Another way is to use the IDirectDrawSurface::Blt() function, specifying a solid color operation with a 1x1 rectangle. A third way is to obtain a pointer to the memory representing the surface by calling IDirectDrawSurface::Lock(), and drawing the pixel in memory yourself. Each of these methods demonstrates general techniques whose usefulness goes beyond a simple PutPixel function.

2.1. PutPixel by surface locking

The basic technique here is to obtain a pointer to the memory representing the surface. You then calculate the offset into that memory for the coordinates of the pixel you wish to draw, and then you write the bytes representing the color of the pixel you want into that position in memory. There is nothing complex or difficult here, it's just a lot of grunt-work and jumping through hoops.

By default, you may not just write directly into the memory of a surface. You first have to obtain exclusive access to that surface, by "locking" it. This is done with a call to Lock().

HRESULT IDirectDrawSurface::Lock( LPRECT pRect, LPDDSURFACEDESC pDDSD, DWORD dwFlags, HANDLE hEvent );
pRect
Area of surface to lock. NULL locks entire surface.
pDDSD
Points to struct that describes the surface.
dwFlags
Control flags.
hEvent
Unused. Microsoft loves these unused parameters.

The pointer to the surface memory is one of the members of the DDSURFACEDESC structure, and is filled by the call to Lock() with a valid pointer. You may then write to this memory, and when you are finished with it you must call Unlock on that surface.

Now, to draw a pixel, you need to understand how pixels are represented in memory, which will depend on what color depth screen mode your surface is (see Chapter 2). Of course, if you create a "generic" PutPixel routine, that will work no matter what mode you are in, you will probably want to specify the color in some human-readable format, like an RGB triple. The PutPixel routine must then convert this, store it somewhere, and dump it to the surface memory. I use an unsigned int to temporarily store the pixel value. For convenience, I have created a CreateRGB function that can generate this unsigned int from supplied RGB values which range from 0 to 255:


/*--------------------------------------------------------------------------*/
// Create color from RGB triple
unsigned int CreateRGB( int r, int g, int b )
{
   unsigned int pixel;
   switch (g_iBpp)
   {
   case 8:
      // Here you should do a palette lookup to find the closest match.
      // I'm not going to bother with that. Many modern games no
      // longer support 256-color modes.
      break;
   case 16:
      // Break down r,g,b into 5-6-5 format.
      pixel = ((r/8)<<11) | ((g/4)<<5) | (b/8);
      break;
   case 24:
   case 32:
      pixel = (r<<16) | (g<<8) | (b);
      break;
   default:
      pixel =0;
   }
   return pixel;
}
/*--------------------------------------------------------------------------*/

Here is the actual PutPixel routine, taking as input a DirectDraw surface, an (x,y) location, and an RGB triple with r,g and b in the range 0 to 255. The function returns 0 for success. Notice the function, like CreateRGB, assumes the existence of g_iBpp, which I will explain in a bit.


/*--------------------------------------------------------------------------*/
int PutPixel( LPDIRECTDRAWSURFACE pDDS, int x, int y, int r, int g, int b )
{
   DDSURFACEDESC   ddsd;       // Surface description
   unsigned int    pixel;      // Store pixel to be dumped to screen
   int             offset;     // Store offset of destination memory location
   unsigned char * szSurface;  // Store pointer to surface
   HRESULT         hr;
   int             pixelwidth; // The width of a single pixel, in bytes

   // Initialize struct information
   ddsd.dwSize = sizeof(ddsd);

   // Calculate how the pixel is represented in memory
   pixel = CreateRGB( r, g, b );

   // Lock entire surface, wait if it is busy, return surface memory pointer
   hr = pDDS->Lock( NULL, &ddsd, DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR, NULL );
   if (FAILED(hr))
      return -1;

   // Get surface memory pointer
   szSurface = (unsigned char*)ddsd.lpSurface;

   // Calculate the width of a pixel in bytes - this is how many bytes
   // we must write to the surface
   switch (g_iBpp)
   {
   case 16: pixelwidth = 2; break;
   case 24: pixelwidth = 3; break;
   case 32: pixelwidth = 4; break;
   default: pixelwidth = 1;
   }

   // Calculate memory offset
   // (Notice I use dwPitch instead of dwWidth - see Chapter 2)
   offset = (y*ddsd.lPitch + x*pixelwidth);

   // Copy pixel onto the surface, need string.h for this
   memcpy( szSurface + offset, &pixel, pixelwidth );

   // Unlock the surface. The parameter may be NULL if you locked the entire
   // surface, otherwise it must be a pointer to the DirectDraw surface memory.
   hr = pDDS->Unlock( NULL );
   if (FAILED(hr))
      return -2;

   return 0;
}
/*--------------------------------------------------------------------------*/

There are a few things worth mentioning about this function. Firstly, it can be optimized quite a lot, I know. But in a production game, this is not the function you will be using for all your drawing routines anyway. (One of the rules of optimization - never optimise what you don't need to.) Secondly, it makes some crass assumptions about your architecture - it may not work if your system is big endian. However, the Intel x86 is little endian, and as far as I know DirectX is only really available on the Intel x86. Thirdly, it only knows about a limited number of screen modes, and sometime in the future, when people start using video modes with higher bit depths (like 48), the code may no longer work. (However, as far as I can tell, DirectDraw itself may not be able to handle 48-bit color depths.) Anyway, the point is that to make the above PutPixel routine "correct" requires quite a bit more work still.

The g_iBpp in the code above is an integer representing the bits per pixel of the screen mode of the surface. You can get this from the DDSURFACEDESC.

Something else worth mentioning is that if the DirectDraw surface here is the primary surface - that is, it represents the entire screen, and you are in windowed mode and not in full-screen mode - then the pixel offsets here are relative to the top left of the screen, and not to your application window. You will find yourself drawing all over the screen, unless you work the offset of your window into your calculations (eg by using the ClientToScreen Win32 call or something.)

2.2. PutPixel by using IDirectDrawSurface::Blt

This routine is demonstrated in the supplied sample DDSamp.

This method also makes use of the CreateRGB function described in 5.1.1, so go read that part now if you have not done so yet.

This method is quite simple, and probably a bit "safer" than the method described in 5.1.1.


/*--------------------------------------------------------------------------*/
// PutPixel routine for a DirectDraw surface
void DDPutPixel( LPDIRECTDRAWSURFACE pDDS, int x, int y, int r, int g, int b )
{
   HRESULT hr;
   DDBLTFX ddbfx;
   RECT    rcDest;
   POINT   p;

   // Safety net
   if (g_pDDS == NULL)
      return;

   // Initialize the DDBLTFX structure with the pixel color
   ddbfx.dwSize = sizeof( ddbfx );
   ddbfx.dwFillColor = (DWORD)CreateRGB( r, g, b );

   // Prepare the destination rectangle as a 1x1 (1 pixel) rectangle
   p.x = x;
   p.y = y;
   ClientToScreen( g_hWnd, &p );
   OffsetRect( &rcDest, p.x, p.y );
   SetRect( &rcDest, p.x, p.y, p.x+1, p.y+1 );
   
   // Blit 1x1 rectangle using solid color op
   hr = g_pDDS->Blt( &rcDest, NULL, NULL, DDBLT_WAIT | DDBLT_COLORFILL, &ddbfx );
   if (FAILED(hr))
      OutputDebugString( (LPCTSTR)"Royal fuckup\n" );
}
/*--------------------------------------------------------------------------*/

All I've done here is:

That's about it.

3. Some words about surface locking

Before you go running off locking every surface you see now, it must first be mentioned that there are some limitations to surface locking (what did you expect?) These are summarized from the DirectX help files.

Note that you can have multiple rectangles on a surface locked at the same time, so long as the rectangles don't overlap.

The DirectX documentation also makes the following warning which I don't fully understand *:

"Copy aligned to display memory. Windows 95 uses a page fault handler, Vflatd.386, to implement a virtual flat-frame buffer for display cards with bank-switched memory. The handler allows these display devices to present a linear frame buffer to DirectDraw. Copying unaligned to display memory can cause the system to suspend operations if the copy spans memory banks."

* Thomas writes: "You wanted to understand why the DirectX documentation mentions that some hardware relies on a specific library for linear buffers and that unaligned data can cause pauses. The answer is somewhat complicated, but considering I managed to squeeze a solid 50+ frames/sec out of a PIII 500 back when I had DJGPP and had to make jumps into and out of protected mode, I have a pretty good idea of what Microsoft is talking about and it makes sense to mention it in MSDN.

Okay, so say you have a chunk of data you want to write out and you want to directly dump it out to the screen. The hardware you are running on is using the page buffering method. Let's say page 0 of the video memory (64K) is mapped to virtual address 0x95310000. Let's also say that the software is doing bitmap sprite drawing and each sprite fits in a grid with a cell size of 20x20 pixels. Finally, the video mode being displayed is 640x480x256 for simplicity's sake.

Drawing the individual sprites directly to memory causes the following problems:
1) Vertical-sync refresh shearing problems (the code ignores the VSync signal from the display controller and draws the graphic anyway...only part of the graphic gets drawn causing weird shearing problems).
2) Poorer response due to extra page swaps and hundreds of extra page faults.

The second one is what Microsoft is referring to.

Time to do some math:
640x480x256 = 640 * 480 bytes = 307200 bytes video ram
Since only one 64K video bank can be loaded at a time:
307200 / 65536 = 4.6875 pages of video ram ~= 5 pages of video ram
Assuming the OS uses 4K pages (Windows tends to):
5 pages of video ram * 65536 / 4096 = 80 4K pages
The OS, however, only needs 16 4K pages (65536 / 4096 = 16) since the others have to be swapped into the buffer. So, the 16 pages are mapped starting at address 0x95310000 and ending on address 0x9531FFFF. The remaining 64 (80 - 16) pages are marked for page faults so that when they are referenced, the OS will swap in the appropriate pages.

Now, pretend that you are drawing sprite row 5 (zero-based). This row starts at address 0x9531FA00. Here is what happens when each row of the sprite is written:

Sprite Row 0) Writes to 0x9531FA00-0x9531FA13 okay.
Sprite Row 1) Writes to 0x9531FC80-0x9531FC93 okay.
Sprite Row 2) Writes to 0x9531FF00-0x9531FF13 okay.
Sprite Row 3) Writes to 0x95320180-0x95320193 Page Fault.

Here is where the OS pauses the process until it can get the next 64K of memory (16 pages). The OS probably overwrites the current 64K since allocating a new chunk would take too long. If this is the case, the OS has to update the mapping table. Before resuming the process, the OS marks the previous 16 pages so that they will page fault on reference (since they are no longer in memory). Finally, the process has to be resumed and execution returned back to the point of failure. All of this makes the video memory look like one big chunk of data to the program, but it really is not. To make this situation worse is when you move to draw the next sprite in the row:

Sprite Row 0) Writes to 0x9531FA14-0x9531FA27 Page Fault.
...
Sprite Row 3) Writes to 0x95320194-0x953201A7 Page Fault.

The OS has to continually swap video memory banks in and out...which is an extremely slow operation. For this reason, Microsoft made the wise decision to have back-buffers and let DirectX handle the issues with video memory banks.

So, as you can see, there is a reason why Microsoft put that documentation into MSDN, but you probably did not want to know how it all worked...but now you know!

Hope this helps!"

[ Home | Chapter 1 | Chapter 2 | Chapter 3 | Chapter 4 | Chapter 5 | Chapter 6 ]
Follow @d_joffe