Login

 

Site Menu

SDL2 Guides: Part 2Welcome to part two of my SDL2 Guide series! In this guide you will learn how to load images and render them to the screen using the SDL2 library. The prerequisites for this course include the following:
  • You must already have mastered most key concepts of C/C++. 
  • You are already familiar with your development environment and platform.
  • You have already downloaded SDL2 and configured a blank project.
  • You have followed (if necessary) part one of these guides.
I'm going to assume you've already setup your project. The project should have the appropriate include and library directories, and it should also link to SDL2main.lib and SDL2.lib. I will not go over how that is done as it's a part of being familiar with your development environment. If you want to learn how to set this up with Visual Studio, I have written a guide for that.

SOMETHING TO DOWNLOAD (LODEPNG) ...
In this guide one of the image files we're loading is a PNG file, so to facilitate this in the simplest way for the guide and for you I am using LodePNG which can be obtained here. You need to download lodepng.cpp and lodepng.h, also if you're compiling for C (not C++) you'll need to rename lodepng.cpp to lodepng.c. Just add them to your project's folder where your main source file is and add them into your project if you're using an IDE. The reason I like to use LodePNG is partly because it comes with no end-user dependencies and finally because it's just so simple, but does all that I need. There are other ways to load image formats (other than / and PNG) such as SDL_Image, but they're often more complex, slightly more difficult for you to implement, and comes with dependencies (libraries) that you'll have to provide to your end-user. Here's a quote from http://lodev.org/lodepng/"LodePNG is a PNG image decoder and encoder, all in one, no dependency or linkage to zlib or libpng required. It's made for C (ISO C90), and has a C++ wrapper with a more convenient interface on top."
 
IMAGE FILES
The image files used for this guide can be downloaded here: http://download.xeekworx.com/sdl2guide_part02_images.zip
 
Place these images in the folder that will be the current working directory of your application when it runs. That usually means for Visual Studio users the project directory (default), unless you've changed it. For others it may simply be the output directory (where your executable eventually resides).

SURFACES & TEXTURES
This may be a new concept to you depending on your background with graphics. In SDL there are two different ways an image might be stored; in a surface or in a texture. Both are created using calls from SDL and a pointer is returned to either a SDL_Surface or a SDL_Texture. You're never really going to handle these structures without using pointers. Also in most cases you're not going to want to access the variables within those structures directly with the exception of the surface, this is most certainly true with textures. SDL provides calls to query information about surfaces and textures that you should use instead.

A surface is going to store pixel data and specifications of an image, but it does so in system memory (RAM). This is generally the place you will need to get your image into first before you can do anything with it. To create a blank surface you would use SDL_CreateRGBSurface or, if you already have some pixel data loaded, SDL_CreateRGBSurfaceFrom. Surfaces aren't going to be useful to you for rendering, especially if you're using accelerated rendering (which we usually are), but before we can use an image it has to start here. To free your surface when you're done with it use SDL_FreeSurface. If you created your surface with existing pixel data by using SDL_CreateRGBSurfaceFrom, you'll also want to free that pixel buffer after freeing the surface as management of pixel data in this scenario is up to you.

