Lab 0.4 — Metal Bring-up: MTKView to First GPU Capture
← Course 4 syllabus · Module 0 · Prev: « Lab 0.3 · Next: Lab 1.1 »
Goal
Bring up the same “cleared, animated window” as Lab 0.3 — this time in Swift with Metal — and end inside Xcode’s GPU capture, the tool that anchors the whole Metal track. The instructive part is the contrast: everything Lab 0.3 built by hand (instance, device selection, swapchain, layout transitions, semaphores) either collapses into MTKView or disappears into Metal’s automatic hazard tracking. Naming what disappeared, and what you gave up for it, is this lab’s written deliverable — and the mental model for Module 4, where the engine drives both APIs from one C++ core.
Recommended reading
- MbT — the opening chapters: the “Hello, Metal!”/first-render and rendering-pipeline introduction chapters (title-level references; 5th-ed. numbering — confirm against the copy in hand). The book’s project style (SwiftUI/MTKView host + MSL) is this course’s Metal-track style.
- Apple, Metal documentation — “Performing Calculations on a GPU” and “Using Metal to Draw a View’s Contents”; the Metal Best Practices Guide’s persistent-object section (what to create once vs. per frame).
- Apple, Capturing a Metal workload in Xcode (developer documentation) — read before the capture task, not after.
Prerequisites
- Full Xcode installed (command-line tools alone cannot GPU-capture); a signing team selected so the app runs locally.
- Lab 0.3 done — the comparison table at the end needs its notes.
Project & environment setup
- Xcode project
metal-swift/MetalClear/(SwiftUI app hosting anMTKViewviaNSViewRepresentable, or an AppKit app — either is fine; MbT’s template is the path of least resistance). Add the project path to the repo README’s build table. - MSL sources live with the app for now (
.metalfiles compiled by Xcode into the default library); the sharedshaders/convention starts mattering in Module 2 when GLSL and MSL implement the same passes side by side. - Enable the Metal HUD for the app (Xcode scheme → Diagnostics → Metal → “Show graphics overview”, or
MTL_HUD_ENABLED=1) — frame rate and memory on screen from day one.
Where results go:
| Artifact | Path |
|---|---|
| Notes, device table, Vulkan↔︎Metal comparison | labs/lab-0-4/notes.md |
First .gputrace capture, screenshot |
labs/lab-0-4/captures/ |
Background
The Metal object model, mapped onto Lab 0.3’s vocabulary: MTLDevice (physical+logical device in one; on Apple Silicon, the GPU sharing unified memory with the CPU) → MTLCommandQueue (queue) → per-frame MTLCommandBuffer (command buffer) → MTLRenderCommandEncoder (roughly vkCmdBeginRendering…vkCmdEndRendering with the attachment description playing render-pass descriptor). MTKView owns what the swapchain lab hand-rolled: the CAMetalLayer, drawable acquisition, pixel format, and the draw/resize callbacks.
Two philosophical differences to observe now and measure later:
- Hazard tracking. Metal tracks resource dependencies automatically by default (
hazardTrackingMode); Vulkan makes you write every barrier. Convenience vs. control — Module 4’s render graph is exactly the machinery that buys the control back. - TBDR is visible in the API. The render-pass descriptor’s
loadAction/storeAction(clear/dontCare/store) are not hints: on Apple’s tile-based GPU they decide whether tile memory ever touches DRAM. This lab’s clear screen is the smallest possible demonstration; Lab 5.2 builds a whole deferred renderer inside tile memory.
The frame loop in MTKView’s delegate: current drawable + render-pass descriptor arrive ready-made; you encode, present, commit. A CADisplayLink-driven 60/120 Hz cadence replaces Lab 0.3’s FIFO analysis.
Tasks
- Device interrogation. Create the system default
MTLDevice; log and record a properties table: name,supportsFamilyresults across the Apple-family GPUs, unified-memory flag,recommendedMaxWorkingSetSize, max threads per threadgroup. This is the Metal twin of Lab 0.3’s table. - The view. Host an
MTKView: set device, pixel format (note the sRGB choice matching Lab 0.3), clear color; implement the delegate with an empty encoder pass so the clear happens purely vialoadAction = .clear. - Animate. Drive the clear color from elapsed time (same animation as Lab 0.3 — the two windows side by side should be indistinguishable). Handle resize via the delegate callback and confirm Retina drawable size vs. view bounds, mirroring 0.3’s extent distinction.
- First capture. Run under Xcode, press the Metal camera button, capture one frame. In the capture: find your command buffer, the render pass, its attachments and actions; screenshot the dependency view. Export the
.gputracetocaptures/. - Counters preview. In the capture’s GPU timeline, note where the frame’s ~“nothing” cost goes; toggle
storeActionbetween.storeand.dontCareon the (unused) depth attachment you temporarily add, re-capture, and find the difference in the memory traffic counters — the TBDR lesson in miniature. Remove the scratch attachment after. - The comparison table. Close
notes.mdwith the deliverable table: for each concept (instance/loader, device selection, swapchain/drawable, image layout, sync trio, validation), one row — how Vulkan spelled it, how Metal spelled it, who does the work, what it costs to not control it. Keep it to a page; Module 4 turns this table into an abstraction layer.
Deliverable & expected results
- The Swift app clearing/animating at display rate with the Metal HUD showing, plus a saved
.gputraceof one frame. - The device table and the Vulkan↔︎Metal comparison table in
notes.md.
| Quantity | Predicted | Measured |
|---|---|---|
| Frame rate (HUD) | display refresh — 60, or 120 on ProMotion | … |
Lines of code vs. Lab 0.3’s vk_clear |
a small fraction — record the actual ratio | … |
| GPU time for the clear pass (capture timeline) | ~tens of microseconds | … |
| Store-action toggle visible in memory-traffic counters | yes — dontCare writes ~nothing |
… |
Profiling & performance
Xcode GPU capture is the instrument being learned; the deliverable is the capture. Also open Instruments → Metal System Trace once on this trivial app just to see the shape of an uncontended CPU→GPU→display pipeline — the baseline picture that Lab 6.2 will show contended and Lab 6.4 will show fixed.
Analysis & reconciliation
Reconcile the two bring-ups in prose, using your comparison table: which Vulkan machinery was essential complexity (something Metal still does, hidden) and which was API ceremony? Where did MTKView make a decision you’d made deliberately in 0.3 (image count, present cadence, format), and what did it choose? State, with the store-action counter evidence, one sentence on why load/store actions exist on a TBDR GPU.
Going further
- Rebuild the same clear without
MTKView:CAMetalLayer+ your own display link — the shape a metal-cpp backend must take in Module 4, since metal-cpp has no MTKView. - Try
MTLCaptureManagerfor programmatic capture (trigger from a key press) — the workflow that scales past toy apps. - Read one WWDC TBDR session (“Metal for Apple GPUs” family) and annotate your store-action experiment against its tile-memory diagrams.