In the previous tutorial, we looked at creating a window. In this tutorial, we will look at creating a render buffer so that we can actually draw things to the screen.
A render buffer is an array of pixels to be rendered. While the array is one-dimensional, we can set a fixed width and height in a metadata structure so that every time the pixels reach the width of the window, they wrap back around. In this tutorial, we will create one and have it clear to black.
I have decided to switch our width and height constants' names. In the previous tutorial, they were called WINDOW_WIDTH and WINDOW_HEIGHT, but as we now want to work with a buffer, it is more suitable to call them BUFFER_WIDTH and BUFFER_HEIGHT (apologies for the inconvenience). This is important, as in the next tutorial we will be working with resizing the window, so the window width and height will no longer be constant. In this tutorial, we will also move the PeekMessage loop into its own function, but I will explain that in more detail when we get to it. To add to this, we will be storing the window handle inside the new render buffer structure we will be creating, so we will eventually replace "windowHandle" with "renderBuffer.windowHandle". Don't worry if this is confusing now - we will come back to it when we've created all our new structures and functions. Like before, the code for this tutorial can be found at the bottom of this webpage.
To create our render buffer, we will need to create two new structures: a pixel structure and a render buffer structure.
The pixel structure will be made up of 3 unsigned 8-bit integers. The first will be the blue value, then the green value and then the red value. I will use a uint8_t type to define an unsigned 8-bit integer, and therefore I will include the stdint.h header file. You should be able to use an unsigned char as well, if you would rather. Make sure that the blue byte comes first, then the green and finally the red. This may be a little confusing, as we typically think of colours as red-green-blue. This is because the function we will use to send our buffer data to the screen requires pixel data in this format. I am not certain why this is the case, but it may have something to do with Windows being a little endian system (where the most significant byte comes last). Regardless of the reason why, our program will display the wrong colours if we do not do this. I will typedef this, and call our new type "Pixel".
The render buffer is a more complex structure. It should first start with a handle to a device context (a HDC type). A device context is a region of memory used by the OS to determine what should be drawn and how (i.e. it stores pixel data and metadata). Every window will have a device context, and we cannot access it ourselves, so we will need a handle to get the OS to send our image data to said window. The render buffer structure will also store a window handle, which will replace our windowHandle variable in WinMain later on. Next is a Pixel pointer, which I have called "pixels" at the bottom of this page. We will use this to allocate space for our array of pixels. Next is a BITMAPINFO structure. This is a stucture defined by Windows, and contains a lot of important metadata that will be used by the OS to send our buffer data to the window. Remeber that in this context, our buffer is effectively just a bitmap image, so I will be using the terms "bitmap" and "buffer" somewhat interchangeably in this series. Once again I will typedef this structure, this time using the name "RenderBuffer".
Before we can initialise our render buffer, we must create an instance of our RenderBuffer structure. I will create a global variable called renderBuffer. I will use global variables for demonstration purposes throughout these tutorials, but if you would rather pass renderBuffer as a parameter, you could do that instead.
We will initialise our render buffer in our WinMain function. We will start by replacing the "windowHandle" local variable inside WinMain, with renderBuffer.windowHandle, which belongs to our global render buffer variable.
After retrieving our window handle, just before the ShowWindow function, we can allocate memory for our pixels array. We should initialise our pointer to 0, with "renderBuffer.pixels = 0;". We then can use malloc to allocate BUFFER_WIDTH * BUFFER_HEIGHT * sizeof(Pixel) bytes. This is because our buffer is made up of BUFFER_WIDTH * BUFFER_HEIGHT pixels, each made of sizeof(Pixel) bytes. We can do this with the line "renderBuffer.pixels = (Pixel *) malloc(BUFFER_WIDTH * BUFFER_HEIGHT * sizeof(Pixel));". We should then check that we were able to allocate enough memory. We can use a similar if statement to the one we used to check if our window handle created properly (e.g. "if(!renderBuffer.pixels){..."), and can display a similar message box before returning -1.
After this, we can get a handle to the device context of our window using the GetDC function. This takes our window handle as an argument. We can do this with the line "renderBuffer.deviceContextHandle = GetDC(renderBuffer.windowHandle);".
After getting our device context, we will need to fill out our renderBuffer's metadata structure (which I called bitmapInfo). Before we can set it's properties, we should first set all the properties to 0 using "memset(&renderBuffer.bitmapInfo, 0, sizeof(BITMAPINFO));". Then we can start filling our structure in. The BITMAPINFO structure is made up of two parts: a BITMAPINFOHEADER structure and a colour table. We can ignore the colour table - it contains a set of colours and colour codes, but typically only has to be defined for 8-bit and 16-bit numbers. We do need to set the BITMAPINFOHEADER structure, which is called bmiHeader in the actual bitmapInfo structure. The BITMAPINFOHEADER structure contains a lot of metadata we must provide for our buffer. These are as follows:
renderBuffer.bitmapInfo.bmiHeader.cbSize - this is the size of the BITMAPINFOHEADER structure in bytes, and should be set to sizeof(BITMAPINFOHEADER)
renderBuffer.bitmapInfo.bmiHeader.biWidth - this is the width of the buffer in pixels and should be set to BUFFER_WIDTH
renderBuffer.bitmapInfo.bmiHeader.biHeight - this is the height of the buffer in pixels and should be set to BUFFER_HEIGHT
renderBuffer.bitmapInfo.bmiHeader.biPlanes - this is the number of planes we can draw to. It isn't currently possible to have more than one plane in a bitmap/buffer, so this must be set to 1
renderBuffer.bitmapInfo.bmiHeader.biBitCount - this is the number of bits per pixel and should be set to 24 (since we are using 3 bytes per pixel)
renderBuffer.bitmapInfo.bmiHeader.biCompression - this is the type of compression used. We are not using any compression, so we should set this to BI_RGB
After doing this, we have finished setting up our render buffer.
For easier programming, we will move our main loop from our WinMain function to its own function, which I will call mainLoop. This will be a void function and will not take any parameters.
Inside our mainLoop function, we will need to create a while loop, "while(running){...". Inside this loop we want to call two functions. The first is going to be called handleEvents() and the second is going to be called render(). We will create these next.
Our handleEvents function will be a void function and will not take any parameters. We can copy and paste our code that was inside the previous "while(running){" loop here, in case you do not have this, I will recap it briefly.
First, we will need to create a MSG structure, which I will call message. This will store our received message. Then we need to start a while loop, with a PeekMessage function call as its condition. PeekMessage will return 0 when there are no messages to process and non-zero when there are still messages to process, so our loop will repeat until all messages have been received. The PeekMessage function takes five arguments, a pointer to our message structure, the window handle (remember to change this to "renderBuffer.windowHandle" and ensure it is NOT just "windowHandle"), the minimum message ID (set to 0 to override the min-max range), the maximum message ID (set to 0 for the same reason) and the flag PM_REMOVE, which instructs the function to remove the message from the message queue when processed.
This function will carry out two very important tasks - it will first set all of the pixels to 0 (setting red, green and blue to 0 will create the colour black) before sending the pixel data to the window's memory, using the StretchDIBits function.
We can set all of the pixels to black, by using memset to set all of the pixels to 0. We can do this with the line "memset(renderBuffer.pixel, 0, BUFFER_WIDTH * BUFFER_HEIGHT * sizeof(Pixel));".
We can send our buffer data to the window to be rendered with the StretchDIBits function. StretchDIBits takes 13 parameters, which are listed below:
The handle to the device context for the window we wish to draw to. We will pass in renderBuffer.deviceContextHandle here.
The x coordinate of the top-left corner of our buffer in the window client area. We will pass in 0 here, which will draw the buffer so that its top-left corner is in the top-left corner of the window client area.
The y coordinate of the top-left corner of our buffer in the window client area. We will pass in 0 here, which will draw the buffer so that its top-left corner is in the top-left corner of the window client area.
The width of the buffer on screen. StretchDIBits will automatically apply scaling, so we can set this to whatever value we want, but to keep things simple and looking nice we will pass in BUFFER_WIDTH here.
The height of the buffer on screen. StretchDIBits will automatically apply scaling, so we can set this to whatever value we want, but to keep things simple and looking nice we will pass in BUFFER_HEIGHT here.
The x-coordinate of the source buffer to start drawing from. We will pass in 0 here, as we want to draw the whole thing.
The y-coordinate of the source buffer to start drawing from. We will pass in 0 here, as we want to draw the while thing.
A pointer to the actual pixel data to draw. We will pass in renderBuffer.pixels here.
A pointer to the BITMAPINFO structure. We will pass in &renderBuffer.bitmapInfo here.
The format of the data. We will pass in DIB_RGB_COLORS here, as we are using RGB values (even if they are formatted in the reverse order to what we'd expect).
The procedure to be carried out. We will pass in SRCCOPY here, which indicates that the source buffer should be copied into the window's memory.
That finishes the theory for this tutorial. I recommend that you have a go at reading through and writing out the code below to consolidate your learning.
Here is the code required to create a window and a render buffer. You could add the previously mentioned changes to your code from the previous tutorial, but, if you have the time, I would recommend writing the program out from scratch as it may help you understand and memorise the code for this process. Writing it out again may also help you avoid bugs where you may have forgotten to change a section of code.
If you are using MinGW, you can compile this code with "gcc main.c -lgdi32 -o app". If you are using Visual Studio, be sure to link the gdi32 library or else this code will not work! (I believe you can do this using: #pragma comment(lib, "gdi32"))
main.c
Running the program should give an output similar to the window shown below. As you can see, the screen is now black. This is our render buffer, which we clear to black every frame.
If we resize the window, we will see that the buffer does not move, and that the buffer remains the same size. We will look at fixing this in the next tutorial.