A texture is an image in video memory (unless acceleration is not supported) and with this we're able to render the image to the screen. To create a blank texture you would use SDL_CreateTexture or if you already have a surface you can use SDL_CreateTextureFromSurface. If you were using a surface to create your texture, this surface can probably now be freed after the texture is created. When you're ready to render a texture to the screen you will make calls to SDL_RenderCopy or SDL_RenderCopyEx (with more advanced options). If you want to get information about your texture (such as it's width and height), call SDL_QueryTexture, but we're not going to need to do that in this guide. Finally to clean-up your texture when you're done with it call SDL_DestroyTexture.

In summary, when you're loading images from the file system, you must first get it into a surface. Then it can be converted into a texture that's more useful to us for rendering.

STEP ONE (MAIN)
Our first step is to create a new blank source file. This file can be .c or .cpp, but our sample code will be in C. You're welcome to use the source code from the SDL2 Guide 01 and adjust it to resemble the following code, but there may be extensive changes in this guide as we move along. Here's the source code you should start with for this guide:

#include <SDL.h>
#include "lodepng.h"
#ifdef __cplusplus
extern "C"
#endif
int main(int argc, char *argv[])
{
	int screen_width = 640, screen_height = 480;
	SDL_Window* main_window = NULL;
	SDL_Renderer* renderer = NULL;
	SDL_Event event = { 0 };
	int should_quit = 0;
	
	// ADDTIONAL VARIABLES NEEDED HERE ...
	
	if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
		SDL_Log("Failed to initialize SDL (\"%s\").\n", SDL_GetError());
		goto exit_app;
	}
	
	if((main_window = SDL_CreateWindow("XeekWorx SDLGuide Part 02", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, screen_width, screen_height, 0)) == 0) {
		SDL_Log("Failed to create a window (\"%s\").\n", SDL_GetError());
		goto exit_app;
	}
	
	if((renderer = SDL_CreateRenderer(main_window, -1, 0)) == 0) {
		SDL_Log("Failed to create the renderer (\"%s\").\n", SDL_GetError());
		goto exit_app;
	}
	
	// LOAD THE BACKGROUND BITMAP HERE ...
	
	// PREPARE COLOR MASKS FOR LOGO SURFACE ...
	
	// LOAD LOGO PNG IMAGE ...
	
	// PRIMARY & EVENT LOOP:
	while(!should_quit) {
		while(SDL_PollEvent(&event)) {
			switch(event.type) {
			case SDL_QUIT:
				should_quit = 1;
				break;
			case SDL_KEYDOWN:
				switch(event.key.keysym.sym) {
				case SDLK_ESCAPE:
					should_quit = 1;
					break;
				}
				break;
			}
		}
		
		// RENDERING HERE ...
		
		SDL_RenderPresent(renderer);
	}
	
exit_app:
	// ADDTIONAL CLEAN-UP HERE ...
	
	if(renderer) SDL_DestroyRenderer(renderer);
	if(main_window) SDL_DestroyWindow(main_window);
	SDL_Quit();
	
	return 0;
}
This code is almost identical to the code we put together in the last guide, but there were a couple of changes. The first thing added is an include for lodepng.h and the last thing is the omitted rendering code, which will be different in this guide.
 
STEP TWO (NEW VARIABLES)
We're going to add a lot of new variables at the top of our main(), but first lets go over what each of them is for:
 
TYPE NAME DESCRIPTION
const char [] (Constant Character Array) backgnd_imagefile This is the background image file path. This will be set to a path (null terminated string) of where our bitmap file is located on your system. We're going to initialize this to "background.bmp".
SDL_Surface* backgnd_surface This is the surface (stored in system memory rather than video memory) for our background image. The data that is read from the bitmap will be stored here.
SDL_Texture* backgnd_texture The texture of the background image. After the bitmap is loaded into system memory (the surface) it will be loaded into video memory (this texture) so that we can use it for rendering.
int logo_bpp The bits per pixel in our logo pixel buffer, this will get determined later so it's initialized to 0.
Uint32 (32-bit Unsigned Integer) rmask, gmask, bmask, amask The masks used in determining the position of bits of our color channels. This is going to be used to help decipher the logo PNG data for SDL.
const char [] (Const Character Array) logo_imagefile This is the logo image file path. This will be set to a path string of where our png file is located on your system. We're going to initialize this to "logo.png".
unsigned char* logo_buffer The pixel data lodepng gives us from the logo png file.
unsigned int logo_width, logo_height The width and height of the logo image.
unsigned int lodepng_result A result returned by lodepng calls.
SDL_Surface* logo_surface This is the surface that will enventually utilize our logo's png pixel data (logo_buffer) so that SDL can create a texture from it. 
SDL_Texture* logo_texture The texture of the logo image. After the logo's pixel data is in logo_buffer and a surface created from that, it can then go into video memory so it can be used (a texture).
SDL_Rect logo_destination The SDL_Rect destination of the logo image on screen. 
 
