Handmade Penguin Chapter 5: Graphics Refactoring and Review

This chapter covers roughly the content in the Windows Graphics Review part of the Handmade Hero course, under the Linux operating system.

<-- Chapter 4 | Back to Index | Chapter 6 -->

CS_HREDRAW, CS_VREDRAW and Optimisation

In the stream, Casey pointed out that we should have the CS_HREDRAW and CS_VREDRAW in the style member of our WNDCLASS. Since we're using SDL, we need to know what the equivalent is. The good news is that there isn't one: CS_HREDRAW and CS_VREDRAW tell Windows that we want to redraw the whole window when it is resized, rather than just the bits that changed. SDL has this behaviour by default.

The other thing that was mentioned, was that the RECT parameter to Win32UpdateWindow() was being passed as a pointer, and (due to aliasing) this might be slower than passing it directly by value. We can't do that for our SDL_Window and SDL_Renderer pointers, as we don't actually know what the struct contains. That's private to the SDL library: you'll note we never access anything within it. Because we don't know, the compiler doesn't know how bug the struct is, so can't allocate one on the stack. It does know how big a pointer is, so we can always have a pointer.

The one place we can get rid of a pointer, would be the SDL_Event pointer in our HandleEvent() function. However, the SDL_Event union is 56 bytes in size (much bigger than a pointer), so it might not actually improve performance. If you try it out, let me know if it's better or worse!

Making the Offscreen Buffer Struct

Let's move all of the variables for our buffer into a structure. This will make it much easier to use (we'll know where all of our buffer variables are), as well as letting us have multiple buffers in the future.
Check this out! SDL has its own structure for an offscreen buffer (or surface): SDL_Surface. We won't be using it here, but if you're making a game that is only going to be using SDL for rendering, it's worth going looking at. It's also worth checking out all of the 2D accelerated rendering functions for using SDL_Renderer and friends: these are often hardware accellerated, and will be faster than doing everything by hand.
So:

global_variable SDL_Texture *Texture;
global_variable void *BitmapMemory;
global_variable int BitmapWidth;
global_variable int BitmapHeight;
global_variable int BytesPerPixel = 4;
should become:
struct sdl_offscreen_buffer
{
    SDL_Texture *Texture;
    void *Memory;
    int Width;
    int Height;
    int BytesPerPixel;
};
Note that we can't initialise BytesPerPixel to 4 in our structure here. We'll cover this when we change SDLResizeTexture(). The final thing we'll need is to have an instance of this struct:
global_variable sdl_offscreen_buffer GlobalBackbuffer;

Let's now make RenderWeirdGradient() use our sdl_offscreen_buffer struct. We'll first make it accept an instance of our struct:

internal void
RenderWeirdGradient(sdl_offcreen_buffer Buffer, int BlueOffset, int GreenOffset)
Then we just need to replace all occurances of BitmapMemory with Buffer.Memory, and the same for BitmapWidth -> Buffer.Width, BitmapHeight -> Buffer.Height and BytesPerPixel -> Buffer.BytesPerPixel.

Now let's use sdl_offscreen_buffer in our SDLResizeTexture() function. Because we need to change sdl_offscreen_buffer, we'll pass it as a pointer. We could also just return a new sdl_offscreen_buffer, but using a pointer is easier. So, our function becomes:

internal void
SDLResizeTexture(sdl_offscreen_buffer *Buffer, SDL_Renderer *Renderer, int Width, int Height)
We now just have to do the same sort of variable translation. The other thing we need to do is add a
Buffer->BytesPerPixel = 4;
line in, as we aren't initialising it anywhere else, anymore.

We also need to change SDLUpdateWindow(). As SDLUpdateWindow() doesn't change our offscreen buffer, we won't pass it our sdl_offscreen_buffer as a pointer:

internal void
SDLUpdateWindow(SDL_Window *Window, SDL_Renderer *Renderer, sdl_offscreen_buffer *Buffer)
We now just have to do the familiar change of variables:

The last thing we need to do is pass our global GlobalBackbuffer as a parameter to our RenderWeirdGradient() and SDLUpdateWindow() functions. We also need to pass the address of GlobalBackbuffer to SDLResizeTexture() as it accepts a pointer. If you remember, that's &GlobalBackbuffer.

Stretching the Buffer

Instead of resizing our buffer each frame, how about we stretch it instead. Let's remove the call to SDLResizeTexture() in our SDL_WINDOWEVENT_SIZE_CHANGED handler. Compile and run, and wow:
Resized gradients
It works! We can resize the window and the display will stretch. That's because we wrote our code to support stretching anyway: SDL_RenderCopy() will stretch if the source and target are a different size. (On most systems, it will also bilinearly interpolate when it's stretching, which looks a lot nicer than it does on Windows, which uses nearest-neighbour interpolation.)

