Lab 0.3 — Vulkan Bring-up: Instance to Cleared Swapchain

Course 4 syllabus · Module 0 · Prev: « Lab 0.2 · Next: Lab 0.4 »

Goal

Bring Vulkan up from nothing on the Mac: SDK verification, an instance with validation layers, physical-device selection (and the portability subset dance MoltenVK requires), a logical device and queues, a GLFW window and surface, and a swapchain cleared to an animated color with correct frame synchronization. No triangle yet — deliberately. The object of this lab is Vulkan’s machinery: who owns what, what must be synchronized with what, and how the validation layers turn silent misuse into named errors. Every rendering lab in Modules 2–7 stands on the loop built here.

Prerequisites

  • Lab 0.1 skeleton building; GLFW and volk already declared in the dependency block.
  • LunarG Vulkan SDK installed on the Mac with its environment sourced (VULKAN_SDK set, vulkaninfo on PATH).

Project & environment setup

  • New engine component engine/vulkan/ (target engine_vulkan) plus lab executable labs/lab-0-3/vk_clear.
  • Loader policy: link volk, not the loader directly; volkInitialize() → instance → volkLoadInstance → device → volkLoadDevice.
  • Environment check before any code: vulkaninfo --summary must show a MoltenVK-backed device (apiVersion 1.2+, driverName MoltenVK). Save the full vulkaninfo dump to labs/lab-0-3/captures/vulkaninfo.txt.

Where results go:

Artifact Path
Notes, device-properties table, validation-error postmortems labs/lab-0-3/notes.md
vulkaninfo dump, screenshots labs/lab-0-3/captures/

Background

The Vulkan object hierarchy this lab instantiates, in dependency order: instance (the API connection; where layers live) → physical device (enumerated hardware) → logical device (your configured handle to it; where queues come from) → surface (the window, via GLFW) → swapchain (the presentable images). Three Mac-specific facts:

  • MoltenVK is a layered implementation of Vulkan over Metal, and it is not fully conformant — so instance creation must pass VK_KHR_portability_enumeration (+ the enumerate-portability flag), and the device must enable VK_KHR_portability_subset if exposed. Treat every capability you use as something to query, a discipline that pays again on the Linux desktop (RTX 4090).
  • Dynamic rendering (VK_KHR_dynamic_rendering, core in 1.3, supported by MoltenVK) replaces render-pass/framebuffer objects for this course’s purposes: begin rendering against attachment info, draw, end. Less machinery now, and closer to how Metal thinks — a comparison Lab 0.4 makes explicit.
  • The synchronization trio per frame: an image-available semaphore (GPU⇄GPU: presentation engine → your submission), a render-finished semaphore (your submission → present), and an in-flight fence (GPU → CPU: don’t reuse this frame’s command buffer until the GPU is done). Getting these three right — and being able to say why each exists — is the actual point of the lab.

Validation layers are the other half of the curriculum: VK_LAYER_KHRONOS_validation with the debug-utils messenger, plus synchronization validation switched on. This course’s rule: validation clean at every commit.

Tasks

  1. Instance + validation. Create the instance with portability enumeration, the validation layer, and a debug messenger that routes to the engine log with severity mapped to log level. Confirm a deliberate mistake (destroy the instance twice in a scratch build) produces a named validation error, then remove it.
  2. Device selection. Enumerate physical devices; print and record a properties table (device name, apiVersion, driver, queue families with flags, memory heaps with sizes). Select by explicit scoring (graphics+present queue required), not “first device”.
  3. Logical device + queues. Enable required extensions (VK_KHR_swapchain, portability subset if present, dynamic rendering if not core at your apiVersion); retrieve graphics and present queue handles.
  4. Surface + swapchain. GLFW window (no OpenGL context hint); choose surface format (prefer sRGB — tie back to Course 1 §16), present mode (start FIFO), image count, and extent, handling the Retina framebuffer-size ≠ window-size distinction. Save your choices and why in notes.md.
  5. The clear loop. Command pool + per-frame command buffers; per-frame sync trio; record: acquire → transition image to COLOR_ATTACHMENT_OPTIMALvkCmdBeginRendering with a clear color animated over time → end → transition to PRESENT_SRC → submit → present. Two frames in flight.
  6. Resize & teardown. Handle window resize (swapchain recreation) and clean shutdown (vkDeviceWaitIdle before destruction, everything through Lab 0.2’s RAII wrappers). Validation must be silent through open-resize-close.
  7. Break it on purpose (postmortem × 3). Separately: omit the image-available semaphore wait; skip the layout transition to PRESENT_SRC; destroy a semaphore still in use. For each, record the exact validation message in notes.md and the one-sentence story of what race it names.

Deliverable & expected results

  • vk_clear opening a window with a smoothly animating clear color, resizable, closing cleanly, zero validation messages.
  • The device-properties table and the three postmortems in notes.md; vulkaninfo.txt and a screenshot in captures/.
Quantity Predicted Measured
Reported apiVersion / driver on the Mac Vulkan 1.2+, MoltenVK driver string
Swapchain min image count granted (FIFO) 2–3
Frame rate with FIFO present mode pinned at display refresh (60 / ProMotion 120)
CPU time per frame in the clear loop (Tracy zone) tens of microseconds — this is the floor everything later adds to

Profiling & performance

Wrap the frame loop in Tracy zones now (acquire, record, submit, present) and keep them forever: every later lab’s frame cost is read against this floor. Capture one trace showing the FIFO cadence — the present call blocking to refresh — and save it; Lab 6.1 revisits present modes and pacing properly.

Analysis & reconciliation

In notes.md: draw the frame’s timeline by hand — acquire, record, submit, present, and where each of the three sync primitives gates it — then reconcile against the Tracy capture. Explain the granted image count vs. your request, and what MoltenVK’s portability subset actually removed on this device (from the feature query, not folklore). File anything mysterious as a question Lab 6.3’s tooling can answer.

Going further

  • Swap FIFO for MAILBOX (if exposed) or IMMEDIATE and watch both the Tracy cadence and the power draw change; formalized in Lab 6.1.
  • Run the same binary tree on the Linux desktop’s RTX 4090 — native Vulkan, no portability machinery — and diff the two device-properties tables: first contact with the course’s two-GPU comparison.
  • Read MoltenVK’s environment-variable reference (MVK_CONFIG_*) and find the switch that logs the Metal objects it creates — a preview of what Lab 0.4 shows from the other side.