Let's go ahead and add them to our code where you see "// ADDTIONAL VARIABLES NEEDED HERE ..."
	const char backgnd_imagefile[] = "background.bmp";
	SDL_Surface* backgnd_surface = NULL;
	SDL_Texture* backgnd_texture = NULL;
	int logo_bpp = 0;
	Uint32 rmask = 0, gmask = 0, bmask = 0, amask = 0;
	const char logo_imagefile[] = "logo.png";
	unsigned char* logo_buffer = NULL;
	unsigned int logo_width = 0, logo_height = 0, lodepng_result = 0;
	SDL_Surface* logo_surface = NULL;
	SDL_Texture* logo_texture = NULL;
	SDL_Rect logo_destination = { 0 };

STEP THREE (LOADING A BITMAP USING SDL)
SDL has a function built in to load bitmap files, SDL_LoadBMP. Using this is simple with only one parameter for the file path and the result from the call is a pointer to a SDL_Surface. If this call fails the result will be NULL so you should check for that. The most common problem with failure is that the author failed to realize where his file really was in relation to the running program. For example, the default working directory for built applications started from Visual Studio is the project directory not the output directory. So if you were to put your file in the same location as the executable, it would not find it because it's looking for it in the project's root folder.

Let's go ahead and add code in place of where you see "// LOAD THE BACKGROUND BITMAP HERE ..."

	// LOAD THE BACKGROUND BITMAP INTO A SURFACE & CONVERT IT TO A TEXTURE:
	// ... the surface is only temporary, but rendering requires a texture.
	if((backgnd_surface = SDL_LoadBMP(backgnd_imagefile)) == NULL) {
		// Failure ...
		SDL_Log("Failed load background image (\"%s\") for \"%s\".\n", SDL_GetError(), backgnd_imagefile);
		goto exit_app;
	}

If we were successful up to this point we can now create a texture using this surface and then free the surface (as we will not need it anymore).
Add this code below the code above:

	else {
		// Success (Continue converting it to a texture so it's usable) ...
		backgnd_texture = SDL_CreateTextureFromSurface(renderer, backgnd_surface);
		SDL_FreeSurface(backgnd_surface); // The back surface is no longer needed now that we have a texture for it.
		backgnd_surface = NULL;
		// If texture creation failed, now is the time to do something about it:
		if(backgnd_texture == NULL) {
			// Failure ...
			SDL_Log("Failed to create texture from background surface (\"%s\").\n", SDL_GetError());
			goto exit_app;
		}
	}
STEP FOUR (LOADING A PNG FILE USING LODEPNG)
PNG (Portable Network Graphic) files have a few advantages over Bitmaps, but the main one I find most useful is support for an alpha channel (32-bit color). With this advantage we can have images without opaque backgrounds that blend into our scene. A few other advantages are lossless compression (BMP only supports RLE encoding) and built-in color correction. There is going to be quite a bit more to this than loading a simple Bitmap so please bear with me!
 
The first thing we need to do is determine the color masks for the image so that SDL can figure out how to create a surface. Now, I could have omitted this part to reduce lines and hard coded the masks later on, but it could prove useful to you in the future to get proper masks for different pixel formats. Nonetheless, it gives me a chance to talk about it in this guide. Each mask (one for each color channel) will tell SDL where the bits are for that channel in each pixel. Fortunately, SDL gives us a function to figure this out, SDL_PixelFormatEnumToMasks. We are going to use a 32-bit format with 8 bits per pixel.
 
Here is how to get those color masks. Add this code below the code above in place of where you see "// PREPARE COLOR MASKS FOR LOGO SURFACE ...":
	// PREPARE COLOR MASKS FOR LOGO SURFACE:
	if(SDL_PixelFormatEnumToMasks(SDL_PIXELFORMAT_ABGR8888, &logo_bpp, &rmask, &gmask, &bmask, &amask) == SDL_FALSE) {
		// Failure ...
		SDL_Log("Failed to determine color masks from SDL_PIXELFORMAT_ABGR8888 (\"%s\").\n", SDL_GetError());
		goto exit_app;
	}
Now we can finally load our PNG file and LodePNG gives us a simple function to use that will always result in 32-bit pixel data, lodepng_decode32_file. This function is going to read all of the information we need from the PNG file and put it into the parameters (output). I'm not going to go over the parameters as they're well named with the variables we pass to it (as pointers). If lodepng_decode32_file succeeds it will return 0 and if it doesn't there's also a function to convert that return value into error text, lodepng_error_text.
 
