..
#gamedev

(Mac) 003 - Allocate a Back Buffer for the Game

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.

  1. Allocate memory where the pixels persist.
  2. 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;