Metal API, vertex interleaved buffer

Haven’t updated my blog for years, here’s a new beginning.

Started playing around with Apple’s Metal API recently, and found a lack of information on vertex interleaved buffer, so I thought I could write a post about this helping others who also had issues using an interleaved buffer.

As to why I want an interleaved buffer instead of using multiple buffers, its for better memory locality so that so that we can store the attributes of that vertex in registers, instead of fetching them from global memory. Pardon my terminology as they are heavily Nvidia influenced. More details about this [1] [2].

So to get this working, start off defining a struct, I am just doing a simple 2D triangle with position and color specified per vertex. I also instantiate an array of VertexAttrib using vertices in anti-clockwise with different colors.

struct VertexAttrib
    var position: float4
    var color: float4

var test: [VertexAttrib] =
    [VertexAttrib(position: float4(x: 0.0, y: 0.0, z: 0.0, w: 1.0),
                  color:    float4(x: 1.0, y: 0.0, z: 0.0, w: 1.0)),
     VertexAttrib(position: float4(x: 1.0, y: 0.0, z: 0.0, w: 1.0),
                  color:    float4(x: 0.0, y: 1.0, z: 0.0, w: 1.0)),
     VertexAttrib(position: float4(x: 0.0, y: 1.0, z: 0.0, w: 1.0),
                  color:    float4(x: 0.0, y: 0.0, z: 1.0, w: 1.0))]

A similar struct needs to be defined for metal shader as an input parameter feeding into the vertex shader. Because we are using vertex attribute, i.e. multiple attributes defined for the vertex, so we have to specify attribute qualifiers for position and color. The vertex shader passThroughVertex receives a vertex ID and VertexIn marked as stage_in as parameters. VertexIn is stage_in in this case because the entire struct is considered as a per vertex input.

struct VertexIn
    float4  position [[attribute(0)]];
    float4  color    [[attribute(1)]];

vertex VertexOut passThroughVertex(uint vid [[ vertex_id ]],
                                   VertexIn v  [[ stage_in ]])
    VertexOut outVertex;
    outVertex.position = v.position;
    outVertex.color    = v.color;
    return outVertex;

I am going to skip irrelevant parts to avoid redundant code. A vertex descriptor (MTLVertexDescriptor) needs to be specified to match the attribute qualifiers specified. If you are familiar with OpenGL/DirectX, then this is pretty straight forward, format is simply the vertex format we have defined in the struct. Offset is the offset in bytes to the first element of the attribute from the start of the array. We are using only one vertex buffer which we are going to set at index 0 (later), so buffer index is set to 0.

One layout has to be specified per vertex buffer, so we have to initialize one layout here. The stride here is the total bytes per vertex, and stepFunction specifies that this struct changes per vertex. We then set the pipeline state descriptor to use this vertex descriptor.

// create vertex descriptor
let vertexDescriptor : MTLVertexDescriptor = MTLVertexDescriptor()
vertexDescriptor.attributes[0].format = MTLVertexFormat.float4
vertexDescriptor.attributes[0].offset = 0
vertexDescriptor.attributes[0].bufferIndex = 0;
vertexDescriptor.attributes[1].format = MTLVertexFormat.float4
vertexDescriptor.attributes[1].offset = MemoryLayout.size(ofValue: test[0].position)
vertexDescriptor.attributes[1].bufferIndex = 0;
vertexDescriptor.layouts[0].stride = MemoryLayout<VertexAttrib>.size
vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunction.perVertex

let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = vertexProgram
pipelineStateDescriptor.fragmentFunction = fragmentProgram
pipelineStateDescriptor.vertexDescriptor = vertexDescriptor
pipelineStateDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormat.bgra8Unorm
pipelineStateDescriptor.sampleCount = view.sampleCount
_pipelineState = device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)

In the render loop where we have to create a command encoder and set the pipelineState and the vertexBuffer before we draw the triangle. Note that for setVertexBuffer() we set the vertex buffer at index 0, so the attributes are referring to the buffer at this location.

let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: d)
encoder.setVertexBuffer(vertexBuffer, offset: 0, at: 0)
encoder.drawPrimitives(type: MTLPrimitiveType.triangle, vertexStart: 0, vertexCount: 3)

And this should be the result:

Vertex descriptor test for metal

Leave a Reply

Your email address will not be published. Required fields are marked *