Add this code in place of where you see "// LOAD LOGO PNG IMAGE ...":
	// LOAD LOGO PNG IMAGE INTO A BYTE BUFFER & ULTIMATELY CONVERT TO A TEXTURE FOR RENDERING:
	// ... this always decodes to 32-bit ABGR raw image.
	lodepng_result = lodepng_decode32_file(&logo_buffer, &logo_width, &logo_height, logo_imagefile);
	if(lodepng_result != 0) {
		// Failure ...
		SDL_Log("Failed lodepng_decode32_file() (\"%s\") for \"%s.\"\n", lodepng_error_text(lodepng_result), logo_imagefile);
		goto exit_app;
	}
	else {
SDL is going to want a lot of information from what we've loaded so when we create a surface using SDL_CreateRGBSurfaceFrom it's going to appear slighlty overwhelming. I've added comments to some of the parameters to help you understand what they're for.
 
Now that we have a surface we can convert that to a texture so it's more useful by calling SDL_CreateTextureFromSurface. This function is going to need the pointer to our renderer and of course the pointer to the surface we're converting. After the texture is created our logo's surface and pixel buffer are no longer needed because this information is all in the texture and managed by the render driver. The following may seem like a large chunk of code, but dont worry, it's mostly failure checking and cleaning up.
 
Add this code next to create a surface for our logo, convert it to a texture, and clean-up a bit:
		// Success (Continue Preparing Logo, we need a surface first to work with it)...
		// NOTE: SDL_CreateRGBSurfaceFrom() does not copy the buffer, but uses the existing buffer that we're managing.
		if((logo_surface = SDL_CreateRGBSurfaceFrom(
				(void*) logo_buffer, logo_width, logo_height,
				logo_bpp,					// depth or bits per pixel (4 channels X 8 bits per channel)
				logo_width * 4,				// pitch (width X 4 channels)
				rmask, gmask, bmask, amask	// color masks
			)) == 0) {
			// Failure ...
			free(logo_buffer);
			SDL_Log("Failed to create surface for logo image buffer (\"%s\").\n", SDL_GetError());
			goto exit_app;
		}
		else { 
			// Success (Convert the logo surface to a texture so it's usable) ...
			logo_texture = SDL_CreateTextureFromSurface(renderer, logo_surface);
			
			// Clean-up the logo surface & buffer, it's no longer needed:
			SDL_FreeSurface(logo_surface);
			free(logo_buffer); // SDL_FreeSurface() does not do this for us.
			logo_surface = NULL;
			logo_buffer = NULL;
			
			// Now it's a good time to check if texture creation failed:
			if(logo_texture == NULL) {
				SDL_Log("Failed to create texture from logo surface (\"%s\").\n", SDL_GetError());
				goto exit_app;
			}
		}
	}
STEP FIVE (RENDERING TEXTURES)
We've finally finished the hard part and can move on to getting these textures onto the screen. You might notice in this guide we're not going to clear the screen. Since we're drawing a background image that convers the entire screen there is no need to do that, as that essentially solves the same problem clearing the screen does. There are a couple of different functions SDL provides to render textures, SDL_RenderCopy and SDL_RenderCopyEx.
 
SDL_RenderCopy takes 4 parameters. The first and second parameter do not need much explanation, your current renderer and texture. The third parameter is a pointer the source rectangle (SDL_Rect), that lets you determine what part of the texture will be rendered. The last parameter will determine where on the screen (destination) to draw your texture. If the destination is larger in size than the source rectangle, the image will be stretched, and if it's smaller it will be reduced. Either one of these parameters can be NULL which will cause the entire texture is used for the source or the entire destination area is used to render (it will fill the screen).
 
SDL_RenderCopyEx takes 3 additional parameters. An angle for rotation, a destination to deterine where (from the destination rectangle) will the rotation occur, and finally SDL_RenderFlip value that can be one or more values (SDL_FLIP_NONE, SDL_FLIP_HORIZONTAL, SDL_FLIP_VERTICAL). We're not going to use this in this guide, but implementing it should be very easy with what have learned so far.
 
