Glusoft

Make a loading screen

In this tutorial, we will make a loading screen with SDL.

An animated loading progress bar

Introduction

Make a loading screen with SDL2 can seem to be difficult at first because SDL cannot share his context. The non-shareable context is the rendering (SDL_Renderer) with multiple threads.

I don’t see the problem, why can’t we do both?
You want to render a progress bar or a loading screen, for that, you need to use the method SDL_RenderCopy to render the loading screen.

The first argument of this function is the context: SDL_Renderer

int SDL_RenderCopy(SDL_Renderer* renderer, SDL_Texture* texture, const SDL_Rect* srcrect, const SDL_Rect* dstrect)

If you want to load new textures you must do it in another thread, and you can load an SDL_Surface without having to use the renderer because the surface stays in the RAM of the computer.

But when you want to convert the SDL_surface into an SDL_Texture you will use SDL_CreateTextureFromSurface :

SDL_Texture* SDL_CreateTextureFromSurface(SDL_Renderer* renderer, SDL_Surface*  surface)

What happens when the thread try the access the renderer at the same instant the renderer is used for the rendering?
The program crash, so a solution is to use the thread to load an SDL_Surface. And then return to the main thread to convert the surface into a texture.

Image class


First let us create a class for a image:

class Image {
public:
Image(std::string n, SDL_Surface* sur) {
	name = n;
	surface = sur;
}

std::string getName() { return name; }
SDL_Surface* getSurface() { return surface; }
private:
std::string name;
SDL_Surface* surface;
};

We can also create a class Images to have all the images we need to load.

Why not simply use a vector of images instead of creating a simple class?
The problem is the vector is not thread-safe so it cannot be accessed while a new image is added.

Every time an object is added to a vector, the capacity of the vector is checked. If the vector capacity is reached the vector reallocates some memory.
When there is a memory allocation, every iterator is invalidated 🙂

What happens when we try to access an object with that iterator? => The program crash.

Here is a post about the iterator invalidation rules.

Basically, to solve that we need to use a mutex, in this case, SDL_mutex to lock the vector.

class Images{
public:
Images(SDL_mutex* mu) {
	mutex = mu;
	completed = false;
}

std::vector<Image>* getImages() { return &images; }
SDL_mutex* getMutex() { return mutex; }
void setCompleted(bool comp) { completed = comp; }
bool getCompleted() { return completed; }

private:
SDL_mutex* mutex;
bool completed;
std::vector<Image> images;
};

This time we can check if the image is locked!

The next thing to do is the main where we create a window and a context.

In this example I have used the Roboto font you can download here

In this tutorial, I use SDL_CreateThread to create the thread. Most of the time I use the standard library std::thread.

This tutorial is also a nice way to use the SDL functions to draw texts :

std::string textLoading = "Loading " + std::to_string((total*100)/200) + " %";
SDL_Surface *sur = TTF_RenderText_Blended(font, textLoading.c_str() , SDL_Color{ 255, 255, 255, 255 });
int posX = (800 - sur->w) / 2;
int posY = (600 - sur->h) / 2;
SDL_Rect pos { posX, posY , sur->w, sur->h };
SDL_Texture *tex = SDL_CreateTextureFromSurface(renderer, sur);
SDL_FreeSurface(sur);

The main function

Here is the full source code for the program, everything is standard, you have the event loop and the rendering loop.

