Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

DirectX Tutorial Part I: DirectX Dialog Template for fast pixelwise drawing to DirectX surfaces

0.00/5 (No votes)
16 May 2006 1  
CDirectXDialog is a base class for dialog classes in which you want to use DirectX.

Introduction

This is the first part of my article series on DirectX programming: working titled "Where do we come from?". I will give you a brief overview of some techniques which were used back in times where maybe some blueprints of DirectX existed on Mr. Gates' work bench. This will give us a better understanding about the wondrous things under the hood of DirectX and modern graphics hardware. This tutorial consists of six parts, with two major examples of how to use the code provided with this tutorial.

  • DirectX tutorial part I: DirectX Dialog Template
  • DirectX tutorial part II: Using the CDirectXDialog class in a pinball game
  • DirectX tutorial part III: Basic 3D mathematics
  • DirectX tutorial part IV: Using the Vector class in a Vectorballs scroller
  • DirectX tutorial part V: Lambert and Gouraud polygon fillers from scratch
  • DirectX tutorial part VI: Doing it the Direct3D way...

At the end of this tutorial, you should have a basic knowledge about 3D mathematics, polygon fillers, and about how easy it is to do all these kind of things with DirectX. But first, let us begin our expedition with a glimpse at the DirectX capabilities for fast accessing the surface memory. This will allow us to emulate VGA like drawing to the video memory (remember 0xA000). Our Lambert and Gouraud polygon fillers will only use this functionality to manipulate pixels on screen.

To give you an idea of how fast this pixel routine is, I decided to calculate a mathematical figure (see the screenshot above) which is based on the function, f(x,y) = (x2+y2)2. As you can see, this function is symmetrical to both axis, so it is sufficient to calculate only one quarter and to flip the values to the other ones. But doing this will not prevent us from drawing all of the pixels from any quarter. If you maximize the dialog on an average resolution, this means that you have to draw around 780000 pixels. On my current hardware configuration (Pentium IV, 3GHz,...), this only takes about 72ms. By the way, I've done this maybe 15 years ago on my first computer (it was an Amiga, and only had 7.14MHz). For the default resolution of 320x256=81920pixels, printing the whole image took around 5 minutes (I've done all of the programming in MC6800x0 assembly those times). For hardware enthusiasts, let's calculate the increase of speed: (780000 / 72ms) / (81920 / (5*60000ms)) = 39672.85!!! I think this value is amazing, isn't it?

Requirements for compilation

You will have to download and install the Microsoft DirectX SDK for compiling the sample projects provided by this tutorial. If the compiler cannot find the appropriate DirectX headers, please take a look at your project settings. You will have to specify where exactly the common includes are located. In my special case, I have installed the DirectX SDK in C:\DXSDK. Therefore, the additional include path of my project settings is C:\DXSDK\Samples\C++\Common\Include.

Direct Surface Access

The IDirectDrawSurface7 interface provides a method for obtaining DirectX surface memory for directly manipulating its pixels. The method is called Lock, and you will have to take care that you will release the lock when your drawing to the surface memory is finished. The appropriate release method is named Unlock. While your application has a lock on the surface memory, no other DirectX unit may have access to it, not even the graphics hardware itself. Therefore, you shouldn't use this method in a real DirectX application, because of the overall pure performance. But in our case, this is not very important. At the end, we are just interested in a straightforward implementation of a pixel based direct drawing routine.

HRESULT Lock(LPRECT lpDestRect, 
        LPDDSURFACEDESC2 lpDDSurfaceDesc, 
        DWORD dwFlags, HANDLE hEvent );
HRESULT UnLock(LPRECT lpRect);

When you obtain access to the surface memory, the DirectX runtime will return information about that surface in a structure called DDSURFACEDESC2. The memory layout of a DirectX surface can vary according to your system's display settings. Therefore, we will have to scrutinize this structure to understand which bytes correspond to which pixel on screen. The surface memory can be divided into a visible and a non-visible area. The width and height of the visible area are determined by the attributes dwWidth and dwHeight of the DDSURFACEDESC2 structure. I am not quite sure why there is a non-visible part, but I suppose that it has something to do with horizontal scrolling. By changing the starting address of the surface, you can manipulate which part of the memory is used for each scan line. The effect is that pixels from the non-visible area become visible, which looks as if you are scrolling the surface.

Now, to change the pixels on the surface, we must map the screen coordinates to positions within the surface memory. This mapping can be described through a linear mathematical function:

memory_position = start_address + x * bytes_per_pixel + y * bytes_per_line