In the following code we're also going to position our logo texture at the center of the screen. This is done with simple math and you might have to use this formula on occasion:
(DESTINATION WIDTH / 2) - (SOURCE WIDTH / 2)
 
Here's the rendering code (Replace where you see "// RENDERING HERE ..." ):
		// We're not going to clear the screen in this application because we're rendering a background
		// ... that essentially clears it already.
		// Render the entire background to the entire screen:
		SDL_RenderCopy(renderer, backgnd_texture, NULL, NULL);
		
		// Render the entire logo to the center of the screen:
		logo_destination.x = (screen_width / 2) - (logo_width / 2);
		logo_destination.y = (screen_height / 2) - (logo_height / 2);
		logo_destination.w = logo_width;
		logo_destination.h = logo_height;
		SDL_RenderCopy(renderer, logo_texture, NULL, &logo_destination);

STEP SIX (CLEAN-UP)
Let's not forget to destroy our textures before exiting. Put this code where you see "// ADDITIONAL CLEAN-UP HERE ...":

	if(logo_texture) SDL_DestroyTexture(logo_texture);
	if(backgnd_texture) SDL_DestroyTexture(backgnd_texture);

 IN CLOSING & THE COMPLETED CODE

SDL2 Guide 2 Screenshot
The image on the right is a screen shot of the end result and what your application should look like (on Windows 8 / 8.1). That's all there is to it and I hope you found my guide as a good explanation on how to use SDL2 as a beginner. I've tried to be detailed and concise enough to give you a good start. The code we have here can be the base for just about any SDL application you want to create. If you have any suggestions on revising this guide please let me know in the comments or by e-mail.

Here is your completed source code:

