Cannot debug Metal shaders with Indirect Command Buffers

Using Indirect Command Buffers seems to break XCode's ability to debug Metal shaders.

Attempting to debug a vertex shader either yields errors like Unable to connect to device (6) or Function argument 'vertexBuffer.0' does not have a valid vertex buffer binding at index '0'. I've tried the XCode versions 13.1 (13A1030d) and 13.2 beta 2 (13C5081f).

Is this a known problem/limitation of XCode's Metal Shader debugger?

Are there any workarounds for this?

Are there better ways to "debug" Metal shaders in general?

I thought my code was doing something bad/weird (I'm still new to Metal), but found even the Apple Developer sample code Encoding Indirect Command Buffers on the CPU, XCode cannot debug the shaders.

I've reduced a reproduction to a simple, single file Swift MacOS application drawing a triangle (see Github Repo link below).

Setting up Render Pipeline:

let desc = MTLRenderPipelineDescriptor()
desc.label = "RenderPipeline"
desc.vertexFunction = vertexFn
desc.fragmentFunction = fragFn
desc.colorAttachments[0].pixelFormat = COLOR_PIXEL_FORMAT
desc.supportIndirectCommandBuffers = true
desc.inputPrimitiveTopology = .triangle

let vertexDesc = MTLVertexDescriptor()
vertexDesc.attributes[0].bufferIndex = 0
vertexDesc.attributes[0].offset = 0
vertexDesc.attributes[0].format = .float4
vertexDesc.layouts[0].stride = MemoryLayout<VertexPosition>.stride
vertexDesc.layouts[0].stepRate = 1
vertexDesc.layouts[0].stepFunction = .perVertex
desc.vertexDescriptor = vertexDesc

Setting up Indirect Command Buffer:

let desc = MTLIndirectCommandBufferDescriptor()
desc.commandTypes = .draw
desc.inheritBuffers = false
desc.inheritPipelineState = false
desc.maxVertexBufferBindCount = 1
desc.maxFragmentBufferBindCount = 0

let buf = device.makeIndirectCommandBuffer(descriptor: desc, maxCommandCount: 1, options: MTLResourceOptions.storageModeManaged)!
buf.label = "IndirectCommandBuffer"

let renderEncoder = buf.indirectRenderCommandAt(0)
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexFnArgBuffer, offset: 0, at: 0)
renderEncoder.drawPrimitives(.triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1, baseInstance: 0)

Issue command:

let commandBuffer = commandQueue.makeCommandBuffer()!
commandBuffer.label = "@CommandBuffer"

let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: view.currentRenderPassDescriptor!)!
renderEncoder.label = "@RenderCommandEncoder"
renderEncoder.setViewport(drawableViewport)

renderEncoder.use(vertexFnArgBuffer, usage: .read, stages: .vertex)
renderEncoder.executeCommandsInBuffer(indirectCommandBuffer, range: 0..<1)
renderEncoder.endEncoding()

commandBuffer.present(view.currentDrawable!)
commandBuffer.commit()

The XCode project is pushed to this Github Repo peterwmwong/MetalGPUFrameCaptureDebugRepro and includes a GIF and MOV screen capture of the specific errors I encountered attempting to debug the simple shader.

Any help would be appreciated!

Thanks.

Accepted Reply

ICB shader debugging is currently not available, AFAIK.

Enabling shader validation might help somewhat:

MTL_SHADER_VALIDATION_GPUOPT_ENABLE_INDIRECT_COMMAND_BUFFERS=1

See this issue for more info.

Replies

ICB shader debugging is currently not available, AFAIK.

Enabling shader validation might help somewhat:

MTL_SHADER_VALIDATION_GPUOPT_ENABLE_INDIRECT_COMMAND_BUFFERS=1

See this issue for more info.

Thanks @Qwiff for the link.

This is a bit of a bummer. Given ICB's have been out for a number of years, not sure I should be holding my breath on support anytime soon...

I wonder what Apple's guidance is on development/debug workflow without support... I can only think of:

  • Feature flag app code to switch between using/not-using ICBs (huge pain when ICBs are encoded on the GPU)
  • Whatever the GPU equivalent of "put a printf there" is
  • ... Don't use ICBs for non-trivial things that may change/need-debugging in the future
  • It's indeed a bummer that not all GPU debugging features are supported for ICB workflows. Having said that, the GPU trace does show the buffers, geometry, textures, etc for ICB-issued draw calls, so it's definitely still useful.

    Side note: the equivalent of printf for graphics programming in general is outputting specific channels in your fragment shader. I.e. the values from only albedo map, normal map, ambient occlusion, etc. In my experience with Vulkan, actual text logging from a shader is not practical due to the sheer volume of output.

    My own workflow is to develop a CPU-driven, non-ICB pipeline first, then port it to a GPU-encoded ICB. More work, but it saves time in the end.

  • Thanks again @Qwiff!

    Very true, I'm finding the GPU trace's access to buffers, geometry and textures seems to go a long way. Sharing your workflow/experience is very helpful to me (a n00b). As you said, if I can jam a debug-value in a debug buffer it gets be pretty close... just a bit annoying/time-suck given there's code setup and removal once I figure out what's wrong.

Add a Comment