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 of 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.

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.