(Mac) 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 contentView
as coordinates.
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. It's rule is important if you want to work with window resize and reallocate a new back buffer.
Let's see the code:
static u8* data; static int bytes_per_pixel; static int pitch;
static void create_back_buffer(int width, int height) { if (data) { free(data); } bytes_per_pixel = 4; pitch = width * bytes_per_pixel; size_t buffer_size = pitch * height; data = (u8 *) malloc(buffer_size); if (!data) { // TODO: Log Error printf("Failed to allocate memory address!\n"); exit(1); } }
The key point of this function is the malloc
that allow us allocate a dynamic memory on the heap.
The pitch
value represents the size of the row in pixels. So, the width * bytes per pixel (RGBA);
The buffer array consists in 4 bytes (32 bits) for each pixel. I mean, 255 bits for each channel (RGBA).
Maybe we change the malloc to a similar alternative mmap or munmap/free and see which is better.
Update the Window (Blit)
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 mac_update_window
.
static void update_window(NSWindow *window, int dst_width, int dst_height, int src_width, int src_height) { @autoreleasepool { NSBitmapImageRep *rep = [[[NSBitmapImageRep alloc] initWithBitmapDataPlanes:&data pixelsWide:src_width pixelsHigh:src_height bitsPerSample:8 samplesPerPixel:bytes_per_pixel hasAlpha:YES isPlanar:NO colorSpaceName:NSDeviceRGBColorSpace bytesPerRow:pitch bitsPerPixel:32] autorelease]; NSSize image_size = NSMakeSize(dst_width, dst_height); NSImage *image = [[[NSImage alloc]initWithSize:image_size] autorelease]; [image addRepresentation:rep]; window.contentView.layer.contents = image; } }
This functions is very simple. We pass the pointer of memory data, the source and destination rectangle and finally, create a image object that represents our view.
The MacOS (objective-c) has a resource to auto release memory allocated with alloc
and autorelease
keyword.
As you can see, we only need the NSBitmapImageRep
for creating the NSImage and add into.
After that, we can free this memory.
This is a self host memory managment with @autoreleasepool
.
The autoreleasepool is a concept to manage the temporary memory. It's part of ARC (Automatic Reference Counting) and helps to release/free memory, avoiding memory leak.
When the block autoreleasepool ends, all objects inside that doesn't have a reference and will be freed. It's very useful in loops where there are many temporary objects, consuming memory.
Put it Together
Go back to the main function and create the back buffer after create a window.
#define WIDTH 960 #define HEIGHT 540
Next, go to the main loop, and after process the message queue, update the window.
Let's see the code:
int width = window.contentView.bounds.size.width; int height = window.contentView.bounds.size.height; // allocate back buffer mac_create_buffer(width, height); while(!should_quit) { NSEvent *event; do { event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES]; mac_process_keyboard(mac_controller, event); switch ([event type]) { default: { [NSApp sendEvent:event]; } } } while(event != nil); // blit the content here! update_window(window, width, height, width, height); }
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.
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; }
In the next post, I'll delve into the pixels and moviment to ensure that this code works.
My Quick Access
- Allocate Memory with malloc and free (create_back_buffer);
- Create the update_window with NSImageBitmapRep;
- Get Window Size - width and height;
- Add some data to the memory;
- Blit the Window every frame;