Check this out! SDL actually has built-in support for stretching your entire display, no matter how many textures you are copying. Look up SDL_RenderSetLogicalSize(). It lets you tell SDL to pretend your window is a given size, and just stretches everything you render to match the real size of the window. It even corrects the aspect ratio for you!

Wrapping SDL_GetWindowSize()

In the stream, Casey created a new Win32GetWindowDimension() function, because getting the size of the window is not particularly pleasant on Windows, and we were doing it a lot. With SDL, we aren't getting the window size quite as often, and it's pretty easy, but it's worth writing an SDLGetWindowDimension() function for a couple of reasons:

  1. It's good practice.
  2. It's a good idea to keep the Windows and Linux code similar. What we'll be wanting to do eventually is build an abstraction layer, so that the rest of the code doesn't even know what operating system it's running on.

Fortunately, this is very, very easy. We'll start with creating a new struct to store the result. This largely is a matter of taste: to return the size of a window, we need to return two values: the width and the height. However, functions only have one return value. There are (at least) two ways to work around this:

  1. Create a struct with all of the return values we want, and return that.
  2. Pass in pointers to variables to hold the results as parameters, and modify those variables.
SDL_GetWindowSize() uses the second option: if you recall, we declare Width and Height variables, and then pass pointers to them to SDL_GetWindowSize(). In Handmade Hero, Casey prefers the first style, so that's what we'll use:
struct sdl_window_dimension
{
    int Width;
    int Height;
};

I suspect you all know exactly how you'll implement SDLGetWindowDimension() already, so let's not hold up:

sdl_window_dimension
SDLGetWindowDimension(SDL_Window *Window)
{
    sdl_window_dimension Result;

    SDL_GetWindowSize(Window, &Result.Width, &Result.Height);

    return(Result);
}
Easy as pie! We can now look for places we're using SDL_GetWindowSize() and swap them to use our new SDLGetWindowDimension() function.

Fixes from the Q&A: BytesPerPixel and Pitch

During the Q&anp;A, a few more changes were made to the code, so you'll need to do just a couple more things to catch up!

If sdl_offscreen_buffer's BytesPerPixel member is always 4, why bother keeping it. Indeed, a lot of our other code requires there to be 4 bytes per pixel: the RenderWeirdGradient() function never checks what BytesPerPixel is before blindly writing 32-bit RGBX data. Similarly, SDLResizeTexture() always passes SDL_PIXELFORMAT_ARGB8888 to SDL_CreateTexture(). It makes sense, therefore, to simply state that our pixel data is always in that format, and get rid of the BytesPerPixel member entirely.

Let's put a comment in our sdl_offscreen_buffer structure to say that pixels are always 32-bit and have the bytes in BGRX order (remember that everything is little-endian). Now, let's just outright delete the BytesPerPixel variable. If we compile now, we'll see a bunch of places where we now get an error. This tells us where we'll have to replace things.

Looking at the places we use BytesPerPixel, we see that most of them are actually calculating either:

  1. The pitch, (which is the number of bytes you need to add to get to the next row of pixels)
  2. or the total amount of memory we need to store the bitmap, which is really just the pitch times the height.
So let's add the pitch to our struct instead:
struct sdl_offscreen_buffer
{
    // NOTE(casey): Pixels are alwasy 32-bits wide, Memory Order BB GG RR XX
    SDL_Texture *Texture;
    void *Memory;
    int Width;
    int Height;
    int Pitch;
};
We can initialise the pitch to Width*4, but, even though we know the number of bytes per pixel will always be 4, if there's just a 4 in our code, we might not remember what it represents. These are called magic numbers, and it's often a good idea to avoid them (if it doesn't make things much harder).

Let's, therefore, reinstate our BytesPerPixel variable, just in SDLResizeTexture(), and just set it to 4. (If you like using the const keyword to mark a variable as constant — the oxymoronicness of this is well understood — this is an excellent place to do so.) We can then simply set our pitch value:

Buffer->Pitch = Width * BytesPerPixel;
We then just need to find all of the places where calculate the pitch, and replace them with references to Buffer.Pitch: easy!

End of Lesson!

Congratulations: we've survived the first week1 That's almost all of the platform-specific graphics code done! Next week we'll be looking at fixing the aspect ratio, and maybe even getting into sound or input.

As always, direct any questions or comments to david@ingeniumdigital.com, and I'll try to sort them out!

If you've bought Handmade Hero, the source for the Linux version can be downloaded here.


<-- Chapter 4 | Back to Index | Chapter 6 -->