
If you are chasing a vertex offset mistake or a fragment output that looks wrong, Xcode's GPU Frame Capture and shader debugger are the fastest way to find it. Both tools live in the Xcode GUI on macOS, as described in Apple's Metal debugger documentation. This guide shows how to set them up on a rented M-series Mac reached over screen sharing, so you can step through Metal Shading Language (MSL) without buying Apple hardware.
Direct recommendation: request a Figment M-series Mac from $95/month for Metal shader debugging. The $29 virtual Intel macOS plan runs on Linux/KVM with no GPU acceleration, so there is no Apple GPU for Metal to attach to on that plan. The shader debugger and GPU Timeline view are built against Apple's tile-based GPUs, which in practice means Apple silicon. The physical M-series plan is request-based; the exact chip, RAM, and disk are confirmed in the quote, and current availability is gathered as part of the request. If you also want backups, CI setup, monitoring, and 30 minutes of monthly support bundled around one M-series Mac, the Managed Build Mac is $249/month. Start a request at /mac-hosting/start.
GPU Frame Capture records draw calls from a live MTLDevice. The $29 virtual Intel macOS plan is an Intel macOS environment on Linux/KVM with no GPU acceleration or audio, so there is no device for Metal to capture from, and no Apple GPU for the shader debugger to simulate thread execution against. The shader debugger reads MSL source and Apple GPU state; it is not designed for third-party GPUs. Choose an M-series rental when you need to run and debug Metal, and confirm the specific chip in your quote if a particular GPU family matters.
Intel Xcode 26 or earlier can still compile and sign Metal code for other targets, and the virtual Intel macOS plan is useful for building, signing, and running non-GPU parts of your toolchain. Commands run in its own desktop terminal through the browser console, and the CI pieces of your pipeline can reach out from it to your services. For live Metal debugging, step across to an M-series Mac.
Confirm each of these before you hit the capture button. The details come from Apple's guide to capturing a Metal workload in Xcode.
| Item | Where to set it | Notes |
|---|---|---|
| GPU Frame Capture enabled | Scheme → Run → Options → GPU Frame Capture: Metal | Required for the capture button to appear |
| Metal API Validation | Scheme → Run → Diagnostics → Metal API Validation: Enabled | Surfaces API misuse before you capture |
| Shader debug info | Build Settings → MTL_ENABLE_DEBUG_INFO: Yes, with source map (Debug) | Needed for line-level shader stepping |
| Capture scope (programmatic) | MTLCaptureDescriptor.captureObject = device or command queue | Narrow the capture to one queue when you can |
| GPU Timeline | Apple GPU only | Not available on third-party GPUs |
Keep MTL_ENABLE_DEBUG_INFO off for Release builds so symbols do not ship with your binary. If you need to debug an optimised build, archive the matching .metallibsym separately and keep it with your dSYMs. Exact menu wording and build-setting labels move between Xcode versions; verify against the Xcode you have installed, since Intel Macs are limited to Xcode 26 or earlier per Apple's porting guide.
Figment M-series Macs are reached through the browser console or a VNC client pointed at the machine's screen-sharing address. Use a VNC client with at least 24-bit colour so the debugger's heatmaps, buffer previews, and attachment thumbnails render correctly.
Network latency shows up as paint delay in the viewer, not as debugger inaccuracy. The captured frame is replayed locally on the Mac, so shader stepping does not depend on VNC round-trip time.
With the scheme configured, run the app on the M-series Mac. The Metal capture button appears in the Xcode debug bar once a Metal command queue is detected. Press it and Xcode records the next frame and opens the GPU Frame Capture view.
To trigger a capture programmatically from a specific frame in your renderer:
#if DEBUG
import Metal
func captureOneFrame(commandQueue: MTLCommandQueue) {
let manager = MTLCaptureManager.shared()
let descriptor = MTLCaptureDescriptor()
descriptor.captureObject = commandQueue
descriptor.destination = .developerTools
guard manager.supportsDestination(.developerTools) else { return }
do {
try manager.startCapture(with: descriptor)
} catch {
NSLog("Metal capture failed: \(error)")
}
}
#endif
Call MTLCaptureManager.shared().stopCapture() after the frame you want recorded ends. This example is illustrative, not a benchmark; run it against your own renderer to confirm the capture appears in Xcode.
The GPU debugger's left panel shows the render pass tree. Expand a pass to see its draw calls, pipeline states, and the vertex and fragment functions bound to each one. Click the draw call that produces the pixels you are investigating.
Click a shader function name in the pipeline state row to open the shader debugger. Xcode shows MSL source with a thread picker at the top.
float4 position you can read x, y, z, w at the line that wrote them.For intermittent errors or stalls, open Instruments and choose the Metal System Trace template, then attach to your app process on the remote Mac. Record a few seconds of rendering. The trace shows GPU work duration and gaps, parallel vertex and fragment stage execution on Apple silicon, and per-draw occupancy.
If a vertex pass and the fragment pass that consumes its output show an unexpected gap, it usually points at a missing barrier or fence between a compute write and a vertex read. Select the two encoders in the trace and read the dependency annotation Xcode inserts.
Start a configuration at /mac-hosting/start, or read the plan summary at /mac-hosting.
Related guides: