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.
Recommended reading
- Vulkan Tutorial (vulkan-tutorial.com) — “Drawing a triangle → Setup” and “Presentation” parts: instance, validation layers, devices and queues, window surface, swapchain. Read before coding; the lab’s tasks deviate from it where modern Vulkan does (dynamic rendering,
volk, VMA later). - vkguide.dev — the “new chapter” initialization sections, for the 1.3-era shape of the same steps (dynamic rendering instead of render passes) and honest commentary on what boilerplate to tolerate.
- LunarG’s MoltenVK user guide — the portability-enumeration and portability-subset sections; ten minutes that save an evening of
VK_ERROR_INCOMPATIBLE_DRIVER. - C&S — skim the engine-setup chapter (title-level reference) to see where this boilerplate ends up living in a real engine; Module 4 returns to it properly.
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_SDKset,vulkaninfoonPATH).
Project & environment setup
- New engine component
engine/vulkan/(targetengine_vulkan) plus lab executablelabs/lab-0-3/→vk_clear. - Loader policy: link volk, not the loader directly;
volkInitialize()→ instance →volkLoadInstance→ device →volkLoadDevice. - Environment check before any code:
vulkaninfo --summarymust show a MoltenVK-backed device (apiVersion1.2+,driverNameMoltenVK). Save the fullvulkaninfodump tolabs/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 enableVK_KHR_portability_subsetif 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
- 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.
- 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”. - Logical device + queues. Enable required extensions (
VK_KHR_swapchain, portability subset if present, dynamic rendering if not core at yourapiVersion); retrieve graphics and present queue handles. - 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. - The clear loop. Command pool + per-frame command buffers; per-frame sync trio; record: acquire → transition image to
COLOR_ATTACHMENT_OPTIMAL→vkCmdBeginRenderingwith a clear color animated over time → end → transition toPRESENT_SRC→ submit → present. Two frames in flight. - Resize & teardown. Handle window resize (swapchain recreation) and clean shutdown (
vkDeviceWaitIdlebefore destruction, everything through Lab 0.2’s RAII wrappers). Validation must be silent through open-resize-close. - 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 innotes.mdand the one-sentence story of what race it names.
Deliverable & expected results
vk_clearopening 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.txtand a screenshot incaptures/.
| 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) orIMMEDIATEand 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.