Serebyte


Home | Tutorials | Portfolio

Win32 Software Renderer in C: Part 1 - Create a Window With Win32


Note: this tutorial series requires significant knowledge of the C programming language


In this tutorial we will look at perhaps the most important step in creating a software-rendered application: creating a window.




The Theory:

Before we get into writing our code, we first need to cover some basic theory...


As Windows is a closed source operating system, there is a limit to what we can do from scratch within it. Ultimately, window creation is a process handled by the operating system, so we cannot just create a window ourselves. What we can do, though, is communicate to the operating system that we would like a window created via an application programming interface (a set of functions used by one piece of software to communicate with another). In this case, we will be using the Win32 API. It should be ready to use for most Windows computers, but if your code does not work, you may wish to look into downloading the latest version of the Windows SDK (software development kit).




Initialisation

First, we will need to include some headers. We will include "windows.h", which will give us access to almost all of the Win32 functions, as well as the "stdlib.h" and "stdio.h" headers for logging data to the console when debugging. Next, we will define two constants - our window width and window height. I will create a 640x480 window, but you can use whatever size you would like. Note that since we are using software rendering, you will want to choose a smaller size, since larger resolutions will take longer to render. Without the added power of hardware acceleration and the GPU, we may need to make a compromise between our program's resolution and its framerate later on. In my experience, resolutions of 800x600 and smaller run reasonably well, even when there is a lot going on on screen.


