Lazy Foo' Productions

SDL Forums external link SDL Tutorials 🐰SDL3 Tutorials🥚 Articles OpenGL Tutorials OpenGL Forums external link
Follow BlueSky Follow Facebook Follow Twitter Follow Threads
Donate
News FAQs Contact Bugs

Textures and Extension Libraries

Textures and Extension Libraries screenshot

Last Updated: Jun 7th, 2025

In this tutorial we'll be rendering with textures instead of surfaces because textures are hardware accelerated and therefore faster to render. We'll also be loading PNG files using SDL_image.
/* Headers */
//Using SDL, SDL_image, and STL string
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_image/SDL_image.h>
#include <string>
For this tutorial we'll be using the SDL_image extension library. Make sure to reuse your SDL3 project (otherwise you'll have to set it up again for a new SDL3 project) and download and install SDL_image much like you did with SDL3.

Also, from now on we will not be covering the entire source code, just highlighting the pieces relevant to the lesson.
/* Class Prototypes */
class LTexture
{
public:
    //Initializes texture variables
    LTexture();

    //Cleans up texture variables
    ~LTexture();

    //Loads texture from disk
    bool loadFromFile( std::string path );

    //Cleans up texture
    void destroy();

    //Draws texture
    void render( float x, float y );

    //Gets texture attributes
    int getWidth();
    int getHeight();
    bool isLoaded();

private:
    //Contains texture data
    SDL_Texture* mTexture;

    //Texture dimensions
    int mWidth;
    int mHeight;
};
Here is our lazy texture class which will be wrapping the SDL_Texture class. We have our constructor and destructors, loadFromFile which will load the image and destroy which will free the texture when we're done with it. The render function will draw the image at the specified position. The getWidth/getHeight and isLoaded functions get the image attributes.

