My Setup for Developing SDL Programs on Windows
2023-12-28
This is not a recommendation! It’s just what I do and it happens to work on two of my machines. It may not be a good idea, though! I’m listing out the steps here in case I need to replicate them in a new environment. Do not blindly mimic this approach unless you understand what’s going on.
The idea is to use Clang for compilation and MSVC as the linker. Makefile is used to build, originating from an MSYS2 installation which also enables usage of other Unix-style utilities. VS Code is the editor I currently use.
Windows is Windows 10 and SDL is SDL2. New versions of both are right around the corner, but I haven’t switched yet.
Development binaries
Install MSYS2 by following the instructions at https://www.msys2.org.
For good measure, also run a package update as per https://www.msys2.org/docs/updating.
From the same MSYS2 terminal program, install Make with pacman -S make
.
Add C:\msys64\usr\bin
and C:\msys64\ucrt64\bin
(substitute with your installation location) to the system PATH variable.
Install LLVM and Clang - visit https://releases.llvm.org/download.html which will likely direct you to https://github.com/llvm/llvm-project/releases. Download LLVM-XX.X.X-win64.exe
and run it.
Add C:\Program Files\LLVM\bin
to PATH, if you haven’t selected that option in the installer.
Installing SDL2
Create a C:\SDL2
directory.
https://www.libsdl.org will likely direct you to https://github.com/libsdl-org/SDL/releases.
From there, download SDL2-X.XX.X-win32-x64.zip
and SDL2-devel-X.XX.X-VC.zip
. Extract both and move the extracted files into C:\SDL2
. The devel archive will give you a directory called SDL2-X.XX.X
- rename it to just SDL2
.
Do the analogous thing for https://github.com/libsdl-org/SDL_image/releases - download the corresponding two archives, extract them into the directory, rename what devel archive gave you to SDL2_image
. Additionally, rename the optional
directory to optional-SDL2_image
and rename the README
to something like README-SDL2_image
.
Do the analogous for https://github.com/libsdl-org/SDL_mixer/releases.
Do the analogous for https://github.com/libsdl-org/SDL_net/releases.
Do the analogous for https://github.com/libsdl-org/SDL_ttf/releases. Be a decent developer and save the license files as well.
In C:\SDL2
, create a file called sdl2-config
. Installing SDL2 on Linux will create this script for you, but there’s no such thing on Windows. So, fake it by copying this into it:
#!/bin/sh
# Calculate the canonical path of the prefix, relative to the folder of this script
prefix="C:\SDL2"
exec_prefix=${prefix}
exec_prefix_set=no
#usage="\
#Usage: $0 [--prefix[=DIR]] [--exec-prefix[=DIR]] [--version] [--cflags] [--libs]"
usage="\
Usage: $0 [--prefix[=DIR]] [--exec-prefix[=DIR]] [--version] [--cflags] [--libs] [--static-libs]"
if test $# -eq 0; then
echo "${usage}" 1>&2
exit 1
fi
while test $# -gt 0; do
case "$1" in
-*=*) optarg=`echo "$1" | sed 's/[-_a-zA-Z0-9]*=//'` ;;
*) optarg= ;;
esac
case $1 in
--prefix=*)
prefix=$optarg
if test $exec_prefix_set = no ; then
exec_prefix=$optarg
fi
;;
--prefix)
echo $prefix
;;
--exec-prefix=*)
exec_prefix=$optarg
exec_prefix_set=yes
;;
--exec-prefix)
echo $exec_prefix
;;
--version)
echo X.XX.X
;;
--cflags)
echo "-I${prefix}\SDL2\include -I${prefix}\SDL2_image\include -I${prefix}\SDL2_ttf\include -I${prefix}\SDL2_mixer\include -I${prefix}\SDL2_net\include -Dmain=SDL_main"
;;
--libs)
echo "-L${prefix}\SDL2\lib\x64 -L${prefix}\SDL2_image\lib\x64 -L${prefix}\SDL2_ttf\lib\x64 -L${prefix}\SDL2_mixer\lib\x64 -L${prefix}\SDL2_net\lib\x64 -lSDL2main -lSDL2"
;;
--static-libs)
# --libs|--static-libs)
echo "I was too lazy to figure out --static-libs and I haven't used them yet." 1>&2
exit 1
;;
*)
echo "${usage}" 1>&2
exit 1
;;
esac
shift
done
That one is adapted from SDL’s Linux script. Modify the paths to match where you installed SDL2 modules.
Add these entries to the system PATH variable: C:\SDL2
, C:\SDL2\optional-SDL2_image
, C:\SDL2\optional-SDL2_mixer
.
Writing code
You can include SDL with a simple #include "SDL.h"
(analogous for other headers).
main
is to be declared with int main(int argc, char *argv[])
, as SDL expects it.
In my projects, I like to put both header and source files in the same directory. src
for my code, lib
for libraries, bin
for built files, and obj
for intermediary built files. Then I could also have separate directories for tests, assets, etc. Of course, it’s all up to you and it can depend on the needs of the project.
Makefile
This is a basic Makefile:
HDRS = $(wildcard src/*.h)
BUILD_FLAGS = -std=c++20 -Wall -Wextra -pedantic -Werror -Isrc -O2 -g -fno-omit-frame-pointer -D_CRT_SECURE_NO_WARNINGS
LINK_FLAGS = `sdl2-config --cflags --libs` -Xlinker /subsystem:windows -lshell32
bin/main.exe: src/main.cpp $(HDRS)
@mkdir -p $(@D)
clang++ $(BUILD_FLAGS) $< -o $@ $(LINK_FLAGS)
clean:
rm -rf bin
-g
asks the compiler to generate debug information which, on Windows, causes it to generate PDB files next to the executable.
On Windows, -D_CRT_SECURE_NO_WARNINGS
is desirable to silence Microsoft’s… opinions on libc.
-Xlinker /subsystem:windows -lshell32
lets you compile SDL2 into a Windows program. Alternatively, -Xlinker /subsystem:console -lshell32
will also cause a console terminal to run along with the window.
You may add -lSDL2_image
, -lSDL2_ttf
, -lSDL2_mixer
, and -lSDL2_net
to LINK_FLAGS
as required.
The Makefile file can then be extended. Eg. you may want another rule for a release build with -O3
and other compiler flags. Again, be a decent developer and copy over license files from C:\SDL2
when packaging your releases.
To make the same Makefile usable on both Windows and Linux, you can do something like this:
HDRS = $(wildcard src/*.h)
ifdef WIN
EXE = main.exe
else
EXE = main
endif
BUILD_FLAGS = -std=c++20 -Wall -Wextra -pedantic -Werror -Isrc -O2 -g -fno-omit-frame-pointer
ifdef VC
BUILD_FLAGS += -D_CRT_SECURE_NO_WARNINGS
endif
LINK_FLAGS = `sdl2-config --cflags --libs`
ifdef VC
LINK_FLAGS += -Xlinker /subsystem:windows -lshell32
endif
bin/$(EXE): src/main.cpp $(HDRS)
@mkdir -p $(@D)
clang++ $(BUILD_FLAGS) $< -o $@ $(LINK_FLAGS)
clean:
rm -rf bin
The run make WIN=1 VC=1
to build. WIN=1
means Windows is the target environment, VC=1
means MSVC is used as the linker. Change/merge the conditional variable names as you please.
If tests are added, I like to run them with ASan, UBSan, MSan, and Valgrind. MSan and Valgrind aren’t supported on Windows, so similar conditions can be used to exclude them. Empty targets can be used to track when the previous test was run.
Project configurations in VS Code
I use VS Code’s official C/C++ extension. To make Intellisense work, do something like this in .vscode/c_cpp_properties.json
:
{ "configurations": [ { "name": "Win32", "includePath": [ "${workspaceFolder}/**",
"C:/SDL2/**"
], "defines": [ "_DEBUG", "UNICODE", "_UNICODE" ], "windowsSdkVersion": "10.0.19041.0",
"compilerPath": "C:/Program Files/LLVM/bin/clang++.exe", "intelliSenseMode": "windows-clang-x64"
} ], "version": 4 }
To be able to debug in VS Code (with breakpoints and everything), you’ll need this in .vscode/launch.json
:
"configurations": [
{
"name": "C++ Launch",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/main.exe",
"cwd": "${workspaceFolder}"
}
]
Pro-tip: if you are making changes to your code, but they don’t seem to be taking effect - you may be forgeting to rebuild with Make. It happens.
You may also want this in your settings.json
:
"C_Cpp.default.cppStandard": "c++20",
"C_Cpp.default.cStandard": "c17",
Debugging in Visual Studio
Bonus round - you can debug in Visual Studio too.
Open your project directory in Visual Studio.
Right-click on the exe file and select Set as Startup Item.
The click on Debug > Start Debugging (or just hit F5).
More on writing code
Bonus bonus round - this one will be more contentious.
I prefer to only have a single translation unit (approach referred to as a unity build). I may split out some libraries that compile slowly into a separate translation unit, so they don’t need to be re-compiled on my every code change.
I take it a step further and place all of my includes at the start of main.cpp
. That way, the rest of my code files (which are all headers) don’t need those pesky include guards and don’t need to be littered with superfluous includes. This occasionally causes VS Code to draw red squiggles all over my code in horror and panic - running an Intellisense rescan or restarting VS Code can fix that.
As you can imagine, this approach can have some serious downsides. Eg. time spent compiling can wildly increase as the project grows larger, since any change necessitates a complete re-compilation (it’s especially bad if a lot of C++ templates are used).
If you don’t know much about unity builds and why they can be bad, I suggest not doing this.
Conclusion
On Linux, development is much easier. Why do people work on Windows then, you ask - because, according to Steam surveys, over 95% of players use Windows to play games. You are kinda forced to develop for it and you want to playtest on it to make your experience close to what the players will see.
I haven’t seen this approach brought up anywhere. Usually the mentioned options are to use Visual Studio or Code::Blocks (I prefer VS Code with Makefiles) or to compile with MinGW. I haven’t run into any issues yet, so I’ll keep doing what I’m doing.