Handmade Penguin Chapter 4: Animating the Backbuffer

<-- Chapter 3 | Back to Index | Chapter 5 -->

A Quick Translation

Casey is using some different variable names than previous chapters of Handmade Penguin. To keep up, you'll need to rename:

Pixels -> BitmapMemory

-> TextureWidth -> BitmapWidth

int

BytesPerPixel

int BytesPerPixel = 4;

malloc()

SDL_UpdateTexture

Casey talked about the difference between Windows' StretchDIBits and BitBlt functions. As we looked at yesterday, we use SDL_UpdateTexture and SDL_RenderCopy with SDL to update our texture. This is because SDL doesn't have an equivalent of StretchDIBits directly: we can't stretch directly from out own memory. SDL_UpdateTexture is sort-of like StretchDIBits , but without the ability to stretch (so, really like the SetDIBits windows function), and without the ability to output directly to the screen. We then use SDL_RenderCopy to do the actual stretch: this is like the windows StretchBlt function, which is the equivalent of BitBlt but with stretching support.

Memory Management with mmap()

The other thing we need to note is that we used malloc() and free() in the last chapter to allocate memory. Those functions exist on windows as well, but are part of the C runtime library, not the core windows API, so Casey is using VirtualAlloc . The equivalent here is the Linux mmap() function, which is sort-of a combination of VirtualAlloc and the Windows function MapViewOfFile .

mmap() is quite a complicated function:

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

mmap()

mmap()

start : You can tell mmap() that you would like to allocate a specific bit of memory. We'll set this to a null pointer (0), because we don't care what bit of memory we want.

: You can tell that you would like to allocate a specific bit of memory. We'll set this to a null pointer (0), because we don't care what bit of memory we want. length : The amount of memory, in bytes, we want. This is the same as the size we passed to mmap() . Linux will handle all of the issues with the virtual memory page size for us.

: The amount of memory, in bytes, we want. This is the same as the size we passed to . Linux will handle all of the issues with the virtual memory page size for us. prot : This is the memory protection, as a bitfield. Since we want to read and write our memory, we need to bitwise-or PROT_READ and PROT_WRITE together. This is the equivalent of PAGE_READWRITE .

: This is the memory protection, as a bitfield. Since we want to read and write our memory, we need to bitwise-or and together. This is the equivalent of . flags is a set of options. We'll use MAP_ANONYMOUS here, as we don't want to map a file, and MAP_PRIVATE , as we want the memory to only be accessible to our process. (Using one of MAP_PRIVATE or MAP_SHARED is required for anonymous mapping to work.) On some other UNIX systems, you need to use MAP_ANON instead; this will work on Linux, but is officially deprecated.

is a set of options. We'll use here, as we don't want to map a file, and , as we want the memory to only be accessible to our process. (Using one of or is required for anonymous mapping to work.) On some other UNIX systems, you need to use instead; this will work on Linux, but is officially deprecated. fd : This is the file descriptor for the file we want to map. We don't want to map a file, so we'll put -1.

: This is the file descriptor for the file we want to map. We don't want to map a file, so we'll put -1. offset : This is the offset within the file we're mappting that we want to map. We're not mapping a file, so we'll put 0.

Be warned! Anonymous mapping is not a part of the POSIX standard, which governs how UNIX operating systems should function. This means that OSes other than Linux might support it differently, or not at all. Check the documentation for mmap() on your system with the man mmap command. Alternatively, you could keep using malloc() and free() , which will work across all systems.

malloc()

Pixels = mmap(0, Width * Height * 4, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);

munmap()

if (Pixels) { munmap(Pixels, Width * Height * 4); }

sys/mman.h

mmap()

munmap()

VirtualProtect()

mprotect()

Writing some Pixels

Fortunately, the process of writing pixels once we have our backbuffer is pretty similar between platforms. We can re-use Casey's for loops pretty much identically. The only thing we need to change is to make sure all of our variables have the right names, and add BitmapHeight and BytesPerPixel variables and the code will work as-is.

Compile and run, and you'll see a black screen. However, when you resize the window you should see whatever it was you were trying to render:





Polling the Message Loop

Because SDL_WaitEvent() blocks (doesn't return until there's actually an event waiting), we can't do smooth animation. Much like in windows we swap GetMessageA for PeekMessageA , in SDL we replace SDL_WaitEvent with SDL_PollEvent() .

SDL_PollEvent() is very similar to SDL_WaitEvent() , it just returns 1 if there was a message waiting, and 0 otherwise. We therefore just want to run a loop processing events until we have none left. We can then render our screen.

We'll start by replacing our for(;;) loop with a bool Running = true variable and a while(Running) loop. This is because we'll want to signal our desire to quit from loops-within-loops, and break isn't quite that powerful by itself. We then insert a new loop inside to handle events until there are no more left. If HandleEvent() ever returns true, we can comfortably set Running to false.

while(Running) { SDL_Event Event; while(SDL_PollEvent(&Event)) { if (HandleEvent(&Event)) { Running = false; } } RenderWeirdGradient(XOffset, YOffset); SDLUpdateWindow(Window, Renderer); }

If we give that a try, we'll notice that nothing has changed: our window still doesn't display anything for the first frame. This is because we don't get a SDL_WINDOWEVENT_SIZE_CHANGED event when our window is first opened, so we start off with no bitmap. Let's get the window size and resize our texture as soon as we've created our renderer.

int Width, Height; SDL_GetWindowSize(Window, &Width, &Height); SDLResizeTexture(Renderer, Width, Height);

Be warned! The exact mechanics of when your window gets events can differ depending on what window manager you're using. The way X11 and window managers work is something of a huge mess, and while SDL does its best to force some consistency on them, sometime's you're on your own. Fortunately, resizing the window to the same size wouldn't cause any problem (at worst, it'll be a slightly slower, but since it would only happen once, it's not a big deal.

Cool! Now it works. If we add the animation code with XOffset and YOffset from the stream, you'll see your code animate!



Notice how it gets faster the smaller the window is? That's because we have fewer pixels to fill when the window is smaller.

Note that, unlike the windows version, it's quite likely that SDL is using something like OpenGL under the hood. Sometimes this is faster (we're bypassing a lot of old cruft designed for boring applications this way), though it may seem slower if your computer has VSync enabled. VSync makes SDL_RenderPresent() wait until the monitor is just about to redraw the screen before doing any rendering. This stops tearing from happening, as you'll only see complete frames renderered, but it also limits the performance of your application to the refresh rate of the monitor: usually 60 frames per second. If you're testing performance out, try experimenting with the SDL_RENDERER_SOFTWARE and SDL_RENDERER_PRESENTVSYNC flags for the SDL_CreateRenderer() function.

End of Lesson!

Wow: animation already! It's been quite a week so far. Tomorrow's stream will largely revolve around refactoring, and is likely pretty cross-platform, so the chapter will probably be quite a bit smaller. We're pretty much at the point now where our graphics is cross-platform!

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

Update (2014-11-23): Thanks to Adam Rosenfield for pointing out that some mmap() implementations require fd to be -1 when performing an anonymous mapping.

<-- Chapter 3 | Back to Index | Chapter 5 -->