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.

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 an MTKView via NSViewRepresentable, 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 (.metal files compiled by Xcode into the default library); the shared shaders/ 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 vkCmdBeginRenderingvkCmdEndRendering 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

  1. Device interrogation. Create the system default MTLDevice; log and record a properties table: name, supportsFamily results across the Apple-family GPUs, unified-memory flag, recommendedMaxWorkingSetSize, max threads per threadgroup. This is the Metal twin of Lab 0.3’s table.
  2. 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 via loadAction = .clear.
  3. 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.
  4. 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 .gputrace to captures/.
  5. Counters preview. In the capture’s GPU timeline, note where the frame’s ~“nothing” cost goes; toggle storeAction between .store and .dontCare on 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.
  6. The comparison table. Close notes.md with 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 .gputrace of 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 MTLCaptureManager for 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.