(Win) 003 - Allocate a Back Buffer for the Game
Table of Contents
The main goal of this post is to teach you how to allocate a back buffer for rendering graphics on the screen by software rendereing.
There are two main steps.
- Allocate memory where the pixels persist.
- Swap the back buffer to allow users to visualize the pixels on the screen (we call it blit).
Before we start, I want to add some definitions for our code. As mentioned before, the static
keyword could have different meanings depending on where you put it.
So, It's ok to use a preprocessor #define
to define a "new" keyword if you want.
You could declare:
#define internal static // for functions #define local_persist static // for local variable that persists #define global static // for global scope that initialize everything with zero.
In my project, I don't use these definitions. I prefer static
itself because it forces me to remember each situation.
Allocate Memory
We need a place to persist the memory data and the bitmap information which is a struct for configuring the bitmap.
We can send a specific width and height or retrieve the current window size by GetClientRect.
For now, I send a specific size that I'll mention it later.
Anyway, create a new function for allocate the back buffer. In that function, I ensure that there is only one allocation. I mean, if I call it again, firstly free the previous memory and allocate a new one. The rule is important if you want to work with window resize and reallocate a new back buffer.
Let's see the code:
static void* data; static BITMAPINFO bitmap_info;
static void create_back_buffer(int width, int height) { if (data) { if (!VirtualFree(data, 0, MEM_RELEASE)) { // TODO: Log Error GetLastError() OutputDebugStringA("Failed to release memory address!"); } } BITMAPINFOHEADER bmi_header = {}; bmi_header.biSize = sizeof(BITMAPINFOHEADER); bmi_header.biWidth = width; bmi_header.biHeight = -height; // top-down bmi_header.biPlanes = 1; bmi_header.biBitCount = 32; bmi_header.biCompression = BI_RGB; int bytes_per_pixel = 4; size_t buffer_size = width * height * bytes_per_pixel; data = VirtualAlloc(NULL, buffer_size, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); if (!data) { // TODO: Log Error GetLastError() OutputDebugStringA("Failed to allocate memory address!"); } bitmap_info.bmiHeader = bmi_header; }
The key point of this function is the bmi_header
that defines how our back buffer works.
The biHeight
is negative because I want the zero corner to start at upper-left, not bottom-left.
The buffer array consists in 4 bytes (32 bits) for each pixel. I mean, 255 bits for each channel (RGBA).
I recommend that you to follow the MSDN Documentation for more details about the BITMAPINFOHEADER properties.
Now, we've allocated the buffer array using VirtualAlloc
and VirtualFree
functions.
It's a Windows way to allocate and free memory, similar to mmap or munmap/free used in systems based on unix.
Stretch Device Independent Bits
The next step is to create another function responsible for swapping the back buffer and presents pixels on the screen. In the other words, make the blit.
Let's call it update_window
.
static void update_window(HDC context, int destWidth, int destHeight, int srcWidth, int srcHeight) { if (StretchDIBits(context, 0, 0, destWidth, destHeight, 0, 0, srcWidth, srcHeight, data, &bitmap_info, DIB_RGB_COLORS, SRCCOPY) == GDI_ERROR) { // TODO: Log Error OutputDebugStringA("Failed to Update the Back Buffer to the Window"); } }
This functions is very simple. We pass the pointer of memory data, the source and destination rectangle and finally, the BITMAPINFO.
Put it Together
Go back to the main function and create the back buffer after create a window.
#define WIDTH 960 #define HEIGHT 540 create_back_buffer(WIDTH, HEIGHT);
Next, go to the main loop, and after process the message queue, update the window.
As you can see, our function update_window
requires a device context. To get the device context call GetDC
(and ReleaseDC
).
Let's see the code:
// get the current device context HDC context = GetDC(window); while(!should_quit) { process_message_queue(); // copy the whole data memory to the screen update_window(context, 960, 540, WIDTH, HEIGHT); } // release the current device context if (!ReleaseDC(window, context)) { // TODO: Log Error OutputDebugStringA("Failed to Release DC"); }
Probably you'll see a black screen.
Draw Pixels
To draw some pixels, let's set values to the memory.
Right after allocate the memory, iterate the whole buffer array and populate some data into the buffer.
if (!data) { // TODO: Log Error GetLastError() OutputDebugStringA("Failed to allocate memory address!"); } int pitch = width * bytes_per_pixel; unsigned char *row = (unsigned char *) data; for (int y = 0; y < height; ++y) { unsigned int *pixel = (unsigned int *) row; for (int x = 0; x < width; ++x) { *pixel = 0xFFFF00FF; pixel++; } row += pitch; }
Finally, add Gdi32.lib
flag to the linker at build.bat file.
set STATIC_LIB=user32.lib gdi32.lib cl %COMPILE_FLAGS% ..\src\win32_main.cpp /link %LINKER_FLAGS% %STATIC_LIB%
In the next post, I'll delve into the pixels and moviment to ensure that this code works.
Important
If you handle the event WM_PAINT inside window callback procedure, you must process the BeginPaint OR not added it. Otherwise, the PeekMessage always returns a value to the MSG (15 = 0x000F) and keep in infinite loop. So, you need to tell to GDI about it.
Conclusion
To draw pixels on the screen we need to call StretchDIBits that is responsable for copying the bytes memory allocated to the window.
Define the StretchDIBits with the rectangle source (bitmap), rectangle destination, buffer array and the info that describes how the bitmap should be rendered.
Also, the StretchDIBits requires a device context that can be obtained by GetDC function. The device context must be released with ReleaseDC after using it.
This function will be called every frame, after processing the queue messages.
The buffer array (back buffer) has been allocated and deallocated with VirtualAlloc and VirtualFree. As mentioned before, the bitmap info should be set.
The biHeight property should have a special attention. We need to set it with a negative value for rendering top-down. Otherwise, the pixels will be rendered bottom-up. bmi.biHeight = -height
.
The buffer size is formed by width * height * bytes_per_pixel(4 bytes)
.
Iterate the whole buffer array and populate some data into the buffer.
Allocate this buffer at the beginning of the program (after create window) and call the update window after process message queue.
The next step is How to Animate Raw Pixels on the Screen.
Review
To draw pixels on the screen we need to call StretchDIBits that is responsable for copy the bytes from allocated memory to the window.
Define the StretchDIBits with the rectangle source (bitmap), rectangle destination, buffer array and the info that describes how the bitmap should be rendered. Also, the StretchDIBits requires a device context that can be obtained by GetDC function. By the way, the device context must be released with ReleaseDC after using it.
This function will be called every frame, after processing the queue messages.
The buffer array (back buffer) has been allocated and deallocated with VirtualAlloc and VirtualFree. As mentioned before, the bitmap info should be set.
Allocate this buffer at the beginning of the program (after create window).
The biHeight property should have a special attention. We need to set it with a negative value for rendering top-down. Otherwise, the pixels will be rendered bottom-up. bmi.biHeight = -height
.
The buffer size is formed by width * height * bytes_per_pixel(4 bytes)
.
Iterate the whole buffer array and populate some data into the buffer.
Now, the window proc handle WM_PAINT must be processed with BeginPaint OR not added. Otherwise, the PeekMessage always returns a value to the MSG (15 = 0x000F) and keep in infinite loop.
Finally, add Gdi32.lib
flag to the linker at build.bat file.
My Quick Access
- Allocate Memory with VirtualAlloc, VirtualFree and BITMAPINFO (create_back_buffer);
- Create the update_window with StrechDIBits;
- Get Device Context at beginning of program;
- Add some data to the memory;
- Blit the Window every frame;
- Release Device Context;
References
- StretchDIBits - copies the color data for a rectangle of pixels in a DIB, JPEG, or PNG image to the specified destination rectangle. If the destination rectangle is larger than the source rectangle, this function stretches the rows and columns of color data to fit the destination rectangle
- BITMAPINFO - The BITMAPINFO structure defines the dimensions and color information for a DIB.
- BITMAPINFOHEADER - The BITMAPINFOHEADER structure contains information about the dimensions and color format of a device-independent bitmap (DIB).
- VirtualAlloc - Reserves, commits, or changes the state of a region of pages in the virtual address space of the calling process. Memory allocated by this function is automatically initialized to zero.
- VirtualFree - Releases, decommits, or releases and decommits a region of pages within the virtual address space of the calling process.
- GetDC - The GetDC function retrieves a handle to a device context (DC) for the client area of a specified window or for the entire screen. You can use the returned handle in subsequent GDI functions to draw in the DC.
- ReleaseDC - The ReleaseDC function releases a device context (DC), freeing it for use by other applications. The effect of the ReleaseDC function depends on the type of DC. It frees only common and window DCs.