// XEEKWORX.COM
// SDL2 Guides, Part 02: Loading & Rendering Images
// Copyright (C) 2014 John A. Tullos
//
// FILE: main.c
// LAST UPDATED: 10.23.2014
#include <SDL.h>
#include "lodepng.h"
#ifdef __cplusplus
extern "C"
#endif
int main(int argc, char *argv[])
{
	int screen_width = 640, screen_height = 480;
	SDL_Window* main_window = NULL;
	SDL_Renderer* renderer = NULL;
	SDL_Event event = { 0 };
	int should_quit = 0;
	const char backgnd_imagefile[] = "background.bmp";
	SDL_Surface* backgnd_surface = NULL;
	SDL_Texture* backgnd_texture = NULL;
	int logo_bpp = 0;
	Uint32 rmask = 0, gmask = 0, bmask = 0, amask = 0;
	const char logo_imagefile[] = "logo.png";
	unsigned char* logo_buffer = NULL;
	unsigned int logo_width = 0, logo_height = 0, lodepng_result = 0;
	SDL_Surface* logo_surface = NULL;
	SDL_Texture* logo_texture = NULL;
	SDL_Rect logo_destination = { 0 };
	
	if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
		SDL_Log("Failed to initialize SDL (\"%s\").\n", SDL_GetError());
		goto exit_app;
	}
	
	if((main_window = SDL_CreateWindow("XeekWorx SDLGuide Part 02", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, screen_width, screen_height, 0)) == 0) {
		SDL_Log("Failed to create a window (\"%s\").\n", SDL_GetError());
		goto exit_app;
	}
	
	if((renderer = SDL_CreateRenderer(main_window, -1, 0)) == 0) {
		SDL_Log("Failed to create the renderer (\"%s\").\n", SDL_GetError());
		goto exit_app;
	}
	
	// LOAD THE BACKGROUND BITMAP INTO A SURFACE & CONVERT IT TO A TEXTURE:
	// ... the surface is only temporary, but rendering requires a texture.
	if((backgnd_surface = SDL_LoadBMP(backgnd_imagefile)) == NULL) {
		// Failure ...
		SDL_Log("Failed load background image (\"%s\") for \"%s\".\n", SDL_GetError(), backgnd_imagefile);
		goto exit_app;
	}
	else {
		// Success (Continue converting it to a texture so it's usable) ...
		backgnd_texture = SDL_CreateTextureFromSurface(renderer, backgnd_surface);
		SDL_FreeSurface(backgnd_surface); // The back surface is no longer needed now that we have a texture for it.
		backgnd_surface = NULL;
		// If texture creation failed, now is the time to do something about it:
		if(backgnd_texture == NULL) {
			// Failure ...
			SDL_Log("Failed to create texture from background surface (\"%s\").\n", SDL_GetError());
			goto exit_app;
		}
	}
	// PREPARE COLOR MASKS FOR LOGO SURFACE:
	if(SDL_PixelFormatEnumToMasks(SDL_PIXELFORMAT_ABGR8888, &logo_bpp, &rmask, &gmask, &bmask, &amask) == SDL_FALSE) {
		// Failure ...
		SDL_Log("Failed to determine color masks from SDL_PIXELFORMAT_ABGR8888 (\"%s\").\n", SDL_GetError());
		goto exit_app;
	}
	
	// LOAD LOGO PNG IMAGE INTO A BYTE BUFFER & ULTIMATELY CONVERT TO A TEXTURE FOR RENDERING:
	// ... this always decodes to 32-bit ABGR raw image.
	lodepng_result = lodepng_decode32_file(&logo_buffer, &logo_width, &logo_height, logo_imagefile);
	if(lodepng_result != 0) {
		// Failure ...
		SDL_Log("Failed lodepng_decode32_file() (\"%s\") for \"%s.\"\n", lodepng_error_text(lodepng_result), logo_imagefile);
		goto exit_app;
	}
	else {
		// Success (Continue Preparing Logo, we need a surface first to work with it)...
		// NOTE: SDL_CreateRGBSurfaceFrom() does not copy the buffer, but uses the existing buffer that we're managing.
		if((logo_surface = SDL_CreateRGBSurfaceFrom(
				(void*) logo_buffer, logo_width, logo_height,
				logo_bpp,					// depth or bits per pixel (4 channels X 8 bits per channel)
				logo_width * 4,				// pitch (width X 4 channels)
				rmask, gmask, bmask, amask	// color masks
			)) == 0) {
			// Failure ...
			free(logo_buffer);
			SDL_Log("Failed to create surface for logo image buffer (\"%s\").\n", SDL_GetError());
			goto exit_app;
		}
		else { 
			// Success (Convert the logo surface to a texture so it's usable) ...
			logo_texture = SDL_CreateTextureFromSurface(renderer, logo_surface);
			
			// Clean-up the logo surface & buffer, it's no longer needed:
			SDL_FreeSurface(logo_surface);
			free(logo_buffer); // SDL_FreeSurface() does not do this for us.
			logo_surface = NULL;
			logo_buffer = NULL;
			
			// Now it's a good time to check if texture creation failed:
			if(logo_texture == NULL) {
				SDL_Log("Failed to create texture from logo surface (\"%s\").\n", SDL_GetError());
				goto exit_app;
			}
		}
	}
	
	// PRIMARY & EVENT LOOP:
	while(!should_quit) {
		while(SDL_PollEvent(&event)) {
			switch(event.type) {
			case SDL_QUIT:
				should_quit = 1;
				break;
			case SDL_KEYDOWN:
				switch(event.key.keysym.sym) {
				case SDLK_ESCAPE:
					should_quit = 1;
					break;
				}
				break;
			}
		}
		
		// We're not going to clear the screen in this application because we're rendering a background
		// ... that essentially clears it already.
		
		// Render the entire background to the entire screen:
		SDL_RenderCopy(renderer, backgnd_texture, NULL, NULL);
		
		// Render the entire logo to the center of the screen:
		logo_destination.x = (screen_width / 2) - (logo_width / 2);
		logo_destination.y = (screen_height / 2) - (logo_height / 2);
		logo_destination.w = logo_width;
		logo_destination.h = logo_height;
		SDL_RenderCopy(renderer, logo_texture, NULL, &logo_destination);
		
		SDL_RenderPresent(renderer);
	}
	
exit_app:
	if(logo_texture) SDL_DestroyTexture(logo_texture);
	if(backgnd_texture) SDL_DestroyTexture(backgnd_texture);
	if(renderer) SDL_DestroyRenderer(renderer);
	if(main_window) SDL_DestroyWindow(main_window);
	SDL_Quit();
	
	return 0;
}