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.

Prerequisites

  • The syllabus toolchain section done on the Mac: cmake, ninja, Xcode command-line tools installed and on PATH.
  • The companion repo diiv_website_custom_courses cloned; all work happens in its course4/ 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_core declares its include paths, its cxx_std_20 requirement, and its dependencies as properties, and anything that links it inherits them. Per-lab executables then become three-line CMakeLists.txt entries.
  • Presets make builds reproducible. CMakePresets.json pins 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 .ioc settings.
  • The observability floor. Tracy’s client is a header and a macro (ZoneScoped); Google Benchmark is a BENCHMARK() 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

  1. Read the scaffold like a reviewer, top down: the top-level CMakeLists.txt, cmake/CompilerOptions.cmake (the course4_options interface target carrying cxx_std_20 and the warning set — note that nothing in the tree sets a global flag), cmake/Dependencies.cmake (FetchContent, every dependency pinned), and labs/CMakeLists.txt (how a lab folder becomes a target). In notes.md, sketch the target graph from lab_0_1 down to glm — if you can’t draw it, you don’t own it yet.
  2. Build every preset — debug (sanitizers ON), release, profile (TRACY_ENABLE) — with cmake --preset <name> && cmake --build --preset <name>, and run ./build/<name>/labs/lab-0-1/lab_0_1 from each. Record the three configure/build times.
  3. 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.
  4. Replace the lab_0_1 stub body: keep engine_core’s logger, open no window, run a few million iterations of trivial work inside ZoneScoped Tracy zones, and exit.
  5. Run the Tracy server, capture lab_0_1 under the profile preset, and save the trace.
  6. Add a first Google Benchmark target benchmarking something honest and small — e.g. glm::mat4 multiply 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.)
  7. Prove the sanitizers work: introduce a deliberate use-after-free in a scratch target, watch ASan catch it under the debug preset, then delete the scratch target.

Deliverable & expected results

  • A repository where git clonecmake --preset releasecmake --build --preset release succeeds on a clean machine with no manually installed dependencies beyond the toolchain.
  • A Tracy trace showing named zones from lab_0_1, saved to captures/.
  • Benchmark JSON in benchmarks/, referenced from notes.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-tidy configs and a format/tidy preset; pin the tidy check list to the modernize/bugprone families you actually want.
  • Try ccache via CMAKE_CXX_COMPILER_LAUNCHER and 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.