There are 4,426,165,368 ways to put eight queens on a chessboard, and in only 92 of them does no queen attack another. A backtracking search finds all 92 after testing 15,720 squares, in about a tenth of a millisecond on one server core. The trick is never to finish an arrangement that is already broken.
This guide builds that search in C, shows how three small arrays make each attack check constant-time, counts the 12 solutions that remain once rotations and reflections are set aside, and draws every solution in a window with SDL2. Every program was compiled for this article on Ubuntu 24.04 with GCC 13.3 and Clang 18.1.3 under -std=c11 and -std=c17, with -Wall -Wextra -pedantic -Werror and no warnings, against SDL 2.30, and the solver and viewer run clean under AddressSanitizer and UndefinedBehaviorSanitizer. The SDL2 viewer was run in a virtual X display on Linux; the Windows build compiles the solver only. Every output block is captured verbatim, and the code is in a GitHub repository whose workflow repeats these checks on every commit.
What Is the Eight Queens Puzzle?
The eight queens puzzle asks you to place eight queens on an 8 × 8 chessboard so that no two attack each other: no two share a row, a column or a diagonal. It has 92 solutions, or 12 if solutions that are rotations or reflections of each other count as one. The general N-queens problem has solutions for every board size except 2 × 2 and 3 × 3.
The chess composer Max Bezzel published the puzzle in 1848, and Franz Nauck published the first full set of solutions in 1850; Gauss was among the mathematicians who worked on it. It became a programming classic in 1972, when Edsger Dijkstra used it to illustrate structured programming with a depth-first backtracking algorithm, as the eight queens puzzle article on Wikipedia records. It is still the standard first example of backtracking because the rules are short and the search space is far too large to try blindly.
How Backtracking Solves It
Two observations shrink the problem before any code runs. Every row must hold exactly one queen, so the search places one queen per row, top to bottom. And a placement only needs checking against the queens above it, because the rows below are still empty.
- Start at row 0.
- Try each column in the current row. Skip any square attacked by a queen already placed.
- On a safe square, place the queen and move to the next row.
- If a row has no safe square, return to the row above, remove its queen and try that row’s next column. This is the backtrack.
- When a queen is placed in the last row, the board is a solution. Record it, then keep backtracking to find the rest.
The search never builds an arrangement that is already broken. As soon as two queens attack each other, every arrangement that extends that position is skipped at once.
One Number per Diagonal
The test in step 2 is where the time goes. The obvious version compares the new square with every queen placed so far. A faster version keeps three arrays of flags: one per column, one per rising diagonal and one per falling diagonal. Each diagonal has a number that every square on it shares, so a square is safe when its three flags are all clear.
On a rising diagonal, moving one square up and one to the right lowers the row by one and raises the column by one, so row + col stays the same. On a falling diagonal both change in the same direction, so row - col stays the same; adding n - 1 keeps it from going negative so it can index an array. An 8 × 8 board has 15 diagonals in each direction, numbered 0 to 14.
The Source Code
queens.h
/* queens.h - the N-queens puzzle by backtracking. */
#ifndef QUEENS_H
#define QUEENS_H
#include <stdbool.h>
enum { QUEENS_MAX = 16 };
/* A placement: col[r] is the column (0..n-1) of the queen in row r. */
typedef struct {
int n;
int col[QUEENS_MAX];
} queens_board;
/* Called once per solution; return false to stop the search. */
typedef bool (*queens_visit)(const queens_board *b, void *ctx);
typedef struct {
unsigned long long solutions;
unsigned long long squares_tested; /* squares checked for attacks */
unsigned long long placements; /* queens put on the board, including ones taken back */
} queens_stats;
/* Finds every solution for an n x n board (1 <= n <= QUEENS_MAX), calling
* visit for each one if it is not NULL. Returns false if n is out of range. */
bool queens_solve(int n, queens_visit visit, void *ctx, queens_stats *stats);
/* True if b has one queen per row and no two queens attack each other. */
bool queens_valid(const queens_board *b);
/* True if b is the first of its family of up to 8 rotations and
* reflections, so counting these counts distinct solutions. */
bool queens_canonical(const queens_board *b);
#endif
queens.c
/* queens.c - N-queens by backtracking, one row at a time. */
#include "queens.h"
#include <stdlib.h>
typedef struct {
queens_board b;
bool col_used[QUEENS_MAX];
bool diag_used[2 * QUEENS_MAX - 1]; /* row + col is the same on a / diagonal */
bool anti_used[2 * QUEENS_MAX - 1]; /* row - col + n - 1 is the same on a \ one */
queens_visit visit;
void *ctx;
queens_stats stats;
bool stop;
} search;
static void place_row(search *s, int row)
{
const int n = s->b.n;
if (row == n) { /* every row has a queen */
s->stats.solutions++;
if (s->visit != NULL && !s->visit(&s->b, s->ctx))
s->stop = true;
return;
}
for (int c = 0; c < n && !s->stop; c++) {
const int d = row + c, a = row - c + n - 1;
s->stats.squares_tested++;
if (s->col_used[c] || s->diag_used[d] || s->anti_used[a])
continue; /* attacked: try the next column */
s->b.col[row] = c;
s->col_used[c] = s->diag_used[d] = s->anti_used[a] = true;
s->stats.placements++;
place_row(s, row + 1);
s->col_used[c] = s->diag_used[d] = s->anti_used[a] = false; /* take it back */
}
}
bool queens_solve(int n, queens_visit visit, void *ctx, queens_stats *stats)
{
if (n < 1 || n > QUEENS_MAX)
return false;
search s = {0};
s.b.n = n;
s.visit = visit;
s.ctx = ctx;
place_row(&s, 0);
if (stats != NULL)
*stats = s.stats;
return true;
}
bool queens_valid(const queens_board *b)
{
if (b->n < 1 || b->n > QUEENS_MAX)
return false;
for (int r = 0; r < b->n; r++) {
if (b->col[r] < 0 || b->col[r] >= b->n)
return false;
for (int q = 0; q < r; q++)
if (b->col[q] == b->col[r] || abs(b->col[q] - b->col[r]) == r - q)
return false;
}
return true;
}
/* Square (r, c) after transform t: t & 1 mirrors left-right, t & 2 mirrors
* top-bottom, t & 4 then swaps rows and columns. Together they give all 8
* symmetries of the square; swapping first would give one of them twice. */
static void transform(int t, int n, int r, int c, int *out_r, int *out_c)
{
if (t & 1)
c = n - 1 - c;
if (t & 2)
r = n - 1 - r;
if (t & 4) {
const int tmp = r;
r = c;
c = tmp;
}
*out_r = r;
*out_c = c;
}
bool queens_canonical(const queens_board *b)
{
const int n = b->n;
for (int t = 1; t < 8; t++) {
int image[QUEENS_MAX];
for (int r = 0; r < n; r++) {
int tr, tc;
transform(t, n, r, b->col[r], &tr, &tc);
image[tr] = tc;
}
for (int r = 0; r < n; r++) { /* compare row by row */
if (image[r] < b->col[r])
return false; /* a smaller image exists */
if (image[r] > b->col[r])
break;
}
}
return true;
}
place_row() is the five steps above. The loop tries each column, continue skips an attacked square, and the three assignments after the recursive call take the queen back, which is the backtrack. A solution is reported through a callback, so the same search can count solutions, store them or stop at the first one, and the caller decides which. Recursion depth is one level per row, so 16 at most.
queens_cli.c
/* queens_cli.c - prints the first solution and counts them all.
* Usage: queens [N] (N is 1 to 16, default 8) */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include "queens.h"
typedef struct {
queens_board first;
unsigned long long distinct;
} tally;
static bool record(const queens_board *b, void *ctx)
{
tally *t = ctx;
if (t->first.n == 0)
t->first = *b;
if (queens_canonical(b))
t->distinct++;
return true; /* keep searching */
}
int main(int argc, char **argv)
{
long n = 8;
if (argc > 2) {
fprintf(stderr, "usage: queens [N] (N is 1 to %d)\n", QUEENS_MAX);
return 2;
}
if (argc == 2) {
char *end;
errno = 0;
n = strtol(argv[1], &end, 10);
if (argv[1][0] < '0' || argv[1][0] > '9' || *end != '\0' || errno != 0 ||
n < 1 || n > QUEENS_MAX) {
fprintf(stderr, "usage: queens [N] (N is 1 to %d)\n", QUEENS_MAX);
return 2;
}
}
tally t = {0};
queens_stats s;
queens_solve((int)n, record, &t, &s);
if (s.solutions == 0) {
printf("No way to place %ld queens on a %ld x %ld board.\n", n, n, n);
return 0;
}
printf("First solution, %ld x %ld board:\n", n, n);
for (int r = 0; r < n; r++) {
for (int c = 0; c < n; c++)
printf(" %c", t.first.col[r] == c ? 'Q' : '.');
printf("\n");
}
printf("%llu solution%s, %llu distinct under rotation and reflection.\n",
s.solutions, s.solutions == 1 ? "" : "s", t.distinct);
printf("Squares tested: %llu. Queens placed: %llu.\n", s.squares_tested, s.placements);
return 0;
}
The board size comes from the command line and is checked with strtol before use: anything that is not a whole number from 1 to 16 is rejected with a usage message and exit status 2.
Build and Run
gcc -std=c11 -Wall -Wextra -pedantic -O2 src/queens_cli.c src/queens.c -o queens
./queens
Output:
First solution, 8 x 8 board:
Q . . . . . . .
. . . . Q . . .
. . . . . . . Q
. . . . . Q . .
. . Q . . . . .
. . . . . . Q .
. Q . . . . . .
. . . Q . . . .
92 solutions, 12 distinct under rotation and reflection.
Squares tested: 15720. Queens placed: 2056.
Each row of the board has one Q, and following them down gives the solution: columns 0, 4, 7, 5, 2, 6, 1 and 3. The last line shows how little work the search did, which the next section looks at. The program also handles boards with no solution:
$ ./queens 3
No way to place 3 queens on a 3 x 3 board.
How Much Work the Search Does
queens_stats solves every board from 4 × 4 to 12 × 12 and reports how many squares the search tested, how many queens it placed and then took back, and how many arrangements a brute-force search would have to check if it only tried one queen per row and column (n!):
n solutions distinct squares tested queens placed arrangements (n!)
4 2 1 60 16 24
5 10 2 220 53 120
6 4 1 894 152 720
7 40 6 3584 551 5040
8 92 12 15720 2056 40320
9 352 46 72378 8393 362880
10 724 92 348150 35538 3628800
11 2680 341 1806706 166925 39916800
12 14200 1787 10103868 856188 479001600
For the standard board the search tests 15,720 squares and places 2,056 queens to find all 92 solutions. That 15,720 is exactly the figure Niklaus Wirth gives for his backtracking program in Algorithms + Data Structures = Programs (1976): two programs written half a century apart, one count. The brute-force alternative, already reduced to one queen per row and column, checks 40,320 complete arrangements; without that reduction it is the 4,426,165,368 of the introduction.
The gap widens fast. At 12 × 12 backtracking places 856,188 queens against 479,001,600 arrangements, a ratio of more than 500 to 1. Measured on one 2.8 GHz Xeon core with GCC 13.3 at -O2, counting without printing, the best of five runs took 0.11 ms for 8 × 8, 2.2 ms for 10 × 10 and 56 ms for 12 × 12. Each step up of two board sizes cost roughly 20 to 40 times more (2,190 ms for 14 × 14); the search is still exponential, just with a much smaller base than brute force.
Counting Distinct Solutions
Turn a solution a quarter-turn and you get another solution, because the rules do not care about orientation. The square has eight symmetries: four rotations, and a mirror image of each. The 92 solutions fall into 12 families of related ones; 11 families have eight members and one has only four, because that solution looks the same after a half-turn.
queens_canonical() keeps one member of each family. It builds all seven other images of a solution and returns true only if none of them is smaller when compared row by row. Counting the canonical solutions counts the families: 12 for 8 × 8, and the published figures for every size up to 12 × 12.
The eight images come from three operations, mirror left to right, mirror top to bottom and swap rows with columns, applied in every combination. The order matters. Swapping rows and columns before the top-to-bottom mirror makes two of the eight combinations the same transform and misses another, and the count comes out at 17 instead of 12. Mirroring first and swapping last produces all eight. The test suite checks the distinct counts against the published sequence for every size from 1 to 12, which is how a mistake like that gets caught.
Drawing the Board with SDL2
queens_view.c shows every solution in a window. It uses SDL2, a cross-platform library for windows, input and 2D drawing that is available on Windows, Linux and macOS. The DOS-era graphics.h library still appears in some tutorials, but it only works with old Borland compilers and emulators.
/* queens_view.c - browse every solution in an SDL2 window.
* Usage: queens_view [N] (N is 4 to 12, default 8)
* queens_view N --screenshot K FILE (draw solution K to a BMP and exit)
* Keys: Right/Space next, Left previous, Home first, Esc or Q quit. */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL.h>
#include "queens.h"
enum { SQUARE = 64, MARGIN = 24 };
typedef struct {
queens_board *items;
size_t count, cap;
} solution_list;
static bool collect(const queens_board *b, void *ctx)
{
solution_list *list = ctx;
if (list->count == list->cap) {
const size_t cap = list->cap ? list->cap * 2 : 64;
queens_board *grown = realloc(list->items, cap * sizeof *grown);
if (grown == NULL)
return false; /* stop the search; count stays valid */
list->items = grown;
list->cap = cap;
}
list->items[list->count++] = *b;
return true;
}
static void fill_circle(SDL_Renderer *r, int cx, int cy, int radius)
{
for (int dy = -radius; dy <= radius; dy++) {
int dx = 0;
while ((dx + 1) * (dx + 1) + dy * dy <= radius * radius)
dx++;
SDL_RenderDrawLine(r, cx - dx, cy + dy, cx + dx, cy + dy);
}
}
/* A queen: a base, a body and a crown of three balls, all from rectangles
* and circles so no image or font files are needed. */
static void draw_queen(SDL_Renderer *r, int x, int y)
{
const int cx = x + SQUARE / 2;
SDL_SetRenderDrawColor(r, 0x46, 0x7f, 0x40, 0xff); /* green */
const SDL_Rect base = {x + 14, y + 46, SQUARE - 28, 8};
const SDL_Rect body = {x + 22, y + 26, SQUARE - 44, 22};
SDL_RenderFillRect(r, &base);
SDL_RenderFillRect(r, &body);
fill_circle(r, cx, y + 26, 11);
fill_circle(r, cx - 14, y + 16, 5);
fill_circle(r, cx, y + 11, 5);
fill_circle(r, cx + 14, y + 16, 5);
}
static void draw_board(SDL_Renderer *r, const queens_board *b)
{
SDL_SetRenderDrawColor(r, 0xf4, 0xf5, 0xf7, 0xff);
SDL_RenderClear(r);
for (int row = 0; row < b->n; row++)
for (int c = 0; c < b->n; c++) {
const SDL_Rect sq = {MARGIN + c * SQUARE, MARGIN + row * SQUARE, SQUARE, SQUARE};
if ((row + c) % 2 == 0)
SDL_SetRenderDrawColor(r, 0xe5, 0xe7, 0xeb, 0xff);
else
SDL_SetRenderDrawColor(r, 0x9c, 0xa3, 0xaf, 0xff);
SDL_RenderFillRect(r, &sq);
}
/* The same SQUARE and MARGIN place the queens, so they cannot drift. */
for (int row = 0; row < b->n; row++)
draw_queen(r, MARGIN + b->col[row] * SQUARE, MARGIN + row * SQUARE);
}
static int screenshot(const queens_board *b, const char *path)
{
const int side = 2 * MARGIN + b->n * SQUARE;
SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, side, side, 32,
SDL_PIXELFORMAT_RGBA32);
SDL_Renderer *r = surface ? SDL_CreateSoftwareRenderer(surface) : NULL;
if (r == NULL) {
fprintf(stderr, "queens_view: %s\n", SDL_GetError());
SDL_FreeSurface(surface);
return 1;
}
draw_board(r, b);
SDL_RenderPresent(r);
const int ok = SDL_SaveBMP(surface, path) == 0;
if (!ok)
fprintf(stderr, "queens_view: %s\n", SDL_GetError());
SDL_DestroyRenderer(r);
SDL_FreeSurface(surface);
return ok ? 0 : 1;
}
static void set_title(SDL_Window *w, int n, size_t i, size_t count)
{
char title[96];
snprintf(title, sizeof title, "%d queens: solution %zu of %zu", n, i + 1, count);
SDL_SetWindowTitle(w, title);
}
static int browse(const solution_list *list, int n)
{
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
fprintf(stderr, "queens_view: %s\n", SDL_GetError());
return 1;
}
const int side = 2 * MARGIN + n * SQUARE;
SDL_Window *w = SDL_CreateWindow("queens", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, side, side, 0);
SDL_Renderer *r = w ? SDL_CreateRenderer(w, -1, 0) : NULL;
if (r == NULL) {
fprintf(stderr, "queens_view: %s\n", SDL_GetError());
if (w != NULL)
SDL_DestroyWindow(w);
SDL_Quit();
return 1;
}
size_t i = 0;
bool redraw = true, quit = false;
while (!quit) {
if (redraw) {
set_title(w, n, i, list->count);
draw_board(r, &list->items[i]);
SDL_RenderPresent(r);
redraw = false;
}
SDL_Event e;
if (!SDL_WaitEvent(&e)) /* sleeps until there is input */
break;
if (e.type == SDL_QUIT)
quit = true;
else if (e.type == SDL_WINDOWEVENT && e.window.event == SDL_WINDOWEVENT_EXPOSED)
redraw = true;
else if (e.type == SDL_KEYDOWN) {
switch (e.key.keysym.sym) {
case SDLK_RIGHT: case SDLK_SPACE: i = (i + 1) % list->count; redraw = true; break;
case SDLK_LEFT: i = (i + list->count - 1) % list->count; redraw = true; break;
case SDLK_HOME: i = 0; redraw = true; break;
case SDLK_ESCAPE: case SDLK_q: quit = true; break;
default: break;
}
}
}
SDL_DestroyRenderer(r);
SDL_DestroyWindow(w);
SDL_Quit();
return 0;
}
static bool parse(const char *s, long low, long high, long *out)
{
char *end;
errno = 0;
const long v = strtol(s, &end, 10);
if (*s < '0' || *s > '9' || *end != '\0' || errno != 0 || v < low || v > high)
return false;
*out = v;
return true;
}
int main(int argc, char **argv)
{
long n = 8, k = 0;
const bool shot = argc == 5 && strcmp(argv[2], "--screenshot") == 0;
if (!((argc == 1) || (argc == 2 && parse(argv[1], 4, 12, &n)) ||
(shot && parse(argv[1], 4, 12, &n) && parse(argv[3], 1, 1000000, &k)))) {
fprintf(stderr, "usage: queens_view [N] or queens_view N --screenshot K FILE\n"
" (N is 4 to 12)\n");
return 2;
}
solution_list list = {0};
queens_stats stats;
queens_solve((int)n, collect, &list, &stats);
if (list.count != stats.solutions) {
fprintf(stderr, "queens_view: out of memory\n");
free(list.items);
return 1;
}
int status;
if (shot) {
if ((unsigned long long)k > list.count) {
fprintf(stderr, "queens_view: there are only %zu solutions\n", list.count);
free(list.items);
return 2;
}
status = screenshot(&list.items[k - 1], argv[4]);
} else {
status = browse(&list, (int)n);
}
free(list.items);
return status;
}
The program finds every solution first and stores them, then opens the window. The arrow keys and Space move between solutions, Home returns to the first, and the window title shows which one is on screen. SDL_WaitEvent() sleeps until there is input, so the program uses no CPU while it waits.
All drawing goes through draw_board(), and the queens are positioned from the same SQUARE and MARGIN constants as the squares, so a queen cannot end up beside its square. The same function draws into a window or, with --screenshot, into an off-screen image through SDL_CreateSoftwareRenderer(), which needs no display at all. That is how this image was made:
[IN-CONTENT IMAGE #2 — eight-queens-sdl2-solution-1.webp, the viewer’s own screenshot of solution 1. See Publishing fields.]
$ ./queens_view 8 --screenshot 1 solution1.bmp
The screenshot is also a test. A small program loads it and checks that queen-green pixels appear in exactly the eight squares of solution 1 and nowhere else, so a drawing mistake that put every queen one square to the right fails the build. A second test opens the real window in a virtual X display, presses Right, Right and Left, and checks that the title reads solution 3 and then solution 2.
To build the viewer, install SDL2 first: libsdl2-dev on Debian and Ubuntu, brew install sdl2 on macOS, or the development package from the SDL releases page on Windows.
gcc -std=c11 -Wall -Wextra -O2 src/queens_view.c src/queens.c $(pkg-config --cflags --libs sdl2) -o queens_view
./queens_view 8
Pitfall: A Backtracking Loop That Never Stops
Backtracking is often written as a loop instead of recursion: an array x[] holds the column for each row, k is the current row, and the loop moves k up and down. The loop must end when the search backtracks past the first row. Leave that condition out and it does not end. never_stops.c in the repository runs such a loop with a guard that reports what happens:
Row index reached -1 after 828 solutions (92 x 9).
When row 1 runs out of columns, k drops to 0, and the loop treats x[0] as one more row. The search starts over from the beginning, finds all 92 solutions again, and does it nine times before k reaches -1. Without the guard, the next step reads x[-1], outside the array, which is undefined behavior. A program that prints each solution would show the same 92 boards repeating with no sign of a problem.
The recursive version in queens.c cannot make this mistake: when the loop in row 0 finishes, the first call returns and the search is over. If you do write it as a loop, the condition is while (k >= 1), and a test that counts solutions, expecting exactly 92, catches the missing exit at once.
Make It Your Own
- Bitmasks. Replace the three
boolarrays with three integers and use bit operations; the available squares in a row become one expression,~(cols | diag | anti) & ((1 << n) - 1). It is the fastest common approach. - Larger boards. Raise
QUEENS_MAXand runqueens_statsfurther. The count for 14 × 14 is 365,596 and takes about two seconds; each further size takes several times longer. - One solution, fast. For very large boards, stop at the first solution by returning
falsefrom the callback, or use a constructive formula that places queens directly without searching. - Animate the search. Draw the board inside
place_row()with a short delay, so the window shows queens being placed and taken back. - Other puzzles. The same row-by-row backtracking solves sudoku and the knight’s tour, and the same divide-into-smaller-problems recursion drives the Towers of Hanoi.
Key Takeaways
- The eight queens puzzle has 92 solutions, 12 distinct under rotation and reflection. Every board size has solutions except 2 × 2 and 3 × 3.
- Backtracking abandons a partial board the moment two queens attack, so the 8 × 8 search tests 15,720 squares, the same count Wirth published in 1976, instead of checking 40,320 complete arrangements.
- One flag per column and per diagonal makes each attack check three lookups.
row + colandrow - col + n - 1number the diagonals. - Generate all eight symmetries carefully. Applying the operations in the wrong order produced 17 distinct solutions instead of 12; the tests compare against the published counts.
- An iterative backtracking loop needs an exit when it backtracks past the first row; without one it repeats every solution and then indexes outside its array.
- Draw and test through the same function. The SDL2 viewer renders off-screen for a pixel test that checks every queen is in the right square.
Frequently Asked Questions
Conclusion
The eight queens puzzle has lasted because it teaches the whole idea of backtracking in one screen of code: make a choice, check it immediately, and undo it the moment it cannot lead anywhere. The numbers show why that matters, 15,720 squares tested instead of billions of arrangements, and why the details matter too, from the order of the symmetries to the exit condition of a loop.
The same structure carries over to sudoku, graph coloring and scheduling problems, where the board is different but the choose, check and undo loop is the same. More algorithms with tested code are in the algorithms section.
Source Code and Tests
All programs on this page are in the recursion/eight-queens directory of the MYCPLUS C examples repository.
Build and test:
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure
What the build checks. It compiles with GCC and Clang under C11 and C17 on Linux and the solver with MSVC on Windows, treating warnings as errors. The unit tests compare the solution and distinct-solution counts for every board from 1 × 1 to 12 × 12 with the published sequences, check that every solution is valid, and check the first solution, the 15,720 squares tested and the 2,056 placements. queens, queens_stats and the pitfall must print exactly the output on this page, and bad arguments must be rejected. On Linux, with SDL2 installed, the viewer’s screenshot must have queens in exactly the right squares, and the viewer must respond to key presses in a virtual X display. Everything runs under AddressSanitizer and UndefinedBehaviorSanitizer, and the project also builds without SDL2. The timings above were measured once and are not part of the build.




