One of the issues I encounter notoriously often is managing resources. While loading auxiliary data from other files is a feasible option for large applications that supply gigabytes of textures, sounds and background music, it is rather inconvenient for small applications that are often under a megabyte in size. In this post, I would like to share my experiences and a reusable solution for embedding resources into SDL2 applications.

Asset types and the build system.

My solution works out-of-the-box for the following asset types:

  • Bitmaps (BMP)
  • TrueType fonts (TTF)
  • Wave files (WAV)
  • Icons (ICO)

However, it is easy to extend it to other kinds of auxiliary data, as demonstrated later.

My build system of choice for small C applications is just make. Assuming that all assets are located in the assets/ directory, definining a few extra targets is enough to integrate the resource embedding into the build process:

PROGRAM=...

# -- Targets
.PHONY: all
all: assets $(PROGRAM)

.PHONY: clean
clean:
	@rm -f assets/*.h assets.h assets.c *.o program
	@touch assets.h assets.c

.PHONY: format
format:
	clang-format -i *.c *.h

# -- Generate assets.h
.PHONY: assets
assets:
	./gen_assets.sh

# -- Source code
CC=...
CFLAGS=...
LDFLAGS=...

SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

$(PROGRAM): $(OBJ)
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

The assets target is responsible for generating the assets.h and assets.c files, which contain the embedded resources. The clean target is responsible for truncating these files, so that they are regenerated on the next build. The format target is a minor convenience target responsible for formatting the source code using clang-format. The all target is responsible for building the entire program.

Generating the C source.

It is apparent that the gen_assets.sh script is responsible for generating the assets.h and assets.c files, which linked together with the program, provide the embedded resources. First, potential leftover data from previous runs is truncated:

#!/bin/bash

rm -f assets.h assets.c assets_load.c assets_free.c assets_decl.c
touch assets.h assets.c assets_load.c assets_free.c assets_decl.c

Then, the assets.h file is generated.

echo "#ifndef ASSETS_H" >> assets.h
echo "#define ASSETS_H" >> assets.h
echo "" >> assets.h
echo "#include <SDL2/SDL.h>" >> assets.h
echo "#include <SDL2/SDL_mixer.h>" >> assets.h
echo "#include <SDL2/SDL_ttf.h>" >> assets.h
echo "" >> assets.h
echo "void assets_load(SDL_Renderer * renderer);" >> assets.h
echo "void assets_free(SDL_Renderer * renderer);" >> assets.h
echo "" >> assets.h

The file will contain extern declarations of all resources defined in assets.c. In the meantime, a few useful macros are defined in assets_load.c to facilitate loading the resources of varying types from memory.

cat > assets_load.c <<LOAD_EOF
#define loadbmp(buf) \
    { \
        SDL_Surface * surface = SDL_LoadBMP_RW(SDL_RWFromConstMem(assets_ ## buf ## _bmp, sizeof(assets_ ## buf ## _bmp)), 1); \
        if (surface == NULL) { \
            abort(); \
        } \
        SDL_SetColorKey(surface, SDL_TRUE, 0xFF00FF); \
        buf = SDL_CreateTextureFromSurface(renderer, surface); \
        SDL_FreeSurface(surface); \
    }
#define loadico(buf) \
    { \
        buf = SDL_LoadBMP_RW(SDL_RWFromConstMem(assets_ ## buf ## _ico, sizeof(assets_ ## buf ## _ico)), 1); \
        if (buf == NULL) { \
            abort(); \
        } \
    }
#define loadwav(buf) \
    { \
        buf = Mix_LoadWAV_RW(SDL_RWFromConstMem(assets_ ## buf ## _wav, sizeof(assets_ ## buf ## _wav)), 1); \
        if (buf == NULL) { \
            abort(); \
        } \
    }
#define loadttf(buf) \
    { \
        buf = TTF_OpenFontRW(SDL_RWFromConstMem(assets_ ## buf ## _ttf, sizeof(assets_ ## buf ## _ttf)), 1, 12); \
        if (buf == NULL) { \
            abort(); \
        } \
    }
LOAD_EOF

Freeing assets is slightly less complicated:

cat > assets_free.c <<FREE_EOF
#define freebmp(buf) \
    { \
        SDL_DestroyTexture(buf); \
    }
#define freewav(buf) \
    { \
        Mix_FreeChunk(buf); \
    }
#define freeico(buf) \
    { \
        SDL_FreeSurface(buf); \
    }
#define freettf(buf) \
    { \
        TTF_CloseFont(buf); \
    }
FREE_EOF

Next, the functions assets_load() and assets_free() are defined in their respective files:

echo "void assets_load(SDL_Renderer * renderer) {" >> assets_load.c
echo "void assets_free(SDL_Renderer * renderer) {" >> assets_free.c

Then, for every argument passed to the script, the data is embedded as follows:

for f in assets/*; do
    # Determine the type of the resource.
    ext="${f##*.}"
    if [ "$ext" == "bmp" ]; then
        type="SDL_Texture *"
    elif [ "$ext" == "wav" ]; then
        type="Mix_Chunk *"
    elif [ "$ext" == "ico" ]; then
        type="SDL_Surface *"
    elif [ "$ext" == "ttf" ]; then
        type="TTF_Font *"
    else
        echo "Error: Invalid file type $ext, skipping..."
        continue
    fi

    # Get the file name without the extension or the path.
    name_noext="${f%.*}"
    name_noext="${name_noext##*/}"

    # (1) - Generate the extern declaration in the header.
    echo "extern $type $name_noext;" >> assets.h
    # (2) - Generate the definition in the source.
    echo "$type $name_noext;" >> assets_decl.c
    # (3) - Generate the C source for the resource using `xxd -i`
    xxd -i $f >> assets_decl.c
    # (4) - Generate the loading and freeing functions in their respective files
    echo "    load$ext($name_noext);" >> assets_load.c
    echo "    free$ext($name_noext);" >> assets_free.c
done

After all files are processed, the script ends the loading and freeing functions and concatenates all auxiliary C files into a single assets.c file:

echo "}" >> assets_load.c
echo "}" >> assets_free.c

echo "#include \"assets.h\"" >> assets.c
echo "" >> assets.c
cat assets_decl.c >> assets.c
echo "" >> assets.c
cat assets_load.c >> assets.c
echo "" >> assets.c
cat assets_free.c >> assets.c
echo "" >> assets.c
echo "#endif" >> assets.h

rm -f assets_decl.c assets_load.c assets_free.c

Source code

The final source code for the gen_assets.sh script can be found in the GitHub repository of SDLMine, a Minesweeper clone I wrote over a couple of hours in C using SDL2. Despite the license of the entire project (AGPLv3), feel free to use the script in your own projects as if it was licensed under CC0.

Compression

When distributing your application, it is advisable to compress the resulting executable using UPX. This will reduce the size of the executable by a significant amount, due to the fact that BMP, ICO and WAV assets are uncompressed. While beneficial for small applications (under a few megabytes), it may be troublesome for large programs, as UPX may take a long while to unpack the executable. Further, pages of uncompressed executables are usually loaded on demand, so the entire executable is not loaded into memory at once. This means that the executable will not take up as much space in memory as it does on disk (unless mlockall(2) or a similar system call is executed).

My Spider Solitaire game that uses a similar technique for asset embedding benefits from the use of UPX: the executable is reduced from 2.12MB to 442KB (around 80% reduction in size!). Compressing the executable with LZMA yields an even better reduction in size, down to 350KB, however UPX’s --lzma mode is notorious for its unreliability (upx#60, upx#612)