int main(int argc, char *argv[]) {
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
TTF_Init();

SDL_Window *window;
SDL_Renderer *renderer;

SDL_CreateWindowAndRenderer(800, 600, SDL_WINDOW_RESIZABLE, &window, &renderer);

bool completed = false;
bool confirm = false;

SDL_mutex* mutex = SDL_CreateMutex();
Images imgLoader (mutex);

std::vector<Image>* images = imgLoader.getImages();

std::vector<SDL_Texture*> imagesTex;

TTF_Font *font = TTF_OpenFont("roboto.ttf", 30);

SDL_Thread *thread = SDL_CreateThread(LoadThread, "Load", (void*) &imgLoader);

size_t total = 0;

while (!(completed && confirm)) {
	SDL_Event e;
	while (SDL_PollEvent(&e)) {
		if (e.type == SDL_QUIT) {
			confirm = true;
		}
	}

	size_t num = 0;

	if (SDL_LockMutex(mutex) == 0) {
		num = images->size();
		if (num > 0) {
			Image img = images->back();
			std::cout << "Create texture from image : " << img.getName() << "\n";
			SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, img.getSurface());
			imagesTex.push_back(tex);

			total++;
			SDL_FreeSurface(img.getSurface());
			images->pop_back();
		}
		completed = imgLoader.getCompleted();
		SDL_UnlockMutex(mutex);
	} else {
		std::cout << "Couldn't lock the mutex for MainThread\n";
	}
	
	std::string textLoading = "Loading " + std::to_string((total*100)/200) + " %";
	SDL_Surface *sur = TTF_RenderText_Blended(font, textLoading.c_str() , SDL_Color{ 255, 255, 255, 255 });
	int posX = (800 - sur->w) / 2;
	int posY = (600 - sur->h) / 2;
	SDL_Rect pos { posX, posY , sur->w, sur->h };
	SDL_Texture *tex = SDL_CreateTextureFromSurface(renderer, sur);
	SDL_FreeSurface(sur);

	SDL_Rect bgSrc{ 0, 0 , 4, 20 };
	SDL_Rect posBg{ 0, 340 , 4, 20 };

	SDL_RenderClear(renderer);

	for (size_t i = 0; i < imagesTex.size(); i++) {
		posBg.x = i*4;
		bgSrc.x = i;
		SDL_RenderCopy(renderer, imagesTex[i], &bgSrc, &posBg);
	}

	SDL_RenderCopy(renderer, tex, NULL, &pos);
	SDL_RenderPresent(renderer);

	SDL_DestroyTexture(tex);
}

for (auto img : imagesTex) {
	SDL_DestroyTexture(img);
}

SDL_DestroyMutex(mutex);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
TTF_Quit();
SDL_Quit();

return 0;
}

Here I have started the thread before the loop with the line:

SDL_Thread *thread = SDL_CreateThread(LoadThread, "Load", (void*) &imgLoader);

In a real game, we can also between start the thread at the beginning but only when we need to load something.
If you have a big chunk of content to load from time to time you should start the thread only when you need it.

The interesting part is where we lock the mutex to create the texture from the surface. The rest of the code is to initialize windows and stuff. Making things nice with a text and a loading bar and cleaning things when the loading is done.

if (SDL_LockMutex(mutex) == 0) {
num = images->size();
if (num > 0) {
	Image img = images->back();
	std::cout << "Create texture from image : " << img.getName() << "\n";
	SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, img.getSurface());
	imagesTex.push_back(tex);

	total++;
	SDL_FreeSurface(img.getSurface());
	images->pop_back();
}
completed = imgLoader.getCompleted();
SDL_UnlockMutex(mutex);
} else {
	std::cout << "Couldn't lock the mutex for MainThread\n";
}

The loading thread

And now the ugly (I will explain) thread function :

static int LoadThread(void *ptr) {
IMG_Init(IMG_INIT_JPG | IMG_INIT_PNG);

Images *loading = (Images*)ptr;
const int num = 200;
std::string paths[num] = { 
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
	"img/1.png" , "img/1.png" , "img/1.png", "img/1.png", "img/1.png",
};

std::vector<Image> *images = loading->getImages();
SDL_mutex *mutex = loading->getMutex();

for (int i = 0; i < num; i++) {
	std::string name = "img" + std::to_string(i);
	SDL_Surface *sur = IMG_Load(paths[i].c_str());

	if (SDL_LockMutex(mutex) == 0) {
		std::cout << "Load image (SDL_Surface) : " << i << "\n";
		images->push_back(Image(name, sur));
		if (i == (num - 1))
			loading->setCompleted(true);
		SDL_UnlockMutex(mutex);
	} else {
		std::cout << "Couldn't lock the mutex for LoadThread\n";
	}
}

IMG_Quit();

return 0;
}

I have used the same texture over and over again but you will be using your real content to load.

For convenience, I have hardcoded the paths inside an array, do not do that in your game use an XML or JSON file so you do not need to recompile the game each time you change an asset!

One interesting thing to notice is that SDL_image is initialized in the thread and not in the rendering thread.

You can download the project for making a loading screen : Loading.7z (VS2015).