Handmade Penguin Chapter 5: Graphics Refactoring and Review

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

global_variable SDL_Texture *Texture; global_variable void *BitmapMemory; global_variable int BitmapWidth; global_variable int BitmapHeight; global_variable int BytesPerPixel = 4;

struct sdl_offscreen_buffer { SDL_Texture *Texture; void *Memory; int Width; int Height; int BytesPerPixel; };

BytesPerPixel

4

SDLResizeTexture()

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)

BitmapMemory

Buffer.Memory

BitmapWidth

Buffer.Width

BitmapHeight

Buffer.Height

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)

BitmapMemory becomes Buffer->Memory

becomes BitmapWidth becomes Buffer->Width

becomes BitmapHeight becomes Buffer->Height

becomes Texture becomes Buffer->Texture

becomes BytesPerPixel becomes Buffer->BytesPerPixel

Buffer->BytesPerPixel = 4;

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)

BitmapMemory becomes Buffer.Memory

becomes BitmapWidth becomes Buffer.Width

becomes BitmapHeight becomes Buffer.Height

becomes Texture becomes Buffer.Texture

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:



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:

It's good practice. 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:

Create a struct with all of the return values we want, and return that. Pass in pointers to variables to hold the results as parameters, and modify those variables.

SDL_GetWindowSize()

Width

Height

SDL_GetWindowSize()

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); }

SDL_GetWindowSize()

SDLGetWindowDimension()

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:

The pitch, (which is the number of bytes you need to add to get to the next row of pixels) or the total amount of memory we need to store the bitmap, which is really just the pitch times the height.

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; };

Width*4

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;

Buffer.Pitch

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 -->