In terms of data members mTexture (which starts with m because it's a member variables much like how global variables start with a g) hold the texture data and mWidth and mWidth holds the dimensions.
/* Global Variables */
//The window we'll be rendering to
SDL_Window* gWindow{ nullptr };

//The renderer used to draw to the window
SDL_Renderer* gRenderer{ nullptr };

//The PNG image we will render
LTexture gPngTexture;
In terms of global variables we have the window like before but instead of a screen surface we have an SDL_Renderer that we'll used to draw to the screen. We also have our wrapped texture object with LTexture.
/* Class Implementations */
//LTexture Implementation
LTexture::LTexture():
    //Initialize texture variables
    mTexture{ nullptr },
    mWidth{ 0 },
    mHeight{ 0 }
{

}

LTexture::~LTexture()
{
    //Clean up texture
    destroy();
}
Here is the constructor and destructor. The constructor initializes the variables (which you should always do because it can lead to some nasty release only bugs if you don't) and as you can see we set the values in the member initializer list as opposed to inside of the constructor itself. This is considered a good habit as it allows for better compiler optimization.

The destructor just calls the function to clean up the texture after we're done with it.
bool LTexture::loadFromFile( std::string path )
{
    //Clean up texture if it already exists
    destroy();

    //Load surface
    if( SDL_Surface* loadedSurface = IMG_Load( path.c_str() ); loadedSurface == nullptr )
    {
        SDL_Log( "Unable to load image %s! SDL_image error: %s\n", path.c_str(), SDL_GetError() );
    }
    else
    {
        //Create texture from surface
        if( mTexture = SDL_CreateTextureFromSurface( gRenderer, loadedSurface ); mTexture == nullptr )
        {
            SDL_Log( "Unable to create texture from loaded pixels! SDL error: %s\n", SDL_GetError() );
        }
        else
        {
            //Get image dimensions
            mWidth = loadedSurface->w;
            mHeight = loadedSurface->h;
        }

        //Clean up loaded surface
        SDL_DestroySurface( loadedSurface );
    }

    //Return success if texture loaded
    return mTexture != nullptr;
}
Our texture loading function first cleans up the texture just in case it already has data loaded. It then tries to load the image with the SDL_image function IMG_Load. IMG_Load can load many types of image and in these tutorials we'll configure it to load PNG images. If the image loading fails, we print an error to the console.

If the image loads successfully, we then try to create a texture from it using SDL_CreateTextureFromSurface which takes in the renderer and the surface we want to create a texture from. If the texture creation fails, we print an error, otherwise we set the texture dimensions.

Whether or not the texture creation fails, we want to destroy the surface after we're done with it. Lastly, we return whether or not the texture was loaded.

If you're wondering why we aren't IMG_LoadTexture it's because while that is a faster way to load the image, in future tutorials we will be making alterations to the loaded surface before creating a texture from it. If you want to eliminate an extra step and you don't need to alter the loaded image before creating a texture from it, you can use IMG_LoadTexture.
void LTexture::destroy()
{
    //Clean up texture
    SDL_DestroyTexture( mTexture );
    mTexture = nullptr;
    mWidth = 0;
    mHeight = 0;
}
In our clean up function we call SDL_DestroyTexture to release the texture after we're done with it and reinitialize the member variables.
void LTexture::render( float x, float y )
{
    //Set texture position
    SDL_FRect dstRect{ x, y, static_cast<float>( mWidth ), static_cast<float>( mHeight ) };

    //Render texture
    SDL_RenderTexture( gRenderer, mTexture, nullptr, &dstRect );
}
Here is the code we use to draw our texture. First we define the destination rectangle (of type SDL_FRect) which defines where on the screen we're going to draw it. Because SDL_FRect is made of floating point types, we want to static cast the image dimensions (which are integers) to float types to get rid of any warnings and because you want to make sure to be explicit with your type conversions. It can lead to some nasty behavior if you aren't careful with how you convert your data types (like it can get you hacked by incorrectly setting your user ID to 0 nasty).

Then we call SDL_RenderTexture to draw the texture. The first argument is the renderer we need to draw stuff. The second argument is the texture itself. The third argument we'll cover in a future tutorial so we'll set it to null for now. The last argument defines the destination rectangle of the texture, so it defines it's position and width/height.
int LTexture::getWidth()
{
    return mWidth;
}

int LTexture::getHeight()
{
    return mHeight;
}

bool LTexture::isLoaded()
{
    return mTexture != nullptr;
}
As you can see, these are simple accessor functions that give us the texture dimensions and whether the texture is loaded or not.
/* Function Implementations */
bool init()
{
    //Initialization flag
    bool success{ true };

    //Initialize SDL
    if( SDL_Init( SDL_INIT_VIDEO ) == false )
    {
        SDL_Log( "SDL could not initialize! SDL error: %s\n", SDL_GetError() );
        success = false;
    }
    else
    {
        //Create window with renderer
        if( SDL_CreateWindowAndRenderer( "SDL3 Tutorial: Textures and Extension Libraries", kScreenWidth, kScreenHeight, 0, &gWindow, &gRenderer ) == false )
        {
            SDL_Log( "Window could not be created! SDL error: %s\n", SDL_GetError() );
            success = false;
        }
    }

    return success;
}
Our initialization function should look familiar with some key changes. Now the window is created with SDL_CreateWindowAndRenderer which as you would expect creates the window and the renderer we need to draw to the window.

If you're wondering where the call to initialize SDL_image is at, that is one thing they got rid of in SDL3_image. This means you don't have to manually initialize it and I don't have to get a bunch of emails from people who need to work on their bitwise operations =)
bool loadMedia()
{
    //File loading flag
    bool success{ true };

    //Load splash image
    if( gPngTexture.loadFromFile( "02-textures-and-extension-libraries/loaded.png" ) == false )
    {
        SDL_Log( "Unable to load png image!\n");
        success = false;
    }

    return success;
}

void close()
{
    //Clean up texture
    gPngTexture.destroy();
    
    //Destroy window
    SDL_DestroyRenderer( gRenderer );
    gRenderer = nullptr;
    SDL_DestroyWindow( gWindow );
    gWindow = nullptr;

    //Quit SDL subsystems
    SDL_Quit();
}
As you would expect, our media loading function loads our image. The clean up function cleans up the loaded texture, cleans up the renderer with SDL_DestroyRenderer and cleans up the window like before.

Much like we don't have to initialize SDL_image anymore, we also don't have to shut it down.
int main( int argc, char* args[] )
{
    //Final exit code
    int exitCode{ 0 };

    //Initialize
    if( init() == false )
    {
        SDL_Log( "Unable to initialize program!\n" );
        exitCode = 1;
    }
    else
    {
        //Load media
        if( loadMedia() == false )
        {
            SDL_Log( "Unable to load media!\n" );
            exitCode = 2;
        }
        else
        {
            //The quit flag
            bool quit{ false };

            //The event data
            SDL_Event e;
            SDL_zero( e );
            
            //The main loop
            while( quit == false )
            {
                //Get event data
                while( SDL_PollEvent( &e ) == true )
                {
                    //If event is quit type
                    if( e.type == SDL_EVENT_QUIT )
                    {
                        //End the main loop
                        quit = true;
                    }
                }

                //Fill the background white
                SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
                SDL_RenderClear( gRenderer );
            
                //Render image on screen
                gPngTexture.render( 0.f, 0.f );

                //Update screen
                SDL_RenderPresent( gRenderer );
            } 
        }
    }

    //Clean up
    close();

    return exitCode;
}
Our main function works much like it did before. This time we call SDL_SetRenderDrawColor to set the color to clear the screen with and SDL_RenderClear to clear the screen. Then we draw the PNG image and update the screen with SDL_RenderPresent.

If you're wondering why we're using 0.f as opposed to just 0 or 0.0 for the position of the image, it's because when we write it as 0.f the compiler knows that this is explicitly a floating point number. 0 can be treated like an interger and 0.0 will be treated as a double. By explicitly defining it as a float, you avoid any of the potential problems that come with converting a integer to a float or a double to a float.

Addendum: Deleting auto generated class functions

When you create a class, C++ will auto generate:
  • The default constructor
  • The copy constructor
  • The copy assignment operator
  • The move constructor (in modern C++)
  • The move assignment operator (in modern C++)
It is considered good (modern) C++ coding to delete default class functions you're not going to use:
class LTexture
{
public:
    //Initializes texture variables
    LTexture();

    //Cleans up texture variables
    ~LTexture();

    //Remove copy constructor
    LTexture( const LTexture& ) = delete;

    //Remove copy assignment
    LTexture& operator=( const LTexture& ) = delete;

    //Remove move constructor
    LTexture( LTexture&& ) = delete;

    //Remove move assignment
    LTexture& operator=( LTexture&& ) = delete;
    
    //Loads texture from disk
    bool loadFromFile( std::string path );

    //Cleans up texture
    void destroy();

    //Draws texture
    void render( float x, float y );

    //Gets texture attributes
    int getWidth();
    int getHeight();
    bool isLoaded();

private:
    //Contains texture data
    SDL_Texture* mTexture;

    //Texture dimensions
    int mWidth;
    int mHeight;
};
A big thing about writing good C++ code is not only making code that works well, but making code that is hard to use badly. For example in this tutorial the default copy constructor does a shallow copy if we were to copy the LTexture which is not going to lead to desirable behavior.

However, since we are not copying LTextures in this demo and the deletion does clutter up the code, I decided to skip doing it for the sake of making the code easier to read. However, in a real application you should watch out for behavior that auto generated functions cause.

Addendum: Inline functions

Inline functions are a way to optimize your code when used properly. When used improperly, they can also make your code slower.

Certain functions like getWidth and getHeight are prime candidates for optimization through inlining. However, the line between what should be inlined and what shouldn't be inlined is blurry and rather than having to test each function to see if they would benefit from inlining, I just won't bother. You shouldn't worry about inlining until you finish your computer architecture course.