Every pixel uses some bits in memory, according to your system's display settings. To determine how many bytes are actually occupied by a pixel (variable bytes_per_pixel), we can look at the dwRGBBitCount attribute of the DDPIXELFORMAT structure. The variable bytes_per_line corresponds to the lPitch attribute of the DDSURFACEDESC2 structure. But calculating the memory position of a specific pixel is not enough to set the right color for it. To do this, we also have to check which bits belong to which color channel. In 32 bit mode, the implementation is very straightforward. In this case, you can directly put the RGB color into the surface memory, because each pixel is four bytes wide. But in other cases, the mapping is a little more complicated. In 16 bit mode, for instance, each pixel only occupies two bytes of surface memory. This means that, each color channel has less than 8 bits for its representation (only 5 bits to be exact). Therefore, we will have to mask and shift our RGB values to the corresponding pixel format. The only piece of information we need to accomplish this task is the bit mask and bit position of each color channel. The corresponding attributes are dwRBitMask, dwGBitMask, and dwBBitMask of the DDPIXELFORMAT structure.

The table below gives you the values of these attributes for the 16 bit mode. As formerly stated, the masks in this mode are 5 bits wide, where the blue color channel occupies the foremost bits and the red color channel the uppermost.

Values of the color channel bit mask

Attribute Value
dwRBitMask 01111100 00000000
dwGBitMask 00000011 11100000
dwBBitMask 00000000 00011111

To determine the number of bits in the bit mask, we constantly have to reduce the bit mask by one and apply a logical AND operation. The following operation of the CDirectXDialog class performs this calculation.

int CDirectXDialog::getNumberOfBits(DWORD mask)
{
    int nob = 0;
    while (mask)
    {
        mask = mask & (mask - 1);
        nob++;
    }
    return nob;
}

Finally, the starting bit of the bit mask can be identified by constantly applying a logical AND operation to the bit mask and a variable bit which is shifted from right to left commencing with the foremost bit. For the sake of completeness, here is the operation for the determination of this starting bit position.

int CDirectXDialog::getBitMaskPosition(DWORD mask)
{
    int pos = 0;
    while (!(mask & 1 << pos)) pos++;
    return pos;
}

Manipulating pixels

The DirectX surface access is completely wrapped by the class CDirectXDialog. When you want to access the surface memory, you have to call the operation BackbufferLock() of this class. After you have finished drawing, you will have to call BackbufferUnlock(). It is very crucial to call these operations pair-wise, that I decided to make them private. Therefore, you cannot directly access these operations. You rather have to use a worker object, which is responsible to lock and unlock the surface. This worker object class is a friend class of CDirectXDialog, and is called CDirectXLockGuard.

The first issue which is handled in BackbufferLock() is the lock of the DirectX surface. Afterwards, the surface is examined to determine the bit mask and bit position of each color channel and, of course, the pitch values. The actual setPixel operation of CDirectXDialog can be accessed through a member function pointer. In BackbufferLock(), this function pointer is initialized according to the current set display mode. The concrete functions for setting a pixel in the DirectX surface are called CDirectXDialog::setPixelOPTIMIZED and CDirectXDialog::setPixelSECURE. The optimized version only copies the RGB value to the corresponding memory position. The secure version also masks and shifts the RGB color value to the right pixel format, thus being much slower as the optimized one.

inline void CDirectXDialog::setPixelOPTIMIZED(int x, int y, DWORD color)
{
    *(unsigned int*)(backbuffervideodata + 
               x*x_pitch + y*y_pitch) = color;
}

inline void CDirectXDialog::setPixelSECURE(int x, int y, DWORD color)
{
    int offset = x*x_pitch + y*y_pitch;
    DWORD Pixel = *(LPDWORD)((DWORD)backbuffervideodata + offset);

    Pixel = (Pixel & ~ sDesc.ddpfPixelFormat.dwRBitMask) | 
            ((RGB_GETRED(color) >> (8 - rbits)) << rpos);
    Pixel = (Pixel & ~ sDesc.ddpfPixelFormat.dwGBitMask) | 
            ((RGB_GETGREEN(color) >> (8 - gbits)) << gpos);
    Pixel = (Pixel & ~ sDesc.ddpfPixelFormat.dwBBitMask) | 
            ((RGB_GETBLUE(color) >> (8 - bbits)) << bpos);

    *(unsigned int*)(backbuffervideodata + offset) = Pixel;
}

The CDirectXDialog class

CDirectXDialog is an abstract class. Therefore, you will have to subclass it and overwrite the pure virtual function displayFrame(). This operation is called by the framework each time the dialog has to be redrawn. The circle image of this example is drawn in the displayFrame() operation of the CDlgBackgroundArtDecoDlg class. Other important and over-writeable operations are.

  • initDirectDraw()

    Called by the framework when the dialog is initialized. You can create and initialize other DirectX resources like other surfaces here.

  • restoreSurfaces()

    Called by the framework when an exception has occurred and the DirectX drawing surface is lost. You will have to re-create and initialize your DirectX resources here.

  • freeDirectXResources()

    Called by the framework when the dialog is going to destroyed. You can release your created resources here.

