Lab 0.1 — CMake & C++20 Project Skeleton
← Course 4 syllabus · Module 0 · Next: Lab 0.2 »
Goal
Take ownership of the build system every later lab lives in: a CMake ≥ 3.28 / C++20 project with presets, a pinned dependency set fetched by the build, a strict warning set, sanitizers, Tracy and Google Benchmark wired in from the first commit. The workspace ships fully scaffolded in the labs repo (course4/ — see the repository structure) so no session is ever spent fighting the build — but a build system you can’t explain is one you can’t extend, so this lab builds every preset, traces how the targets connect, and proves each piece of measurement infrastructure works. The habit this lab installs is the course’s defining one: measurement infrastructure is part of the skeleton, not an afterthought — an empty engine already has a profiler timeline and a benchmark harness.
Recommended reading
- CMake documentation — the CMake Tutorial through “Adding Usage Requirements”, plus the
FetchContentandCMakePresets.jsonreference pages. Read for the target-based model: everything in this course istarget_link_librarieson well-defined targets, never global flags. - Motta — the opening chapter on setting up a GPU development environment (title-level reference; confirm against the copy in hand). Light read; the CUDA half of the toolchain is exercised in Module 1.
- Course 3 Part IV — the dialect-pinning and warnings-as-contract discipline being carried from C17/18 to C++20.
Prerequisites
- The syllabus toolchain section done on the Mac:
cmake,ninja, Xcode command-line tools installed and onPATH. - The companion repo
diiv_website_custom_coursescloned; all work happens in itscourse4/folder.
Project & environment setup
The environment is this lab’s subject; the layout is the syllabus’s repository structure and the quickstart is course4/README.md. The dependency set, pre-pinned in cmake/Dependencies.cmake (bump deliberately, and record why in notes.md):
| Dependency | Fetched via | Used from |
|---|---|---|
| GLM | FetchContent |
Lab 0.2 (math conventions), then everywhere |
| GLFW | FetchContent |
Lab 0.3 (Vulkan windowing) |
| Tracy (client + server) | FetchContent (client); server app built once separately |
This lab onward |
| Google Benchmark | FetchContent |
This lab onward |
| stb, tinygltf, VMA, volk | FetchContent |
Modules 2+ (declared now, used later) |
Where results go:
| Artifact | Path |
|---|---|
| Build/skeleton note, predicted-vs-measured | labs/lab-0-1/notes.md |
| First Tracy capture | labs/lab-0-1/captures/ |
| First benchmark JSON | labs/lab-0-1/benchmarks/ |
Background
Three ideas carry the whole course’s build system:
- Targets, not directories. A modern CMake project is a graph of targets with usage requirements:
engine_coredeclares its include paths, itscxx_std_20requirement, and its dependencies as properties, and anything that links it inherits them. Per-lab executables then become three-lineCMakeLists.txtentries. - Presets make builds reproducible.
CMakePresets.jsonpins generator, build type, and cache variables so “the debug build” and “the release-with-sanitizers build” are named, versioned objects — the same idea as Course 2 pinning.iocsettings. - The observability floor. Tracy’s client is a header and a macro (
ZoneScoped); Google Benchmark is aBENCHMARK()registration. Compiled in from day one (and compiled out cleanly via a CMake option), they cost nothing until used — and every later lab uses them.
A warning set worth pinning for engine C++20: -Wall -Wextra -Wpedantic -Wconversion -Wshadow, warnings-as-errors in CI-style release builds only. Sanitizer presets: ASan+UBSan for debug runs (Metal/Vulkan drivers throw some benign noise — the suppression file is part of the skeleton).
Tasks
- Read the scaffold like a reviewer, top down: the top-level
CMakeLists.txt,cmake/CompilerOptions.cmake(thecourse4_optionsinterface target carryingcxx_std_20and the warning set — note that nothing in the tree sets a global flag),cmake/Dependencies.cmake(FetchContent, every dependency pinned), andlabs/CMakeLists.txt(how a lab folder becomes a target). Innotes.md, sketch the target graph fromlab_0_1down toglm— if you can’t draw it, you don’t own it yet. - Build every preset —
debug(sanitizers ON),release,profile(TRACY_ENABLE) — withcmake --preset <name> && cmake --build --preset <name>, and run./build/<name>/labs/lab-0-1/lab_0_1from each. Record the three configure/build times. - Interrogate the pins: for two dependencies (say GLM and Tracy), find where the tag is pinned, confirm the fetched source landed under
build/<preset>/_deps/, and write down the one-line policy for when a pin may move. - Replace the
lab_0_1stub body: keepengine_core’s logger, open no window, run a few million iterations of trivial work insideZoneScopedTracy zones, and exit. - Run the Tracy server, capture
lab_0_1under theprofilepreset, and save the trace. - Add a first Google Benchmark target benchmarking something honest and small — e.g.
glm::mat4multiply chains vs. a naive hand-rolled 4×4 multiply — and dump JSON results. (The point is the harness, not the numbers — but the numbers preview Lab 0.2’s discussion of what the optimizer does to math code.) - Prove the sanitizers work: introduce a deliberate use-after-free in a scratch target, watch ASan catch it under the
debugpreset, then delete the scratch target.
Deliverable & expected results
- A repository where
git clone→cmake --preset release→cmake --build --preset releasesucceeds on a clean machine with no manually installed dependencies beyond the toolchain. - A Tracy trace showing named zones from
lab_0_1, saved tocaptures/. - Benchmark JSON in
benchmarks/, referenced fromnotes.md.
| Quantity | Predicted | Measured |
|---|---|---|
| Clean configure time (release preset, warm network cache) | tens of seconds — FetchContent clones dominate | … |
| Clean build time, release | single-digit minutes on the M-series | … |
No-op incremental build after touching one .cpp |
under a second with Ninja | … |
glm::mat4 multiply vs. naive 4×4 (release) |
same order of magnitude; check the disassembly before believing either | … |
Profiling & performance
This lab is the profiling setup. Two checks before calling it done: the Tracy timeline shows your zone names (not just frames), and benchmark output is stable across three runs (pin significance with --benchmark_repetitions=10 and compare medians). Record both artifacts’ paths in notes.md.
Analysis & reconciliation
In notes.md, answer three questions with evidence: Which preset differences actually changed the benchmark numbers, and by how much? What did the optimizer do to the naive 4×4 multiply (read the disassembly — Course 3 Module 2’s -O0 vs -O2 forensics, now in C++)? What does an empty Tracy-instrumented frame cost, i.e. what is the observability overhead you’ll be carrying?
Going further
- Add
clang-format+clang-tidyconfigs and aformat/tidypreset; pin the tidy check list to the modernize/bugprone families you actually want. - Try
ccacheviaCMAKE_CXX_COMPILER_LAUNCHERand measure the rebuild delta. - Configure the same tree on the Linux desktop (RTX 4090) now — finding the first portability wart early is cheaper than finding it in Module 1.