Learning SDL3 with C23 by doing the oldest trick in graphics: keep a plain array of pixels in memory, draw into it with your own code, and hand the whole thing to the GPU once per frame.
src/main.c owns the window, the event loop and the frame pacing.
framelib is the drawing layer — it knows nothing about SDL, it just writes
32-bit colors into a flat array.
The framebuffer is a uint32_t[320 * 200]. Every frame it is uploaded to a
streaming texture, which the GPU then scales up to fill the window:
framebuffer SDL_UpdateTexture SDL_RenderTexture
uint32_t[320*200] ──────────────────────► texture ──────────────► window
(320x200) (1280x800)
The window is 4x the framebuffer and the texture uses
SDL_SCALEMODE_NEAREST, so pixels stay crisp blocks instead of being blurred
by interpolation.
The loop is capped at 60 fps using SDL_GetPerformanceCounter to measure the
frame and SDL_DelayPrecise to sleep off the remainder. SDL_Delay would do
too, but it only takes whole milliseconds: a 16.67 ms budget rounds to 16 or
17 and the frame time jitters. SDL_DelayPrecise takes nanoseconds and spins
for the last stretch rather than handing it back to the scheduler.
macOS with Homebrew:
brew install sdl3 gccSDL3 is found through pkg-config, so no paths are hardcoded for it.
make # build into build/
make run # build, then run
make sanitize # build and run under AddressSanitizer + UBSan
make format # rewrite the sources using .clang-format
make clean # remove build/make sanitize builds into build/sanitize/ so the instrumented objects never
mix with the plain ones. It is the quickest way to catch an fb_put_pixel call
that runs off the end of the framebuffer — the function does no bounds checking
by design, and without ASan such a write just corrupts whatever follows.
The compiler defaults to the newest gcc-NN in your Homebrew prefix, falling
back to cc if there is none. Override it whenever you like:
make CC=cc # Apple clang instead
make -j$(sysctl -n hw.ncpu) # parallel buildinclude/framelib.h is documented with Doxygen comments, in the same style
SDL3 uses in its own headers.
brew install doxygen # once
make docs # generate build/docs/html
make docs-open # generate, then open it in a browserThe generated site uses this README as its landing page. Output goes under
build/, so it is never committed, and make clean removes it along with the
object files.
├── Makefile one non-recursive makefile for the whole project
├── Doxyfile documentation settings
├── compile_flags.txt tells clangd how to parse the sources
├── .clang-format formatting rules
├── include/
│ └── framelib.h framelib's public API
├── src/
│ ├── main.c window, event loop, frame pacing
│ └── framelib.c drawing primitives
└── build/ all build output (gitignored)
There is deliberately one Makefile rather than one per directory.
Recursive make cannot see dependencies across directory boundaries, so
editing include/framelib.h would leave a stale main.o behind. Here a
single makefile uses -MMD -MP to generate real header dependencies, and
touching a header rebuilds exactly what depends on it.
The committed .vscode/ config uses clangd for IntelliSense and disables
the C/C++ extension's own engine, including its Tag Parser fallback. Running
both at once produces duplicated and contradictory diagnostics; the Tag Parser
in particular cannot handle C23 and will report errors inside system headers
that compile perfectly well.
clangd reads compile_flags.txt to learn the include paths and language
standard. The C/C++ extension is still used for debugging (F5), which builds
first and launches under lldb.
This is a macOS/Homebrew setup. compile_flags.txt and
.vscode/c_cpp_properties.json still refer to /opt/homebrew, so building on
Linux means dropping those entries — pkg-config already supplies SDL3's real
location, and the Makefile no longer hardcodes a compiler path: it looks for
the newest Homebrew gcc-NN and falls back to cc.
C23 itself needs GCC ≥ 14 or Clang ≥ 18. Apple Clang from Xcode 16 accepts
-std=c23, so make CC=cc works, but it still lacks [[reproducible]] and
[[unsequenced]] — nothing here uses them.
Apache License 2.0 — see LICENSE.