Win32 Assembly Part 2
by Chris Hobbs
Where Did We Leave Off?
The last article discussed many basics of Win32 ASM programming, introduced you to the game we will be creating, and guided you through the design process. Now it is time to take it a few steps further. First, I will cover, in depth, the High Level constructs of MASM that make it extremely readable ( at generally no performance cost ), and make it as easy to write as C expressions. Then, once we have a solid foundation in our assembler we will take a look at the Game Loop and the main Windows procedures in the code. With that out of the way we will take a peek at Direct Draw and the calls associated with it. Once, we understand how DirectX works we can build our Direct Draw library. After that we will build our bitmap file library. Finally, we will put it all together in a program that displays our Loading Game screen and exits when you hit the escape key.
It is a pretty tall order but I am pretty sure we can cover all of the topics in this article. Remember: If you want to compile the code you need the MASM32 package, or at the very least a copy of MASM 6.11+.
If you are already familiar with MASM's HL syntax then I would suggest skipping the next section. However, those of you who are rusty, or have never even heard of it, head on to the next section. There you will learn more than you will probably ever need to know about this totally cool addition to our assembler.
MASM's HL Syntax
I am sure many of you have seen an old DOS assembly language listing. Take a moment to recall that listing, and picture the code. Scary? Well, 9 times out of 10 it was scary. Most ASM programmers wrote very unreadable code, simply because that was the nature of their assembler. It was littered with labels and jmp's, and all sorts of other mysterious things. Try stepping through it with your mental computer. Did you crash? Yeah, don't feel bad. It is just how it is. Now, that was the 9 out of 10 ... what about that 1 out of 10? What is the deal with them? Well, those are the programmers who coded MACRO's to facilitate High Level constructs in their programs. For once, Microsoft did something incredibly useful with MASM 6.0 ... they built those HL MACRO's, that smart programmers had devised, into MASM as pseudo-ops.
If you aren't aware of what this means I will let you in on it. MASM's assembly code is now just as readable and easy to write as C. This, of course, is just my opinion. But, it is an opinion shared by thousands and thousands of ASM coders. So, now that I have touted its usefulness let's take a look at some C constructs and their MASM counterparts.
IF - ELSE IF - ELSE
The C version:
if ( var1 == var2 )
{
// Code goes here
}
else
if ( var1 == var3 )
{
// Code goes here
}
else
{
// Code goes here
}
|
The MASM version:
.if ( var1 == var2 )
; Code goes here
.elseif ( var1 == var3 )
; Code goes here
.else
; Code goes here
.endif
|
DO - WHILE
The C version:
do
{
// Code goes here
}
while ( var1 == var2 );
|
The MASM version:
.repeat
; Code goes here
.until ( var1 != var2 )
|
WHILE
The C version:
while ( var1 == var2 )
{
// Code goes here
}
|
The MASM version:
.while ( var1 == var2 )
; Code goes here
.endw
|
Those are the constructs that we can use in our code. As you can see they are extremely simple and allow for nice readable code. Something assembly language has long been without. There is no performance loss for using these constructs, at least I haven't found any. They typically generate the same jmp and cmp code that a programmer would if he were writing it with labels and such. So, feel free to use them in your code as you see fit ... they are a great asset.
There is one other thing we should discuss and that is the psuedo-ops that allow us to define procedures/functions easily. PROTO and PROC. Using them is really simple. To begin with, just as in C you need to have a prototype. In MASM this is done with the PROTO keyword. Here are some examples of declaring protoypes for your procedures:
;==================================
; Main Program Procedures
;==================================
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
WndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD
The above code tells the assembler it should expect a procedure by the name of WinMain and one by the name of WndProc. Each of these has a parameter list associated with them. They both happen to expect 4 DWORD values to be passed to them. For those of you using the MASM32 package, you already have all of the Windows API functions prototyped, you just need to include the appropriate include file. But, you need to make sure that any user defined procedure is prototyped in the above fashion.
Once we have the function prototyped we can create it. We do this with the PROC keyword. Here is an example:
;########################################################################
; WinMain Function
;########################################################################
WinMain PROC hInstance :DWORD,
hPrevInst :DWORD,
CmdLine :DWORD,
CmdShow :DWORD
;===========================
; We are through
;===========================
return msg.wParam
WinMain endp
;########################################################################
; End of WinMain Procedure
;########################################################################
By writing our functions in this manner we can access all passed parameters by the name we give to them. The above function is WinMain w/o any code in it. You will see the code in a minute. For now though, pay attention to how we setup the procedure. Also notice how it allows us to create much cleaner looking code, just like the rest of the high level constructs in MASM do also.
Getting A Game Loop Running
Now that we all know how to use our assembler, and the features contained in it, lets get a basic game shell up and running.
The first thing we need to do is get setup to enter into WinMain(). You may be wondering why the code doesn't start at WinMain() like in C/C++. The answer is: in C/C++ it doesn't start there either. The code that we will write is generated for you by the compiler, therefore it is completely transparent to you. We will most likely do it differently than the compiler, but the premise will be the same. So here is what we will code to get into the WinMain() function...
.CODE
start:
;==================================
; Obtain the instance for the
; application
;==================================
INVOKE GetModuleHandle, NULL
MOV hInst, EAX
;==================================
; Is there a commandline to parse?
;==================================
INVOKE GetCommandLine
MOV CommandLine, EAX
;==================================
; Call the WinMain procedure
;==================================
INVOKE WinMain,hInst,NULL,CommandLine,SW_SHOWDEFAULT
;==================================
; Leave the program
;==================================
INVOKE ExitProcess,EAX
The only thing that may seem a little confusing is why we MOV EAX into a variable at the end of a INVOKE. The reason is all Windows functions, and C functions for that matter, place the return value of a function/procedure in EAX. So we are effectively doing an assignment statement with a function when we move a value from EAX into something. This code above is going to be the same for every Windows application that you write. At least, I have never had need to change it. The code simply sets everything up and ends it when we are finished.
If you follow the code you will see that it calls WinMain() for us. This is where things can get a bit confusing ... so let's have a look at the code first.
;########################################################################
; WinMain Function
;########################################################################
WinMain PROC hInstance :DWORD,
hPrevInst :DWORD,
CmdLine :DWORD,
CmdShow :DWORD
;====================
; Put LOCALs on stack
;====================
LOCAL wc :WNDCLASS
;==================================================
; Fill WNDCLASS structure with required variables
;==================================================
MOV wc.style, CS_OWNDC
MOV wc.lpfnWndProc,OFFSET WndProc
MOV wc.cbClsExtra,NULL
MOV wc.cbWndExtra,NULL
m2m wc.hInstance,hInst ;<< NOTE: macro not mnemonic
INVOKE GetStockObject, BLACK_BRUSH
MOV wc.hbrBackground, EAX
MOV wc.lpszMenuName,NULL
MOV wc.lpszClassName,OFFSET szClassName
INVOKE LoadIcon, hInst, IDI_ICON ; icon ID
MOV wc.hIcon,EAX
INVOKE LoadCursor,NULL,IDC_ARROW
MOV wc.hCursor,EAX
;================================
; Register our class we created
;================================
INVOKE RegisterClass, ADDR wc
;===========================================
; Create the main screen
;===========================================
INVOKE CreateWindowEx,NULL,
ADDR szClassName,
ADDR szDisplayName,
WS_POPUP OR WS_CLIPSIBLINGS OR \
WS_MAXIMIZE OR WS_CLIPCHILDREN,
0,0,640,480,
NULL,NULL,
hInst,NULL
;===========================================
; Put the window handle in for future uses
;===========================================
MOV hMainWnd, EAX
;====================================
; Hide the cursor
;====================================
INVOKE ShowCursor, FALSE
;===========================================
; Display our Window we created for now
;===========================================
INVOKE ShowWindow, hMainWnd, SW_SHOWDEFAULT
;=================================
; Intialize the Game
;=================================
INVOKE Game_Init
;========================================
; Check for an error if so leave
;========================================
.IF EAX != TRUE
JMP shutdown
.ENDIF
;===================================
; Loop until PostQuitMessage is sent
;===================================
.WHILE TRUE
INVOKE PeekMessage, ADDR msg, NULL, 0, 0, PM_REMOVE
.IF (EAX != 0)
;===================================
; Break if it was the quit messge
;===================================
MOV EAX, msg.message
.IF EAX == WM_QUIT
;======================
; Break out
;======================
JMP shutdown
.ENDIF
;===================================
; Translate and Dispatch the message
;===================================
INVOKE TranslateMessage, ADDR msg
INVOKE DispatchMessage, ADDR msg
.ENDIF
;================================
; Call our Main Game Loop
;
; NOTE: This is done every loop
; iteration no matter what
;================================
INVOKE Game_Main
.ENDW
shutdown:
;=================================
; Shutdown the Game
;=================================
INVOKE Game_Shutdown
;=================================
; Show the Cursor
;=================================
INVOKE ShowCursor, TRUE
getout:
;===========================
; We are through
;===========================
return msg.wParam
WinMain endp
;########################################################################
; End of WinMain Procedure
;########################################################################
This is quite a bit of code and is rather daunting at first glance. But, let's examine it a piece at a time. First we enter the function, notice that the local variables ( in this case a WNDCLASS variable ) get placed on the stack without your having to code anything. The code is generated for you ... you can declare local variables like in C. Thus, at the end of the procedure we don't need to tell the assembler how much to pop off of the stack ... it is done for us also. Then, we fill in this structure with various values and variables. Note the use of m2m. This is because in ASM you are not allowed to move a memory value to another memory location w/o placing it in a register, or on the stack first.
Next, we make some calls to register our window class and create a new window. Then, we hide the cursor. You may want the cursor ... but for our game we do not. Now we can show our window and try to initialize our game. We check for an error after calling the Game_Init() procedure. If there was an error the function would not return true and this would cause our program to jump to the shutdown label. It is important that we jump over the main message loop. If we do not, the program will continue executing. Also, make sure that you do not just return out of the code ... there still may be some things that need to be shutdown. It is good practice in ASM, just as in all other languages, to have one entry point and one exit point in each of your procedures -- this makes debugging easier.
Now for the meat of WinMain(): the message loop. For those of you that have never seen a Windows message loop before here is a quick explanation. Windows maintains a queue of messages that the application receives -- whether from other applications, user generated, or internal. In order to do ANYTHING an application must process messages. These tell you that a key has been pressed, the mouse button clicked, or the user wants to exit your program. If this were a normal program, and not a high performance game, we would use GetMessage() to retrieve a message from the queue and act upon it.
The problem however is, if there are no messages, the function WAITS until it receives one. This is totally unacceptable for a game. We need to be constantly performing our loop, no matter what messages we receive. So, one way around this, is to use PeekMessage() instead. PeekMessage() will return zero if it has no messages, otherwise it will grab it off of the queue.
What this means is, if we have a message, it will get translated and dispatched to our callback function. Furthermore, if we do not, then the main game loop will be called instead. Now here is the trick, by arranging the code just right, the main game loop will be called -- even if we process a message. If we did not do this, then Windows could process 1,000's of messages while our game loop wouldn't execute once!
Finally, when a quit message is passed to the queue we will jump out of our loop and execute the shutdown code. And that ... is the basic game loop.
Connecting to Direct Draw
Now we are going to get a little bit advanced. But, only for this section. Unfortunately there is no cut and dry way to view DirectX in assembly. So, I am going to explain it briefly, show you how to use it, and then forget about it. This is not that imperative to know about, but it helps if you at least understand the concepts.
The very first thing you need to understand is the concept of a Virtual Function Table. This is where your call really goes to be blunt about it. The call offsets into this table, and from it selects the proper function address to jump to. What this means to you is your call to a function is actually a call to a simple look-up table that is already generated. in this way, DirectX or any other type library such as DirectX can change functions in a library w/o you ever having to know about it.
Once we have gotten that straight we can figure out how to make calls in DirectX. Have you guessed how yet? The answer is we need to mimic the table in some way so that our call is offset into the virtual table at the proper address. We start by simply having a base address that gets called, which is a given in DirectX libraries. Then we make a list of all functions for that object appending the size of their parameters. This is our offset into the table. Now, we are all set to call the functions.
Calling these functions can be a bit of work. First you have to specify the address of the object that you want to make the call on. Then, you have to resolve the virtual address, and then, finally, push all of the parameters onto the stack, including the object, for the call. Ugly isn't it? For that reason there is a set of macros provided that will allow you to make calls for these objects fairly easily. I will only cover one since the rest are based on the same premise. The most basic one is DD4INVOKE. This macro is for a Direct Draw 4 object. It is important that we have different invokes for different versions of the same object. If we did not, then wrong routines would be called since the Virtual Table changes as they add/remove functions from the lib's.
The idea behind the macro is fairly simple. First, you specify the function name, then the object name, and then the parameters. Here is an example:
;========================================
; Now create the primary surface
;========================================
DD4INVOKE CreateSurface, lpdd, ADDR ddsd, ADDR lpddsprimary, NULL
The above line of code calls the CreateSurface() function on a Direct Draw 4 object. It passes the pointer to the object, the address of a Direct Draw Surface Describe structure, the address of the variable to hold the pointer to the surface, and finally NULL. This call is an example of how we will interface to DirectX in this article series. Now that we have seen how to make calls to DirectX, we need to build a small library for us to use which we cover in the next section.
Our Direct Draw Library
Alright, we are now ready to start coding our Direct Draw library routines. So, the logical starting place would be figuring out what kinds of routines we will need for the game. Obviously we want an initialization and shutdown routine, and we are going to need a function to lock and unlock surfaces. Also, it would be nice to have a function to draw text, and, since the game is going to run in 16 bpp mode, we will want a function that can figure out the pixel format for us. It would also be a good idea to have a function that creates surfaces, loads a bitmap into a surface, and a function to flip our buffers for us. That should cover it ... so lets get started.
The first routine that we will look at is the initialization routine. This is the most logical place to start, especially since the routine has just about every type of call we will be using in Direct Draw. Here is the code:
;########################################################################
; DD_Init Procedure
;########################################################################
DD_Init PROC screen_width:DWORD, screen_height:DWORD, screen_bpp:DWORD
;=======================================================
; This function will setup DD to full screen exclusive
; mode at the passed in width, height, and bpp
;=======================================================
;=================================
; Local Variables
;=================================
LOCAL lpdd_1 :LPDIRECTDRAW
;=============================
; Create a default object
;=============================
INVOKE DirectDrawCreate, 0, ADDR lpdd_1, 0
;=============================
; Test for an error
;=============================
.IF EAX != DD_OK
;======================
; Give err msg
;======================
INVOKE MessageBox, hMainWnd, ADDR szNoDD, NULL, MB_OK
;======================
; Jump and return out
;======================
JMP err
.ENDIF
;=========================================
; Lets try and get a DirectDraw 4 object
;=========================================
DDINVOKE QueryInterface, lpdd_1, ADDR IID_IDirectDraw4, ADDR lpdd
;=========================================
; Did we get it??
;=========================================
.IF EAX != DD_OK
;==============================
; No so give err message
;==============================
INVOKE MessageBox, hMainWnd, ADDR szNoDD4, NULL, MB_OK
;======================
; Jump and return out
;======================
JMP err
.ENDIF
;===================================================
; Set the cooperative level
;===================================================
DD4INVOKE SetCooperativeLevel, lpdd, hMainWnd, \
DDSCL_ALLOWMODEX OR DDSCL_FULLSCREEN OR \
DDSCL_EXCLUSIVE OR DDSCL_ALLOWREBOOT
;=========================================
; Did we get it??
;=========================================
.IF EAX != DD_OK
;==============================
; No so give err message
;==============================
INVOKE MessageBox, hMainWnd, ADDR szNoCoop, NULL, MB_OK
;======================
; Jump and return out
;======================
JMP err
.ENDIF
;===================================================
; Set the Display Mode
;===================================================
DD4INVOKE SetDisplayMode, lpdd, screen_width, \
screen_height, screen_bpp, 0, 0
;=========================================
; Did we get it??
;=========================================
.IF EAX != DD_OK
;==============================
; No so give err message
;==============================
INVOKE MessageBox, hMainWnd, ADDR szNoDisplay, NULL, MB_OK
;======================
; Jump and return out
;======================
JMP err
.ENDIF
;================================
; Save the screen info
;================================
m2m app_width, screen_width
m2m app_height, screen_height
m2m app_bpp, screen_bpp
;========================================
; Setup to create the primary surface
;========================================
DDINITSTRUCT OFFSET ddsd, SIZEOF(DDSURFACEDESC2)
MOV ddsd.dwSize, SIZEOF(DDSURFACEDESC2)
MOV ddsd.dwFlags, DDSD_CAPS OR DDSD_BACKBUFFERCOUNT;
MOV ddsd.ddsCaps.dwCaps, DDSCAPS_PRIMARYSURFACE OR \
DDSCAPS_FLIP OR DDSCAPS_COMPLEX
MOV ddsd.dwBackBufferCount, 1
;========================================
; Now create the primary surface
;========================================
DD4INVOKE CreateSurface, lpdd, ADDR ddsd, ADDR lpddsprimary, NULL
;=========================================
; Did we get it??
;=========================================
.IF EAX != DD_OK
;==============================
; No so give err message
;==============================
INVOKE MessageBox, hMainWnd, ADDR szNoPrimary, NULL, MB_OK
;======================
; Jump and return out
;======================
JMP err
.ENDIF
;==========================================
; Try to get a backbuffer
;==========================================
MOV ddscaps.dwCaps, DDSCAPS_BACKBUFFER
DDS4INVOKE GetAttachedSurface, lpddsprimary, ADDR ddscaps, ADDR lpddsback
;=========================================
; Did we get it??
;=========================================
.IF EAX != DD_OK
;==============================
; No so give err message
;==============================
INVOKE MessageBox, hMainWnd, ADDR szNoBackBuffer, NULL, MB_OK
;======================
; Jump and return out
;======================
JMP err
.ENDIF
;==========================================
; Get the RGB format of the surface
;==========================================
INVOKE DD_Get_RGB_Format, lpddsprimary
done:
;===================
; We completed
;===================
return TRUE
err:
;===================
; We didn't make it
;===================
return FALSE
DD_Init ENDP
;########################################################################
; END DD_Init
;########################################################################
The above code is fairly complex so let's see what each individual section does.
The first step is we create a default Direct Draw object. This is nothing more than a simple call with a couple of parameters. NOTE: since it is NOT based on an already created object, the function is not virtual. Therefore, we can call it like a normal function using invoke. Also, notice how we check for an error right afterwards. This is very important in DirectX. In the case of an error, we merely give a message, and then jump to the error return at the bottom of the procedure.
The second step is we query for a DirectDraw4 object. We will almost always want the newest version of the objects, and querying after you have the base object is the way to get them. If this succeeds we then set the cooperative level and the display mode for our game. Nothing major ... but don't forget to check for errors.
Our next step is to create a primary surface for the object that we have. If that succeeds we create the back buffer. The structure that we use in this call, and other DirectX calls, needs to be cleared before using it. This is done in a macro, DDINITSTRUCT, that I have included in the DDraw.inc file.
The final thing we do is make a call to our routine that determines the pixel format for our surfaces. All of these pieces fit together into initializing our system for use.
The next routine we will look at is the pixel format obtainer. This is a fairly advanced routine so I wanted to make sure that we cover it. Here is the code:
;########################################################################
; DD_Get_RGB_Format Procedure
;########################################################################
DD_Get_RGB_Format PROC surface:DWORD
;=========================================================
; This function will setup some globals to give us info
; on whether the pixel format of the current diaplay mode
;=========================================================
;====================================
; Local variables
;====================================
LOCAL shiftcount :BYTE
;================================
; get a surface despriction
;================================
DDINITSTRUCT ADDR ddsd, sizeof(DDSURFACEDESC2)
MOV ddsd.dwSize, sizeof(DDSURFACEDESC2)
MOV ddsd.dwFlags, DDSD_PIXELFORMAT
DDS4INVOKE GetSurfaceDesc, surface, ADDR ddsd
;==============================
; fill in masking values
;==============================
m2m mRed, ddsd.ddpfPixelFormat.dwRBitMask ; Red Mask
m2m mGreen, ddsd.ddpfPixelFormat.dwGBitMask ; Green Mask
m2m mBlue, ddsd.ddpfPixelFormat.dwBBitMask ; Blue Mask
;====================================
; Determine the pos for the red mask
;====================================
MOV shiftcount, 0
.WHILE (!(ddsd.ddpfPixelFormat.dwRBitMask & 1))
SHR ddsd.ddpfPixelFormat.dwRBitMask, 1
INC shiftcount
.ENDW
MOV AL, shiftcount
MOV pRed, AL
;=======================================
; Determine the pos for the green mask
;=======================================
MOV shiftcount, 0
.WHILE (!(ddsd.ddpfPixelFormat.dwGBitMask & 1))
SHR ddsd.ddpfPixelFormat.dwGBitMask, 1
INC shiftcount
.ENDW
MOV AL, shiftcount
MOV pGreen, AL
;=======================================
; Determine the pos for the blue mask
;=======================================
MOV shiftcount, 0
.WHILE (!(ddsd.ddpfPixelFormat.dwBBitMask & 1))
SHR ddsd.ddpfPixelFormat.dwBBitMask, 1
INC shiftcount
.ENDW
MOV AL, shiftcount
MOV pBlue, AL
;===========================================
; Set a special var if we are in 16 bit mode
;===========================================
.IF app_bpp == 16
.IF pRed == 10
MOV Is_555, TRUE
.ELSE
MOV Is_555, FALSE
.ENDIF
.ENDIF
done:
;===================
; We completed
;===================
return TRUE
DD_Get_RGB_Format ENDP
;########################################################################
; END DD_Get_RGB_Format
;########################################################################
First, we initialize our description structure and make a call to get the surface description from Direct Draw. We place the masks that are returned in global variables, since we will want to use them in all kinds of places. A mask is a value that you can use to set or clear certain bits in a variable/register. In our case, we use them to mask off the unnecessary bits so that we can access the red, green, or blue bits of our pixel individually.
The next three sections of code are used to determine the number of bits in each color component. For example, if we had set the mode to 24 bpp, then there would be 8-bits in every component. The way we determine the number of bits it needs to be moved is by shifting each mask to the right by 1 and AND'ing it with the number one. This allows us to effectively count all the bits we need to shift by in order to move our component into its proper position. This works because the mask is going to contain a 1 where the bits are valid. So, by AND'ing it with the 1 we are able to see if the bit was turned on or not, since the number one will leave only the first bit set and turn all others off.
Finally, we set a variable that tells us whether or not the video mode is 5-5-5 or 5-6-5. This is extremely important since 16 bpp mode can be either, and we do not want our pictures to have a green or purple tint on one machine, and look fine on another one!
The last function that I want to cover in our Direct Draw library is the text drawing function. This uses GDI and so I figured I should at least give it a small explanation. The code ...
;########################################################################
; DD_Draw_Text Procedure
;########################################################################
DD_Draw_Text PROC surface:DWORD, text:DWORD, num_chars:DWORD,
x:DWORD, y:DWORD, color:DWORD
;=======================================================
; This function will draw the passed text on the passed
; surface using the passed color at the passed coords
; with GDI
;=======================================================
;===========================================
; First we need to get a DC for the surface
;===========================================
DDS4INVOKE GetDC, surface, ADDR hDC
;===========================================
; Set the text color and BK mode
;===========================================
INVOKE SetTextColor, hDC, color
INVOKE SetBkMode, hDC, TRANSPARENT
;===========================================
; Write out the text at the desired location
;===========================================
INVOKE TextOut, hDC, x, y, text, num_chars
;===========================================
; release the DC we obtained
;===========================================
DDS4INVOKE ReleaseDC, surface, hDC
done:
;===================
; We completed
;===================
return TRUE
DD_Draw_Text ENDP
;########################################################################
; END DD_Draw_Text
;########################################################################
Following this code is relatively simple. First, we get the Device Context for our surface. In Windows, drawing is typically done through these DC's ( Device Contexts ), thus ... if you want to use any GDI function in Direct Draw the first thing you have to do is get the DC for your surface. Then, we set the background mode and text color using basic Windows GDI calls. Now, we are ready to draw our text ... again we just make a call to the Windows function TextOut(). There are many others, this is just the one that I chose to use. Finally, we release the DC for our surface.
The rest of the Direct Draw routines follow the same basic format and use the same types of calls, so they shouldn't be too hard to figure out. The basic idea behind all of the routines is the same: encapsulate the functionality we need into some services that still allow us to be flexible. Now, we need to write the code to handle our bitmaps that go into these surfaces.
Our Bitmap Library
We are now ready to write our bitmap library. We will start like the Direct Draw library by determining what we need. As far as I can tell right now, we should be good with two simple routines: a bitmap loader, and a draw routine. Since we will be using surfaces, the draw routine should draw onto the passed surface. Our loader will load our special file format which I will cover in a moment. That should be it, there isn't that much that is needed for bitmaps nowadays. DirectX is how most manipulation occurs, especially since many things can be done in hardware. With that in mind we will cover our unique file format.
Normally, creating your own file format is a headache and isn't worth the trouble. However, in our case it greatly simplifies the code and I have provided the conversion utility with the download package. This format is probably one of the easiest you will ever encounter. It has five main parts: Width, Height, BPP, Size of Buffer, and Buffer. The first three give information on the image. I have our library setup for 16 bpp only but implementing other bit depths would be fairly easy. The fourth section tells us how large of a buffer we need for the image, and the fifth section is that buffer. Having our own format not only makes the code we need to write a lot easier, it also prevents other people from seeing our work before they were meant to see it! Now, how do we load this bad boy?
;########################################################################
; Create_From_SFP Procedure
;########################################################################
Create_From_SFP PROC ptr_BMP:DWORD, sfp_file:DWORD, desired_bpp:DWORD
;=========================================================
; This function will allocate our bitmap structure and
; will load the bitmap from an SFP file. Converting if
; it is needed based on the passed value.
;=========================================================
;=================================
; Local Variables
;=================================
LOCAL hFile :DWORD
LOCAL hSFP :DWORD
LOCAL Img_Left :DWORD
LOCAL Img_Alias :DWORD
LOCAL red :DWORD
LOCAL green :DWORD
LOCAL blue :DWORD
LOCAL Dest_Alias :DWORD
;=================================
; Create the SFP file
;=================================
INVOKE CreateFile, sfp_file, GENERIC_READ,FILE_SHARE_READ, \
NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL
MOV hFile, EAX
;===============================
; Test for an error
;===============================
.IF EAX == INVALID_HANDLE_VALUE
JMP err
.ENDIF
;===============================
; Get the file size
;===============================
INVOKE GetFileSize, hFile, NULL
PUSH EAX
;================================
; test for an error
;================================
.IF EAX == -1
JMP err
.ENDIF
;==============================================
; Allocate enough memeory to hold the file
;==============================================
INVOKE GlobalAlloc, GMEM_FIXED, EAX
MOV hSFP, EAX
;===================================
; test for an error
;===================================
.IF EAX == 0
JMP err
.ENDIF
;===================================
; Put the file into memory
;===================================
POP EAX
INVOKE ReadFile, hFile, hSFP, EAX, OFFSET Amount_Read, NULL
;====================================
; Test for an error
;====================================
.IF EAX == FALSE
;========================
; We failed so leave
;========================
JMP err
.ENDIF
;===================================
; Determine the size without the BPP
;===================================
MOV EBX, hSFP
MOV EAX, DWORD PTR [EBX]
ADD EBX, 4
MOV ECX, DWORD PTR [EBX]
MUL ECX
PUSH EAX
;======================================
; Do we allocate a 16 or 24 bit buffer
;======================================
.IF desired_bpp == 16
;============================
; Just allocate a 16-bit
;============================
POP EAX
SHL EAX, 1
INVOKE GlobalAlloc, GMEM_FIXED, EAX
MOV EBX, ptr_BMP
MOV DWORD PTR [EBX], EAX
MOV Dest_Alias, EAX
;====================================
; Test for an error
;====================================
.IF EAX == FALSE
;========================
; We failed so leave
;========================
JMP err
.ENDIF
.ELSE
;========================================
; This is where code for 24 bit would go
;========================================
;============================
; For now just return an err
;============================
JMP err
.ENDIF
;====================================
; Setup for reading in
;====================================
MOV EBX, hSFP
ADD EBX, 10
MOV EAX, DWORD PTR[EBX]
MOV Img_Left, EAX
ADD EBX, 4
MOV Img_Alias, EBX
;====================================
; Now lets start converting values
;====================================
.WHILE Img_Left > 0
;==================================
; Build a color word based on
; the desired BPP or transfer
;==================================
.IF desired_bpp == 16
;==========================================
; Read in a byte for blue, green and red
;==========================================
XOR ECX, ECX
MOV EBX, Img_Alias
MOV CL, BYTE PTR [EBX]
MOV blue, ECX
INC EBX
MOV CL, BYTE PTR [EBX]
MOV green, ECX
INC EBX
MOV CL, BYTE PTR [EBX]
MOV red, ECX
;=======================
; Adjust the Img_Alias
;=======================
ADD Img_Alias, 3
;================================
; Do we build a 555 or a 565 val
;================================
.IF Is_555 == TRUE
;============================
; Build the 555 color word
;============================
RGB16BIT_555 red, green, blue
.ELSE
;============================
; Build the 565 color word
;============================
RGB16BIT_565 red, green, blue
.ENDIF
;================================
; Transer it to the final buffer
;================================
MOV EBX, Dest_Alias
MOV WORD PTR [EBX], AX
;============================
; Adjust the dest by 2
;============================
ADD Dest_Alias, 2
.ELSE
;========================================
; This is where code for 24 bit would go
;========================================
;============================
; For now just return an err
;============================
JMP err
.ENDIF
;=====================
; Sub amount left by 3
;=====================
SUB Img_Left, 3
.ENDW
;====================================
; Free the SFP Memory
;====================================
INVOKE GlobalFree, hSFP
done:
;===================
; We completed
;===================
return TRUE
err:
;====================================
; Free the SFP Memory
;====================================
INVOKE GlobalFree, hSFP
;===================
; We didn't make it
;===================
return FALSE
Create_From_SFP ENDP
;########################################################################
; END Create_From_SFP
;########################################################################
The code starts out by creating the file, which, in Windows, is how you open it, and then retrieves the file size. This allows us to allocate enough memory to load our entire file in. The process of reading in the file is fairly simple we just make a call. As usual the most important parts are those that check for errors.
Once the file is in memory we compute the size of the desired image based upon the width and height in our header, and the "desired_bpp" level that was passed in to the function. Then we allocate yet another buffer with the information we calculated. This is the buffer that is kept in the end.
The next step is the heart of our load function. Here we read in 3 bytes, since our pictures are stored as 24-bit images, and create the proper color value ( 5-6-5 or 5-5-5 ) for the buffer. We then store that value in the new buffer that we just created. We loop through all pixels in our bitmap and convert each to the desired format. The conversion is based on a pre-defined macro. You could also implement the function by using the members we filled, when we called the function to get the pixel format. This second way would allow you to have a more abstract interface to the code ... but for our purposes it was better to see what was really happening to the bits.
At the completion of our loop we free the main buffer and return the address of the buffer with our converted pixel values. If an error occurs at any point, we jump to our error code which frees the possible buffer we could have created. This is to prevent memory leaks. And ... that is it for the load function.
Once the bitmap is loaded into memory we need to be able to draw it onto a Direct Draw surface. Whether we are loading it in there permanently, or just drawing a quick picture onto the back buffer should not matter. So, we will look at a function that draws the passed bitmap onto our passed surface. Here is the code:
;########################################################################
; Draw_Bitmap Procedure
;########################################################################
Draw_Bitmap PROC surface:DWORD, bmp_buffer:DWORD, lPitch:DWORD, bpp:DWORD
;=========================================================
; This function will draw the BMP on the surface.
; the surface must be locked before the call.
;
; It uses the width and height of the screen to do so.
; I hardcoded this in just 'cause ... okay.
;
; This routine does not do transparency!
;=========================================================
;===========================
; Local Variables
;===========================
LOCAL dest_addr :DWORD
LOCAL source_addr :DWORD
;===========================
; Init the addresses
;===========================
MOV EAX, surface
MOV EBX, bmp_buffer
MOV dest_addr, EAX
MOV source_addr, EBX
;===========================
; Init counter with height
;
; Hard-coded in.
;===========================
MOV EDX, 480
;=================================
; We are in 16 bit mode
;=================================
copy_loop1:
;=============================
; Setup num of bytes in width
;
; Hard-coded also.
;
; 640*2/4 = 320.
;=============================
MOV ECX, 320
;=============================
; Set source and dest
;=============================
MOV EDI, dest_addr
MOV ESI, source_addr
;======================================
; Move by DWORDS
;======================================
REP movsd
;==============================
; Adjust the variables
;==============================
MOV EAX, lPitch
MOV EBX, 1280
ADD dest_addr, EAX
ADD source_addr, EBX
;========================
; Dec the line counter
;========================
DEC EDX
;========================
; Did we hit bottom?
;========================
JNE copy_loop1
done:
;===================
; We completed
;===================
return TRUE
err:
;===================
; We didn't make it
;===================
return FALSE
Draw_Bitmap ENDP
;########################################################################
; END Draw_Bitmap
;########################################################################
This function is a little bit more advanced than some of the others we have seen, so pay attention. We know, as assembly programmers, that if we can get everything into a register things will be faster than if we had to access memory. So, in that spirit, we place the starting source and destination addresses into registers.
Then, we compute the number of WORDS in our line. We can then divide this number by 2, so that we have the number of DWORDS in a line. I have hard-coded this number in since we will always be in 640 x 480 x 16 for our game. Once we have this number we place it in the register ECX. The reason for this is our next instruction MOVSD can be combined with the REP label. This will move a DWORD, decrement ECX by 1, compare ECX to ZERO if not equal then MOVE A DWORD, etc. until ECX is equal to zero. In short it is like having a For loop with the counter in ECX. As we have the code right now, it is moving a DWORD from the source into the destination until we have exhausted the number of DWORDS in our line. At which point it does this over again until we have reached the number of lines in our height ( 480 in our case ).
Those are our only two functions in the bitmap module. They are short and sweet. More importantly, now that we have our bitmap and Direct Draw routines coded we can write the code to display our loading game screen!
A Game ... Well, Kinda'
The library routines are complete and we are now ready to plunge into our game code. We will start out by looking at the game initialization function since it is called first in our code.
;########################################################################
; Game_Init Procedure
;########################################################################
Game_Init PROC
;=========================================================
; This function will setup the game
;=========================================================
;============================================
; Initialize Direct Draw -- 640, 480, bpp
;============================================
INVOKE DD_Init, 640, 480, screen_bpp
;====================================
; Test for an error
;====================================
.IF EAX == FALSE
;========================
; We failed so leave
;========================
JMP err
.ENDIF
;======================================
; Read in the bitmap and create buffer
;======================================
INVOKE Create_From_SFP, ADDR ptr_BMP_LOAD, ADDR szLoading, screen_bpp
;====================================
; Test for an error
;====================================
.IF EAX == FALSE
;========================
; We failed so leave
;========================
JMP err
.ENDIF
;===================================
; Lock the DirectDraw back buffer
;===================================
INVOKE DD_Lock_Surface, lpddsback, ADDR lPitch
;============================
; Check for an error
;============================
.IF EAX == FALSE
;===================
; Jump to err
;===================
JMP err
.ENDIF
;===================================
; Draw the bitmap onto the surface
;===================================
INVOKE Draw_Bitmap, EAX, ptr_BMP_LOAD, lPitch, screen_bpp
;===================================
; Unlock the back buffer
;===================================
INVOKE DD_Unlock_Surface, lpddsback
;============================
; Check for an error
;============================
.IF EAX == FALSE
;===================
; Jump to err
;===================
JMP err
.ENDIF
;=====================================
; Everything okay so flip displayed
; surfaces and make loading visible
;======================================
INVOKE DD_Flip
;============================
; Check for an error
;============================
.IF EAX == FALSE
;===================
; Jump to err
;===================
JMP err
.ENDIF
done:
;===================
; We completed
;===================
return TRUE
err:
;===================
; We didn't make it
;===================
return FALSE
Game_Init ENDP
;########################################################################
; END Game_Init
;########################################################################
This function plays the most important part in our game so far. In this routine we make the call to initialize Direct Draw. If this succeeds we load in our "Loading Game " bitmap file from disk. After that we lock the back buffer. This is very important to do since we will be accessing the memory directly. After it is locked we can draw our bitmap onto the surface and then unlock it. The final call in our procedure is to flip the buffers. Since we have the bitmap on the back buffer, we need it to be visible. Therefore, we exchange the buffers. The front goes to the back and the back goes to the front. At the completion of this call our bitmap is now visible on screen. One thing that may be confusing here is why we didn't load the bitmap into a Direct Draw surface. The reason is we will only be using it once so there was no need to waste a surface.
Next on our list of things to code is the Windows callback function itself. This function is how we handle messages in Windows. Anytime we want to handle a message the code will go in this function. Take a look at how we have it setup currently.
;########################################################################
; Main Window Callback Procedure -- WndProc
;########################################################################
WndProc PROC hWin :DWORD,
uMsg :DWORD,
wParam :DWORD,
lParam :DWORD
.IF uMsg == WM_COMMAND
;===========================
; We don't have a menu, but
; if we did this is where it
; would go!
;===========================
.ELSEIF uMsg == WM_KEYDOWN
;=======================================
; Since we don't have a Direct input
; system coded yet we will just check
; for escape to be pressed
;=======================================
MOV EAX, wParam
.IF EAX == VK_ESCAPE
;===========================
; Kill the application
;===========================
INVOKE PostQuitMessage,NULL
.ENDIF
;==========================
; We processed it
;==========================
return 0
.ELSEIF uMsg == WM_DESTROY
;===========================
; Kill the application
;===========================
INVOKE PostQuitMessage,NULL
return 0
.ENDIF
;=================================================
; Let the default procedure handle the message
;=================================================
INVOKE DefWindowProc,hWin,uMsg,wParam,lParam
RET
WndProc endp
;########################################################################
; End of Main Windows Callback Procedure
;########################################################################
The code is fairly self-explanatory. So far we only deal with 2 messages the WM_KEYDOWN message and the WM_DESTROY message. We process the WM_KEYDOWN message so that the user can hit escape and exit our game. We will be coding a Direct Input system, but until then we needed a way to quit the game! The one thing you should notice is that any messages we do not deal with are handled by the "default" processing function -- DefWindowProc(). This function is defined by Windows already. You just need to call it whenever you do not handle a message.
The game main function we aren't going to look at, simply because it is empty. We haven't added any solid code to our game loop yet. But, everything is prepared so that next time we can get to it. That then leaves us with the shutdown code.
;########################################################################
; Game_Shutdown Procedure
;########################################################################
Game_Shutdown PROC
;============================================================
; This shuts our game down and frees memory we allocated
;============================================================
;===========================
; Shutdown DirectDraw
;===========================
INVOKE DD_ShutDown
;==========================
; Free the bitmap memory
;==========================
INVOKE GlobalFree, ptr_BMP_LOAD
done:
;===================
; We completed
;===================
return TRUE
err:
;===================
; We didn't make it
;===================
return FALSE
Game_Shutdown ENDP
;########################################################################
; END Game_Shutdown
;########################################################################
Here we make the call to shutdown our Direct Draw library, and we also free the memory we allocated earlier for the bitmap. We could have freed the memory elsewhere and maybe next issue we will. But, things are a bit easier to understand when all of your initialization and cleanup code is in one place.
As you can see there isn't that much code in our game specific stuff. The majority resides in our modules, such as Direct Draw. This allows us to keep our code clean and any changes we may need to make later on a much easier since things aren't hard-coded inline. Anyway, the end result of what you have just seen is a Loading screen that is displayed until the user hits the escape key. And that ... primitive though it may be ... is our game thus far.
Until Next Time ...
We covered a lot of material in this article. We now have a bitmap library, and a Direct Draw library for our game. These are core modules that you should be able to use in any game. By breaking up the code like this we are able to keep our game code separate from the library code. You do not want any module to be dependent on another module.
In the next article we will be continuing our module development with Direct Input. We will also be creating our menu system next time. These two things should keep us busy. So, that is what you have to look forward to in the next installment.
Once again young grasshoppers, until next time ... happy coding.
Get the complete source for the game here: Game.zip
Discuss this article in the forums
Date this article was posted to GameDev.net: 9/23/1999
(Note that this date does not necessarily correspond to the date the article was written)
See Also:
Win32 Assembly
© 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
Comments? Questions? Feedback? Click here!
|