After building SDL, here are the next steps: Setting up and testing SDL with a simple test program.
C++ sample program
There are two ways how one can structure a SDL program:
Program loop with a main function
It’s the more common and traditional starting point of a C or C++ program: the main() function (or WinMain() or similar).
#include <SDL3/SDL.h>
int main (int argc, char* argv[])
{
if (not SDL_Init(SDL_INIT_VIDEO))
{
SDL_Log("SDL_Init() failed: %s!", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_Window* window = SDL_CreateWindow("SDL3-Test", 640, 480, SDL_WINDOW_RESIZABLE);
SDL_Delay(5000); // Wait 5 seconds before closing the window and quitting the program.
SDL_Quit();
return 0;
}
Based on the YouTube video “SDL3 hello world - creating a window [SDL3 Episode 5]” by Mike Shah.
Callback functions
To be able to also run on platforms/operating systems that don’t support a program loop with a main function, SDL additionally offers “callback functions” (more about it in the SDL3 documentation).
In particular, operating systems for mobile devices (iOS, Android) and the web want to have more control of how and when an application does something, therefore the need for this this approach.
Since this callback style works for both the “normal” platforms as well as the “extraordinary” ones (as described above), it probably should be used by default, just to be safe/future-proof…
#define SDL_MAIN_USE_CALLBACKS 1 /* Use the callbacks instead of main() */
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
static SDL_Window* window = NULL;
static SDL_Renderer* renderer = NULL;
// Runs once at startup:
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("SDL-Test", 800, 600, SDL_WINDOW_RESIZABLE, &window, &renderer))
{
SDL_Log("Couldn't create window and renderer: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
return SDL_APP_CONTINUE;
}
// Runs when a new event occurs (e.g. mouse input, keypresses, etc.):
SDL_AppResult SDL_AppEvent (void* appstate, SDL_Event* event)
{
if (event->type == SDL_EVENT_KEY_DOWN || event->type == SDL_EVENT_QUIT)
{
return SDL_APP_SUCCESS;
}
return SDL_APP_CONTINUE;
}
// Runs once per frame; it's the heart of the program:
SDL_AppResult SDL_AppIterate (void* appstate)
{
const char* message = "Hello, world!";
int w = 0, h = 0;
float x, y;
const float scale = 4.0f;
// Center the message and scale it up:
SDL_GetRenderOutputSize(renderer, &w, &h);
SDL_SetRenderScale(renderer, scale, scale);
x = ((w / scale) - SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE * SDL_strlen(message)) / 2;
y = ((h / scale) - SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE) / 2;
// Draw the message:
SDL_SetRenderDrawColor(renderer, 120, 155, 217, 255);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 255, 255, 0, 255);
SDL_RenderDebugText(renderer, x, y, message);
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
// Runs once at shutdown:
void SDL_AppQuit (void* appstate, SDL_AppResult result)
{
SDL_Log("Goodbye!");
SDL_Quit();
}
Based on
- YouTube video: Mike Shah: SDL3 SDL_AppInit - callback style hello world [SDL3 Episode 6]
- LibSDL.org GitHub repo: hello.c (Commit 5f086e7, 2026-01-01)
- StudyPlan.dev: SDL3 Main Callbacks
CMake build system
The build system must also be instructed on how to generate (compile/link) a SDL program.
As usual, I assume that the basics of the CMake meta-build system itself are already known;
I only show here what is specific in relation to the usage with SDL.
Where to find SDL
CMake’s find_package() command searches by default certain standard directories on a system for packages.
But if a package is installed elsewhere, one has to tell it where it should additionally search;
that can be done by adding the path to SDL’s directory to the CMAKE_PREFIX_PATH list.
For example via command line:
cmake --preset default -DCMAKE_PREFIX_PATH="C:/Path/To/SDL3"
Or as a cache variable in a configure preset:
"cacheVariables":
{
"CMAKE_PREFIX_PATH": { "type": "PATH", "value": "C:/Path/To/SDL3" }
}
CMakeLists.txt
The actual CMakeLists.txt also needs some adjustments:
set (SDL_LIBRARY_TYPE "<SDL_LIBRARY_TYPE_PLACEHOLDER>")
# I have a utility that replaces placeholders; in this case with either "shared" or "static" (case sensitive!).
find_package (SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3-${SDL_LIBRARY_TYPE})
target_link_libraries (${PROJECT_NAME} PRIVATE SDL3::SDL3-${SDL_LIBRARY_TYPE}) # see note 1 below!
Note 1: The SDL documentation says this:
SDL’s CMake support guarantees a SDL3::SDL3 target.
Neither SDL3::SDL3-shared nor SDL3::SDL3-static are guaranteed to exist.
… but if the SDL installation was setup to have both the static and shared library in the same directory (as I tend to do), then linking with just SDL3::SDL3 will have the effect that always the shared library will be preferred; therefore I need to be more specific with the targetname.
Copy the shared library into the same diretory as the executable
One “gotcha” when linking a program dynamically (i.e. with a shared library) on Windows is that the shared library (the DLL) must be in the same directory as the executable file itself (or in a standard folder that will be searched by Windows automatically), otherwise the program won’t run.
Therefore the build system should also take care of copying the DLL for SDL from its installation directory (which we specified earlier) into the build and/or install folder of this project.
During build step
That can be done with a custom post-build command (“How do I copy a SDL3 dynamic library to another location?”),
which I have modified a bit:
It should only happen if really a shared library is being used:
if ((WIN32) AND (${SDL_LIBRARY_TYPE} STREQUAL "shared")) # see note 2 below!
add_custom_command (
TARGET ${PROJECT_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:SDL3::SDL3> $<TARGET_FILE_DIR:${PROJECT_NAME}>
VERBATIM
COMMENT "Copy $<TARGET_FILE:SDL3::SDL3> to $<TARGET_FILE_DIR:${PROJECT_NAME}>"
)
endif ()
Note 2: What kind of SDL library is being used could be read from the target’s TYPE property (hint),
but that won’t work here, because that requires a generator expression, which are only available during build time — but defining a custom post-build command must be done before that, at configuration time.
Therefore the string variable (that I needed anyways, see above) is being used again.
During install step
When a project then is installed by CMake (to a different location), the same file copying needs to be done.
That can be achieved with a install() command (and at this time, generator expressions can be used):
install (
FILES $<TARGET_RUNTIME_DLLS:${PROJECT_NAME}>
DESTINATION ${${CMAKE_PROJECT_NAME}_INSTALL_BINDIR} # This variable is set up by me.
)
Film & Television (58)
How To (73)
Journal (18)
Miscellaneous (4)
News & Announcements (21)
On Software (12)
Powershell (1)
Projects (26)