SDL School
SDL version Which one?

Load an image

This program loads a small PNG file and draws it at three sizes. The image has clear corners, so the blue background shows through. You learn where image files go, and the difference between a surface and a texture.

The full program

main.c
/* Load a PNG file and draw it at three sizes. */
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_Texture *ship = NULL;

/* Load an image file into a texture. */
static SDL_Texture *load_texture(const char *path)
{
    SDL_Surface *surface = SDL_LoadPNG(path);
    SDL_Texture *texture;

    if (surface == NULL) {
        SDL_Log("SDL_LoadPNG failed: %s", SDL_GetError());
        return NULL;
    }

    texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_DestroySurface(surface);
    if (texture == NULL) {
        SDL_Log("SDL_CreateTextureFromSurface failed: %s", SDL_GetError());
    }
    return texture;
}

SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("SDL_Init failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    if (!SDL_CreateWindowAndRenderer("Load an image", WINDOW_WIDTH, WINDOW_HEIGHT, 0,
                                     &window, &renderer)) {
        SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    SDL_SetRenderVSync(renderer, 1);

    ship = load_texture("assets/ship.png");
    if (ship == NULL) {
        return SDL_APP_FAILURE;
    }

    /* Keep hard pixel edges when the image is scaled up. */
    SDL_SetTextureScaleMode(ship, SDL_SCALEMODE_NEAREST);
    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
    if (event->type == SDL_EVENT_QUIT) {
        return SDL_APP_SUCCESS;
    }
    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void *appstate)
{
    float width, height;

    SDL_GetTextureSize(ship, &width, &height);

    SDL_FRect small = { 80.0f, 224.0f, width, height };
    SDL_FRect medium = { 180.0f, 192.0f, width * 3.0f, height * 3.0f };
    SDL_FRect large = { 340.0f, 128.0f, width * 7.0f, height * 7.0f };

    SDL_SetRenderDrawColor(renderer, 32, 96, 160, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);

    SDL_RenderTexture(renderer, ship, NULL, &small);
    SDL_RenderTexture(renderer, ship, NULL, &medium);
    SDL_RenderTexture(renderer, ship, NULL, &large);

    SDL_RenderPresent(renderer);
    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
    SDL_DestroyTexture(ship);
}
main.c
/* Load a PNG file and draw it at three sizes. */
#include <SDL.h>
#include <SDL_image.h>
#include <stdbool.h>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_Texture *ship = NULL;
static bool running = true;

/* Load an image file into a texture. */
static SDL_Texture *load_texture(const char *path)
{
    SDL_Surface *surface = IMG_Load(path);
    SDL_Texture *texture;

    if (surface == NULL) {
        SDL_Log("IMG_Load failed: %s", IMG_GetError());
        return NULL;
    }

    texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_FreeSurface(surface);
    if (texture == NULL) {
        SDL_Log("SDL_CreateTextureFromSurface failed: %s", SDL_GetError());
    }
    return texture;
}

static void frame(void)
{
    SDL_Event event;
    int width, height;

    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) {
            running = false;
        }
    }

    SDL_QueryTexture(ship, NULL, NULL, &width, &height);

    SDL_FRect small = { 80.0f, 224.0f, (float)width, (float)height };
    SDL_FRect medium = { 180.0f, 192.0f, width * 3.0f, height * 3.0f };
    SDL_FRect large = { 340.0f, 128.0f, width * 7.0f, height * 7.0f };

    SDL_SetRenderDrawColor(renderer, 32, 96, 160, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);

    SDL_RenderCopyF(renderer, ship, NULL, &small);
    SDL_RenderCopyF(renderer, ship, NULL, &medium);
    SDL_RenderCopyF(renderer, ship, NULL, &large);

    SDL_RenderPresent(renderer);
}

int main(int argc, char *argv[])
{
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        SDL_Log("SDL_Init failed: %s", SDL_GetError());
        return 1;
    }

    if (SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_HEIGHT, 0, &window, &renderer) != 0) {
        SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
        SDL_Quit();
        return 1;
    }

    SDL_SetWindowTitle(window, "Load an image");
    SDL_RenderSetVSync(renderer, 1);

    ship = load_texture("assets/ship.png");
    if (ship == NULL) {
        SDL_Quit();
        return 1;
    }

    /* Keep hard pixel edges when the image is scaled up. */
    SDL_SetTextureScaleMode(ship, SDL_ScaleModeNearest);

