Serebyte


Home | Tutorials | Portfolio

Win32 Software Renderer in C: Part 2 - Create a Render Buffer


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


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.




The Theory:

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.




Some Small Changes

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.




Defining New Structures

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.




Initialising our Render Buffer

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:


After doing this, we have finished setting up our render buffer.




The mainLoop Function

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.




The handlEvents Function

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.




The render Function

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:


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.




The code:

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

//Software Renderer in C Part 2 - Creating a RenderBuffer

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

//define window size constants
#define BUFFER_WIDTH 640
#define BUFFER_HEIGHT 480

//create pixel structure
//this is a 24-bit structure that stores a red, green and blue byte
typedef struct Pixel {
 uint8_t blue;
 uint8_t green;
 uint8_t red;
} Pixel;

//define render buffer structure
typedef struct RenderBuffer {
 HWND windowHandle; //handle to the window that the buffer belongs to
 HDC deviceContextHandle; //handle to device context (a device context is an area of memory stored by the OS that will be rendered directly to the window client area)
 Pixel * pixels; //pixel buffer to render to the screen
 BITMAPINFO bitmapInfo; //a bitmap info structure that stores necessary metadata for our renderBuffer (required to draw our data to the screen)
} RenderBuffer;

//create global variables
int running = 1; //1 when program is running, 0 when program is closed
RenderBuffer renderBuffer; //our global render buffer

//main loop function
void mainLoop();

//handle events function
void handleEvents();

//render function
void render();

//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, BUFFER_WIDTH, BUFFER_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
 renderBuffer.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(!renderBuffer.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;
 };

//allocate memory for render buffer pixels
 renderBuffer.pixels = (Pixel *) malloc(BUFFER_WIDTH * BUFFER_HEIGHT * sizeof(Pixel));

//check if memory could not be allocated - terminate program if so
 if(!renderBuffer.pixels){
  //failed to allocate memory for render buffer
  MessageBox(
   NULL, //no parent window - this should display on its own
   "Error - could not allocate memory for render buffer", //inner text
   "Render Buffer 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;
 };

/*
  Get window device context and set renderBuffer context to it.
  The window's device context is the region of memory that is rendered to it.
  This is managed by the OS, so we need to retrieve and handle to it. We do this
  so that we can use the StretchDIBits function to send our buffer data to said region
  of memory, as we cannot simply write to the region ourselves.
 */

 renderBuffer.deviceContextHandle = GetDC(renderBuffer.windowHandle);

//set all bitmap info properties to 0
 memset(&renderBuffer.bitmapInfo.bmiHeader, 0 , sizeof(BITMAPINFOHEADER));

/*
  Fill out bitmapinfo structure for renderBuffer.
  This is necessary so that when we wish to send our data to the window's device context
  the OS knows how to interpret our data.
  
  Remeber that our buffer is really just a large bitmap.
  
  The BITMAPINFO structure is made up of two parts: the bitmap info header and the colour
  table. The bitmap info header contains all of the metadata we must set before we can
  draw our buffer data to the screen. We do not need to worry about the colour table, as
  it is mostly only used for defining colour codes for 16-bit or 8-bit colour palettes.
 */

 renderBuffer.bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); //the size of the BITMAPINFOHEADER structure in bytes
 renderBuffer.bitmapInfo.bmiHeader.biWidth = BUFFER_WIDTH; //the width of our buffer in pixels
 renderBuffer.bitmapInfo.bmiHeader.biHeight = BUFFER_HEIGHT; //the height of our buffer in pixels
 renderBuffer.bitmapInfo.bmiHeader.biPlanes = 1; //this is the number of planes to render - this has to be set to 1
 renderBuffer.bitmapInfo.bmiHeader.biBitCount = 24; //we are using 24-bit colours
 renderBuffer.bitmapInfo.bmiHeader.biCompression = BI_RGB; //uncompressed - we are simply using 3 bytes for the RGB values of each pixel

//there are other properties of the BITMAPINFOHEADER structure, but we can set these all to 0 as they are not relevant to us


//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(renderBuffer.windowHandle, nCmdShow);

//start program loop
 mainLoop();

 return 0;
};


/*
 mainLoop
 -The job of this function is to iterate until the "running" global variable is set to 0
 -Each iteration, it should handle any events and render the current data to the screen
*/

void mainLoop(){
//iterate while window is running
 while(running){
  //handle events for global render buffer
  handleEvents();

  //render to global render buffer
  render();
 };
};

/*
 handleEvents
 -The job of this function is to handle all events for the window that the global render buffer belongs to
 -All messages on the message queue are processed by using the PeekMessage function in a while loop
*/

void handleEvents(){
//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, renderBuffer.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);
 };
};

//render function
void render(){
//set all pixels to 0 red, 0 blue, 0 green
 memset(renderBuffer.pixels, 0, BUFFER_WIDTH * BUFFER_HEIGHT * sizeof(Pixel));

/*
  Send renderbuffer data to client area of window.
  We can do this with the StretchDIBits function.
  This takes many parameters, which are detailed below:
 */

 StretchDIBits(
  renderBuffer.deviceContextHandle, //a handle to the device context we wish to render to
  0, //the x coordinate of the top left coordinate of our buffer on the window client area
  0, //the y coordinate of the top left coordinate of our buffer on the window client area
  BUFFER_WIDTH, //the width of the buffer on the window client area
  BUFFER_HEIGHT, //the height of the buffer on the window client area
  0, //the starting x coordinate on the source buffer to render from (we want to render all data, so this is 0)
  0, //the starting y coordinate on the source buffer to render from (we want to render all data, so this is 0)
  BUFFER_WIDTH, //the width of the source buffer
  BUFFER_HEIGHT, //the height of the source buffer
  renderBuffer.pixels, //a pointer to the actual data we want to send
  &renderBuffer.bitmapInfo, //a pointer to the bitmap info structure for our renderBuffer
  DIB_RGB_COLORS, //use RGB colours
  SRCCOPY //copy the source into the window's buffer
 );
};

/*
 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. 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.