Drawing the image

The displayed image is calculated and drawn in the displayFrame() operation of the CDlgBackgroundArtDecoDlg class. I wanted to examine the speed benefits between a C++ and a straight assembler implementation. Therefore, you can choose which one to activate. If the preprocessor variable IS_IT_WORTH_IT is defined, then the drawing is accomplished by the x86 assembler routine. Otherwise, it is done by the C++ implementation. By the way, I am using a member function pointer to call the setPixel member function from within an assember code fragment. Refer to my other article, if you want to know more about this topic (How to invoke C++ member operations from inline-assembler code segments).

void CDlgBackgroundArtDecoDlg::displayFrame()
{
    CRect rect; GetClientRect(rect);

    int width = rect.Width() / 2;
    int height = rect.Height() / 2;

    DWORD starttime,stoptime;


    starttime = GetTickCount();
    {
        g_pDisplay.Clear();
        CDirectXLockGuard lock(this);

#if defined(IS_IT_WORTH_IT)

        setPixelPTR _setPixel = setPixel;
        int x, y, c, z = zoom;
        _asm
        {
            mov edx, width
loop1:        mov ebx, height
loop2:        mov eax, edx;    //color = (x*x+y*y)

            imul eax, eax;
            mov ecx, ebx;
            imul ecx, ecx;
            add eax, ecx;
            imul eax, eax;    //color = color*color;


            mov ecx, z;        //zoom

            sar eax, cl;

            and eax, 0xFF;
            mov cl, al;
            and cl, 0x80;    //if (color >= 128) color = color - 127;

            jz weiter;
            xor eax, 0x7F;

weiter:        shl eax, 8+1;
            
            //Backup

            mov x, edx;
            mov y, ebx;
            mov c, eax;
                        
            push eax;        //color            

            mov eax, ebx;    //y

            add eax, height;
            push eax;            
            mov eax, edx;    //x

            add eax, width;
            push eax;            
            mov ecx, this;    //this-call of member function pointer

            call _setPixel;

            mov eax, c;        //color            

            push eax;
            mov eax, y;        //y

            add eax, height;
            push eax;            
            mov eax, width;    //x

            sub eax, x;
            push eax;            
            mov ecx, this;    //this-call of member function pointer

            call _setPixel;

            mov eax, c;        //color            

            push eax;
            mov eax, height;//y

            sub eax, y;
            push eax;            
            mov eax, width;    //x

            sub eax, x;
            push eax;            
            mov ecx, this;    //this-call of member function pointer

            call _setPixel;

            mov eax, c;        //color            

            push eax;
            mov eax, height;//y

            sub eax, y;
            push eax;            
            mov eax, x;        //x

            add eax, width;
            push eax;            
            mov ecx, this;    //this-call of member function pointer

            call _setPixel;


            //Reload

            mov edx, x;
            mov ebx, y;            

            sub ebx, 1
            jge loop2
            sub edx, 1
            jge loop1        
        }
#else        
        for (long x = 0; x < width; x++)
            for (long y = 0; y < height; y++)
            {
                long g = x*x + y*y;
                g = g * g;                
                g = g >> zoom;            
                g = g & 0xFf;
                if (g & 0x80) // if (g > 0x7f) g = 0x7f - g;

                    g = 0x7f ^ g;
                g = g << 1;                
                (this->*setPixel)(width + x, height + y,RGBA_MAKE(g,0,0,0));
                (this->*setPixel)(width - x, height + y,RGBA_MAKE(0,g,0,0));
                (this->*setPixel)(width + x, height - y,RGBA_MAKE(0,0,g,0));
                (this->*setPixel)(width - x, height - y,RGBA_MAKE(g,g,g,0));
            }        
#endif
    }
    stoptime = GetTickCount();

    char buffer[128];
    sprintf(buffer, "time: %4dms", stoptime - starttime);
    g_pTextSurface->DrawText(NULL, buffer, 0, 0, RGB(0,0,0), RGB(255,255,0));
    g_pDisplay.Blt(20, 20, g_pTextSurface, NULL);

    CDialog::OnPaint();
}

What is left to be said?

Some might say, that this article is kind of confusing, because on the one hand, I want to explain techniques which are older than DirectX, on the other hand, I am writing about DirectX. But let me assure you, this is all we need to know about DirectX. The only reason I am using DirectX is, that it is fast, and the pixel drawing routine I presented here corresponds to those in the DOS VGA mode. I could have used Microsoft's GDI to implement the pixel drawing routine, and frankly, I have had a version which was based on GDI. But it was too slow to create fast polygon fillers with it. Therefore, I decided that this is a good tradeoff. Hope that you still find these articles helpful and interesting.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here