#ifdef __EMSCRIPTEN__
    emscripten_set_main_loop(frame, 0, 1);
#else
    while (running) {
        frame();
    }
#endif

    SDL_DestroyTexture(ship);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

Add the image to your project

An asset is a file that your program uses, such as an image, a sound, or a font. Put your assets in a folder named assets, beside main.c:

hello/
  CMakeLists.txt
  main.c
  assets/
    ship.png

To follow this lesson, right-click ship.png and save it in the assets folder.

The path assets/ship.png is relative to the folder that you run the program from. Run the program from the hello folder, with ./build/hello.

The function SDL_LoadPNG is new in SDL 3.4. The CMakeLists.txt file from the setup page asks for that version or later. For other image formats, such as JPEG, you need the separate SDL_image library.

SDL2 can load only BMP files by itself. For PNG files, you need the separate SDL_image library. Install it first:

Linux

On Ubuntu or Debian:

sudo apt install libsdl2-image-dev

On Fedora:

sudo dnf install SDL2_image-devel

On Arch:

sudo pacman -S sdl2_image

macOS

brew install sdl2_image

Windows

pacman -S mingw-w64-ucrt-x86_64-SDL2_image

Then replace your CMakeLists.txt file with this one:

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(hello C)

find_package(SDL2 REQUIRED CONFIG)
find_package(SDL2_image REQUIRED CONFIG)

add_executable(hello WIN32 main.c)

# Windows needs SDL2main. The other systems do not have this library.
if(TARGET SDL2::SDL2main)
    target_link_libraries(hello PRIVATE SDL2::SDL2main)
endif()
target_link_libraries(hello PRIVATE SDL2::SDL2 SDL2_image::SDL2_image)

Surfaces and textures

SDL holds images in two forms, and this is the main idea of the lesson.

A surface is an image in main memory. Your C code can read and change its pixels, but it is slow to draw. A texture is an image on the graphics card. It is very fast to draw, and it can scale, but your code cannot easily read its pixels.

PNG file on the disk Surface in main memory Texture on the graphics card load upload one time one time draw in each frame
Each image goes from the disk to main memory to the graphics card, one time. After that, the program only draws the texture.

So each image follows the same path:

    SDL_Surface *surface = SDL_LoadPNG(path);
    SDL_Surface *surface = IMG_Load(path);

This line reads the file into a surface. Next, the program copies the surface to the graphics card:

    texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_DestroySurface(surface);
    texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_FreeSurface(surface);

The texture has its own copy of the pixels, so the program frees the surface at once. If you forget this line, the program leaks memory (it keeps memory that it no longer uses).

Load each image one time, at the start. Reading a file in each frame is slow.

Draw the texture

    SDL_RenderTexture(renderer, ship, NULL, &small);
    SDL_RenderCopyF(renderer, ship, NULL, &small);

The third argument chooses a part of the image, and NULL means the full image. The last argument is the destination rectangle. Its size sets the size on the screen, so the same texture can draw at any size.

To keep the shape, the program multiplies the width and the height by the same number.

Keep the pixels sharp

    SDL_SetTextureScaleMode(ship, SDL_SCALEMODE_NEAREST);

By default, SDL3 blurs a texture as it scales. This is good for photographs, but it is bad for pixel art. The nearest mode keeps hard edges.

    SDL_SetTextureScaleMode(ship, SDL_ScaleModeNearest);

This call asks SDL to keep hard edges as it scales the texture. This is the correct mode for pixel art. Photographs look better in SDL_ScaleModeLinear.

Clean up

You made the texture, so you destroy it. SDL_AppQuit calls SDL_DestroyTexture.

You made the texture, so you destroy it. The end of main calls SDL_DestroyTexture, before it destroys the renderer.

Build for the browser

A browser program cannot read files from your disk. Emscripten must pack them with the program:

emcc main.c -sUSE_SDL=3 --preload-file assets -o index.html
emcc main.c -sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -sSDL2_IMAGE_FORMATS='["png"]' --preload-file assets -o index.html

This command also makes a file named index.data. It holds your assets, and you must publish it with the other files.

Try this

  1. Change the scale mode, then build and run. Compare the edges of the large image.
  2. Draw the image at a size with a different shape, such as twice as wide. The image stretches.
  3. Make your own 32 by 32 PNG in any drawing program, and load it in place of the ship.
  4. Make the ship follow the mouse. Use the code from the mouse lesson.

What you learned