Next, we will create a global variable called "running" this will be initialised to 1. Running will have value 1 while our program is running, and will be set to 0 to mark the end of our program loop later on (we'll get to this soon). In the code below, I have also declared a function called "windowProcedure" at the top, but we will look at this function in more depth shortly.




WinMain - The Windows Entry Point

Now we need to create our entry point. In graphical Windows apps, we use the function WinMain as our entry point, since it takes some useful additional parameters. The first parameter is of type HINSTANCE. A HINSTANCE is a "handle to an instance". A handle is simply just a numerical identifier. When using the Win32 API, the OS will create and manage objects and data strucures for us. We use handles (numerical IDs) to interact with these objects and structures. The "instance" part of HINSTANCE refers to the application instance - i.e. the running program. Therefore, the first parameter is a pointer to the application.


The second parameter of WinMain is another HINSTANCE, typically named "hPrevInstance". This is a redundant feature from 16-bit Windows, and no longer servers any purpose. It is included purely for compatibility's sake with older version of Windows. The next parameter is of type LPSTR. This is short for "long pointer to string", and is a list of command line arguments as a single string. This is named "cmd". The last parameter is an integer often called "nCmdShow". It stores a value that represents how the application should be shown. Remember, all of these parameters are passed into WinMain by the operating system, so we do not need to set them.


Note that WinMain returns an integer, like "int main". We typically need to specify the calling convention of WinMain to the WINAPI calling convention. A calling convention is simply just a way of storing data (such as function return addresses, prameters and local variables) on the system's call stack. We set the calling convention by specifying its identifier after the return type and before the function name. Our definition of WinMain should start with the line "int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstace, LPSTR cmd, int nCmdShow){".




Creating a Window Class

A "window class" is a data structure, typically written as "WNDCLASS" or "WNDCLASSEX" (for the extended version) in code, that stores various pieces of data about our window. If you would like to create a window, you must first create a window class. We will create a window class structure (WNDCLASSEX) inside our entry point. Upon initialisation, we should set all the data in this window class to 0, using "WNDCLASSEX windowClass = {0};".


Once our window class structure has been initialised, we should set its properties. First, we must set the "cbSize" property to the size of a window class structure (i.e. "windowClass.cbSize = sizeof(WNDCLASSEX)). cbSize is short for "count bytes size" and should be set to the number of bytes that the structure takes up. Next we must set the property "lpszClassName", which is short for "long pointer to C-string class name". Unfortunately, I cannot tell you why "lpsz" corresponds to "long pointer to C string", but lpszClassName is definitely a string (and as we're using C, it is certainly a C string). The class name is a string identifier used to ideintify our window class. Once we have created our window class, we must send it to the OS (which stores a table of window classes for running applications), and we will need to use the class name to identify it.


The next property to set is the window style. Somewhat confusingly, style refers to how the window behaves, rather than how it looks, in this context. We will set this to two flas, combied with a bitwise OR operation: CS_HREDRAW CS_VREDRAW. CS stands for class style (I think) and H and V stand for horizontal and vertical. These flags state that all windows of this class should redraw when horizontally or vertically resized. The next property we will set is called lpfnWndProc, and stands for "long pointer to function window procedure". The window procedure is the event handler function for our window class, so we should set lpfnWndProc to the window procedure identifier. We will write our window procedure later on in this tutorial, and will call it (somewhat uncreatively) "windowProcedure", so we can write "windowClass.lpfnWndProc = windowProcedure;".


The final property to set is the background color of the window. This property is called hbrBackground, which is short for "handle to brush background". We can use the CreateSolidBrush function to get the OS to create a solid colour brush object. This brush object is simply just a set of data instructing the OS on how to draw something. CreateSolidBrush takes a colour value, which we can use the RGB macro to generate. The RGB macro takes three 8-bit numbers (a red, green and blue colour value) as inputs. CreateSolidBrush returns a handle, so we can simply set hbrBackground to the return value of CreateSolidBrush, although we will likely need to cast it to a HBRUSH first. Overall, this gives us the line: "windowClass.hbrBackground = (HBRUSH) CreateSolidBrush(RGB(100, 100, 100));". I have chosen the colour (100, 100, 100), which is a reasonably dark grey, but you may choose whichever colour you would like.


Once we have set our class properties, we must send our window class to the operating system, so that it can be added to the system's set of window classes for currently running programs. We do this with the RegisterClassEx function, which takes a pointer to our window class (giving the line: "RegisterClassEx(&windowClass)").




Getting the Window Size

When we create a window, we want to set the client area size - this is the area that we can actually draw to, but unfortunately we cannot just do this. What we can do, though, is set the entire size of the window, including the client area (the area we draw to) and the window frame (the outer border of the window, including the title text at the top and any menu bars we may want). Use a function called AdjustWindowRectEx to get the total window size from a given client area size.


The first thing we want to do is create a RECT structure (a rectangle) to store the client area size. The first property of the RECT structure is the leftmost x coordinate, then the top y coordinate, then the rightmost x coordinate and finally the bottom y coordinate. Somewhat confusingly here, the top left is the point (0,0), even though when we come to drawing shapes to our window later on the point (0,0) will be at the bottom left. We can create our window/client rectangle using the line "RECT windowSize = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT};".


Now that our window rectangle has been created, and currently contains the client area, we can pass it through the AdjustWindowRectEx function to get the total size of our window. AdjustWindowRectEx takes four arguments: the first is a pointer to our window rectangle, the second is a window style, the third is whether there will be a menu bar and the fourth is an extended window style (not to be confused with a window class style). We can set the second paramter to WS_OVERLAPPEDWINDOW, the default standard window style (remember the window "style" is really just the behaviour of the window). The third parameter should be set to 0, as we do not want there to be a menu bar. We will set the fourth parameter to the value WS_EX_OVERLAPPEDWINDOW, which is the default extended style for a window (we do not need to worry about window styles that much, as just want a simple window we can draw to - i.e. the default type). This gives the line: "AdjustWindowRectEx(&windowRect, WS_OVERLAPPEDWINDOW, 0, WS_EX_OVERLAPPEDWINDOW);". Our windowRect rectangle now contains the coordinates (0, 0) and (width, height) of our outer window frame.




Creating the Actual Window

Now we have set up the window class and size, we can create the actual window. As previously mentioned, we cannot just create a window, but we can get the OS to create a window and have it send us back the window handle. We can create a HWND (handle to window) variable and set it to our window's handle.


We can have the OS create a window with the CreateWindowEx function. The CreateWindowEx function returns a window handle, and takes multiple parameters. The first parameter is the extended window style, we will set this to the default: WS_EX_OVERLAPPEDWINDOW.


The second parameter is the window class, we will set this to "windowClass.lpszClassName". The third parameter is the window title, this is a string and is the text that will appear in the top left corner. The fourth parameter is the window style. Again, we will set this to the default: WS_OVERLAPPEDWINDOW. Remember that the window style is simply just the basic behaviour of the window. WS_OVERLAPPEDWINDOW is simply just he default behaviour of the window (i.e. resizable, movable, etc.).


The fifth and sixth parameters are the x and y coordinates of the top left corner of the window. We can set these to the default alues, using CW_DEFAULT (where CW stands for create window). The seventh and eightth parameters are the width and height. The width is equal to windowRect.right - windowRect.left and the height is equal to windowRect.bottom - windowRect.top (remember that the top left is the point (0, 0)).


The ninth parameter is the parent window. Our window is not inside another window, so has no parent window. Therefore, NULL can be passed for the ninth parameter. The tenth parameter is the menu bar we would like for our window, we do not want one so can pass in NULL. The eleventh parameter is the application instance that the window belongs to, so we will pass in hInstance (the first WinMain parameter). The twelfth parameter is an LPARAM, and is used to send additional data. We do not need to send additional data, so can set the twelfth parameter to NULL.




Checking For Errors and Showing The Window

We can check that the window failed to create by checking if its window handle is 0. We can do this with the line: "if(!windowHandle){". If the window handle is 0, and our window has failed to create, we can send an alert to our program using a message box.


We can create a message box using the MessageBox function. The first parameter it takes is a parent window - we should set this to NULL as our window has not created. The second is the message box's title text. The third is the text inside the box and the fourth is a set of flags. We will pass the MB_OK flag and the MB_ICONERROR flag, to indicate that the box should have an OK button and should display an error icon. These should be joined with a bitwise OR operation (e.g. "MB_OK | MB_ICONERROR"). Once our message box has finished (it will block the current thread until the user clicks a button on it), we can return -1 to exit out of WinMain.


If our window handle is valid, that will be ignored, and we can show it. We can do that with the ShowWindow function, which takes two parameters. The first is the window handle to show, and the second is an integer, the value of which instructs the OS on how to show the window. We want to pass in our nCmdShow integer (the fourth WinMain parameter) for this.




The Main Loop

We will want our program to run until the exit button is clicked. To accomplish this, we should first start with a while loop that iterates so long as our "running" global variable is non-zero. We could write this as "while(running){".


Within the main loop, we will need to handle our events, or messages as they are called in Windows. There are thousands of different types of messages in Windows, including keyboard inputs, mouse inputs, window resize messages and many more. We want to handle any messages that pop up.


Before we can start handling messags, we have to create a message ("MSG" in code) structure to contain them. We can do this with the line "MSG message;". Next we want to start a while loop to iterate until all messages are handled. The condition of this while loop will be that the function PeekMessage returns non-zero. PeekMessage checks to see if there are any messages (events) on the OS' message queue. If there are messages, a non-zero value will be returned so we can handle said messages and check again. If there are no messages queued up, PeekMessage will return 0, exitting out of our loop.


PeekMessage takes five parameters. The first is a pointer to a message structure to be filled in with the most recent message, we can put "&message" in here. The second is a window handle. We can pass in windowHandle. The third is the minium message ID (this can be set to allow us to only process certain types of messages). We will set this to 0 to process all messages. The fourth is the maximum message ID. We will also set this to 0, as when both the minimum and maximum message IDs are set to 0, the minimum and maximum ranges will be overridden and all messages will be retrieved. The final parameter is the flags parameter. We will pass in the PM_REMOVE flag, which indicates that messages should be removed from the message queue.


Inside our PeekMessage loop, we need to call two functions. The first is TranslateMessage, and takes a pointer to our message structure. This function converts key codes from text-based messages into their respective characters. The second function is DispatchMessage. It also takes a pointer to our message structure. This sends our message to be handled by the window procedure, which we will look into writing next. This ends our WinMain function, so after our while(running) loop, we can return 0 to finish the WinMain function.




The Window Procedure

The window procedure is the message handler function for a window. A window procedure is connected to a window via our window class - you may remember that we set the window class' lpfnWndProc property to windowProcedure.


A window procedure should return an LRESULT - this is simply just a long long int that is typically used to indicate a success or error status. It has the calling convention CALLBACK. We can name our window procedure whatever we would like, so long as we specify the same name in the lpfnWndProc property of the window class. I have chosen the name windowProcedure.


The window procedure takes four parameters. The first parameter is the window handle, which we could call windowHandle. The second parameter is a UINT (unsigned integer) that holds the ID of the message (e.g. WM_DESTROY is the message ID of the window destroy message - i.e. when the X button is clicked). We could call this messageID. The third parameter is a WPARAM, which we could call wParam. This is a 32-bit integer, typically used to store integers or pointers. The fourth parameter is an LPARAM, which is a redefinition of the WPARAM type (although they used to be different on 16-bit Windows) and will hold a different value. We could call this lParam. Both WPARAM and LPARAM are used to pass in data about the event (for example a key press event will need to pass in which key was pressed).


Inside our window procedure function, we will need to check which message we are responding to. We can do this with the line "switch(messageID){". We only need to check for one message in this program, and that is the window destroy message (WM_DESTROY). If this message is received, we should call the PostQuitMessage function. This tells the program to send a WM_QUIT message to the clicked window. This message will be handled automatically and will close the window. PostQuitMessage takes one parameter, the WPARAM of the WM_QUIT message. We will pass in the value 0 here. After calling PostQuitMessage, we want to set our global variable running to 0 to stop the main loop. After this, we want to return 0 out of our window procedure.


There are thousands of different message types, and we do not want to check them all. Therefore, we can call the DefWindowProc function after the switch-case statement. This carries out the built in default window procedure. We want to carry this out for all unhandled events. DefWindowProc takes the same parameters as our window procedure, so we can just pass those parameters in. We also want to return the value returned by DefWindowProc to exit the function with the corresponding message code. This can be done with the line "return DefWindowProc(windowHandle, messageID, wParam, lParam);"


This completes our window procedure and thus our program. The code for this can be found below.




The code:

The code below will create a window. I recommend that you read through the comments thoroughly and take a while to play around with the code to improve your understanding.


I am working on Windows 10 using the MinGW compiler. I am compiling this program with the command: "gcc main.c -lgdi32 -o app". If you are on Visual Studio, this code should work, but you will need to link the gdi32 library some other way (possibly by including "#pragma comment(lib, gdi32)" at the top of your code).


main.c

//Software Renderer in C Part 1 - Creating a Window

//include headers
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>

//define window size constants
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480

//create global variables
int running = 1; //1 when program is running, 0 when program is closed

//declare window procedure (event handler) function
LRESULT CALLBACK windowProcedure(HWND windowHandle, UINT messageID, WPARAM wParam, LPARAM lParam);


/*
 Entry point
 -In Win32, the entry point is WinMain, not main

 -Like main, WinMain returns an int

 -The calling convention of the function is called WINAPI (you do not need to know what this means as it isn't used elsewhere)

 -The calling convention of a function determines how it stores data on the program's call stack

 -Note that all parameters are passed into the function by the OS

 -On Windows, a handle is a numerical identifier used to identify objects and structures controlled by the OS. Handles pop up quite frequently.

 -A HINSTANCE is a handle to an application instance - the first parameter is the numerical identifier for a specific instance of
 the program, given to it by the OS at runtime

 -The second parameter, hPrevInstace is a feature that is now useless. It served a purpose on 16-bit Windows versions, but now
 must simply be included for compatiblity's sake (each version of Windows mostly aims to be compatible with the previous versions)

 -The third parameter is the command line arguments as a single string. LPSTR stands for long pointer to string, and is simply just
 a redefinition of a char * on modern systems.

 -Note that long pointers are also a redundant feature from 16-bit windows, when pointers could either be 16-bit or 32-bit. Nowadays,
 a long pointer and a pointer are the same on Windows.

 -The fourth parameter is an integer used to specify how the program should be displayed. Again, this is given to us by the OS - we
 do not set this, as we do not set any of the parameters of WinMain.
*/

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstace, LPSTR cmd, int nCmdShow){
//create window class structure and initialise to 0
 WNDCLASSEX windowClass = {0};

//set window class properties
//set size of structure
//cbSize is short for "count bytes size" - it is the size of the structure in bytes
 windowClass.cbSize = sizeof(WNDCLASSEX);

//set class name - this is a string used by the operating system to identify our window class
//lpszClassName is short for "long pointer to string class name"
 windowClass.lpszClassName = "MAIN_WINDOW_CLASS";

//set window style - this is the default behaviour of the windows. We are telling the window to redraw when either horizontally or vertically resized (or both)
 windowClass.style = CS_HREDRAW | CS_VREDRAW;

//set window procedure - this is the event handler function, and is declared at the top of the file
//lpfnWndProc is short for "long pointer to function window procedure", and is used to identify the window procedure (event handler function) for this window class
 windowClass.lpfnWndProc = windowProcedure;
  
/*
 set background colour - we set a handle to a colour we create.
 A brush is just a drawing style stored on the OS. Here, we are
 asking the OS to create a solid colour "brush" and return a
 handle to it so that we can use it.
 */

 windowClass.hbrBackground = (HBRUSH) CreateSolidBrush(RGB(100, 100, 100));

//send window class to operating system to be registered
 RegisterClassEx(&windowClass);

//create window size structure
//note that here, we specify the size of the client area, i.e. the area we can draw to
 RECT windowSize = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT};

//get window frame size
//AdjustWindowRectEx takes a client area, and adds the size of the window frame to it
 AdjustWindowRectEx(
  &windowSize, //pointer to client area rectangle to be modified
  WS_OVERLAPPEDWINDOW, //window style(behaviour) - WS_OVERLAPPEDWINDOW is the default
  0, //no menu bar (this is a boolean value)
  WS_EX_OVERLAPPEDWINDOW //extended style (behaviour) - WS_EX_OVERLAPPEDWINDOW is the default
 );

//windowSize now contains the size of the window (including the window frame)

//create window from window class
//a HWND is a window handle
 HWND windowHandle = CreateWindowEx(
  WS_EX_OVERLAPPEDWINDOW, //default extended window style - a basic window that may be overlapped
  windowClass.lpszClassName, //the class name of the window class this window will use
  "My First Window!!!", //the title text of the window
  WS_OVERLAPPEDWINDOW, //default window style - a basic window that may be overlapped
  CW_USEDEFAULT, CW_USEDEFAULT, //the starting x and y coordinates of the window - use default
  windowSize.right - windowSize.left, //width of window frame
  windowSize.bottom - windowSize.top, //height of window frame
  NULL, //no parent window
  NULL, //no menu bar - we will not need one for our window
  hInstance, //application instance that window belongs to
  NULL //no LPARAM (an LPARAM is an additional piece of data in Win32)
 );

//check if window created successfully
 if(!windowHandle){
  //window failed to create - display an alert
  MessageBox(
   NULL, //no parent window - this should display on its own
   "Error - could not create window", //inner text
   "Window Error", //the title text of the message box
   MB_OK | MB_ICONERROR //flags - message box has an OK button, message box has an error icon
  );
  
  return -1;
 };

//show window if window created successfully
//this function takes the window handle and an integer indicating how it should be shown
//this integer is the nCmdShow parameter passed by the OS upon starting our program
 ShowWindow(windowHandle, nCmdShow);

//start program loop
 while(running){
  //create message structure to store incoming event
  //remember "message" is just the Windows name for event
  MSG message;
  
  //start message loop
  /*
   The PeekMessage function takes five parameters
   -The first is a pointer to a message structure to fill out
   -The second is the window to get events for. This can be
   set to NULL to detect all events that occur on the system.
   -The third is the minimum message ID to retrieve
   -The fourth is the maximum message ID to retreive
   -Note that by setting both the minimum and maximum
   message IDs to 0, the PeekMessage call will detect all
   messages and thus override the minimum-maximum range
   setting
   -The fifth is for flags. We want to add the PM_REMOVE
   flag, which will remove our message from the system's
   message queue once processed
   
   Note that PeekMessage will not block, so if no messages are
   present on the system's message queue, the loop simply just ends.
  */

  
  //get all messages
  while(PeekMessage(&message, windowHandle, 0, 0, PM_REMOVE)){
   /*
    Translate the message.
    This involves converting key codes into characters for text-based events
    we shouldn't need it, but it is a good practice to include it anyway,
    as if we decide to use a message requiring translation later and forget
    this line, the resulting errors may be hard to debug
   */

   TranslateMessage(&message);
   
   //dispatch message - send message and its corresponding window to the relevant window procedure
   DispatchMessage(&message);
  };
 };

 return 0;
};


/*
 Window procedure - this is the window procedure, the event handler function.
 It takes four parameters:
 -The handle (numerical ID) to the window calling the function
 -The message ID (indicating what type of event has occurred)
 -wParam, a general purpose parameter used to store event-dependent data
 -lParam, another general purpose parameter

 Note that it is common to see WPARAM and LPARAM pop up in many Win32 functions.
 Remember that these are just general purpose parameters, and typically
 contain integer values or pointers

 Note that an LRESULT is just a "long long int" type.
*/

LRESULT CALLBACK windowProcedure(HWND windowHandle, UINT messageID, WPARAM wParam, LPARAM lParam){
//check event type/ID
 switch(messageID){
  //window is closed (i.e. X button is clicked)
  case WM_CLOSE:{
   //send quit message to close window
   PostQuitMessage(0);
   
   //set running to 0 to close program
   running = 0;
   return 0;
  };
 };

/*
  There are thousands of different types of message
  on Windows. We cannot check for them all, so we
  can simply call the default window procedure on all
  our parameters for the vast majority of events
 */


//return default window procedure
 return DefWindowProc(windowHandle, messageID, wParam, lParam);
};



Running the Program:

Running the program should give an output similar to the window shown below. The window should close when you click the quit button.