Metal By Example’s second post, in Swift

This is Part 2, following along with Warren Moore’s Up and Running with Metal, Part 2: Drawing Triangles. Part 1 is here.

This is a link to my Xcode project. Again, Xcode 6.3 or later is necessary to run it.

Changes to the main class, MetalView

As I did with Warren’s redraw method in the last post, I got rid of the code relying on CADisplayLink. As only a static triangle is drawn, I had no way to visually verify if that code was working, so I just moved everything into MetalView’s initializer, which isn’t that bad, considering how much code I compartmentalized into other files:

Screen Shot 2015-03-12 at 8.53.29 PM

The entirety of the changes in MetalView, since part, 1 is this addition:

commandEncoder.0.setRenderPipelineState(
    RenderPipelineStateProxy(device,
        descriptor: RenderPipelineDescriptor(
            library: LibraryProxy(device).original,
            pixelFormat: pixelFormat
        )
    ).original
)
let position = Int8.max / 2
commandEncoder.1.draw(VertexBufferCollection(
    device: device,
    positions: [
        Vector2(0, position),
        Vector2(-position, -position),
        Vector2(position, -position)
    ],
    colors: [
        Vector3(UInt8.max, 0, 0),
        Vector3(0, UInt8.max, 0),
        Vector3(0, 0, UInt8.max)
    ]
))

Swift doesn’t offer write-only properties

I’d like commandEncoder.setRenderPipelineState to be a set-only property:

commandEncoder.renderPipelineState = renderPipelineState

…but Swift doesn’t have set-only properties, like C# does. There are a bunch of dogmatic arguments for why not to use set-only properties; each one is useless internet clutter. The Metal API doesn’t have to reflect my opinion, but not being able to design my own wrapper around it, in a way that makes sense to me, is irksome.

Apple, that’s not what Victor meant when he said, “if you can write it, you can read it”.

Swift doesn’t offer named subscripts

My draw method above abstracts away this method of RenderCommandEncoder:

setVertexBuffer(vertexBuffer, offset: 0, atIndex: index)

A named subscript would be more appropriate:

subscript vertexBuffers(bufferIndex: Int, argumentIndex: Int = 0, offset: Int = 0)

That would allow us to write this, for example:

commandEncoder.vertexBuffers[0] = positions

But again, because subscripts are properties that take arguments, we’d still encounter the problem with write-only properties.angry-sick

Generic vectors and vertex buffers

My VertexBufferCollection struct represents what I’d call a “3D model”. I wanted to experiment with using different data types for the vertex positions and colors; generics, along with type inference (Vector2<Int8> and Vector3<UInt8> in this case) , made that easy:

init<VPosition: Vector, VColor: Vector>
(device: MTLDevice, positions: [VPosition], colors: [VColor])

The shader had to be updated accordingly:

vertex ColoredVertex vertex_main(
    constant char2 * position [[buffer(0)]],
    constant packed_uchar3 * color [[buffer(1)]],
    uint vid [[vertex_id]]
) {
    return {
        .position = float4(
            float2(position[vid]) / 127,
            0, 1
        ),
        .color = half3(uchar3(color[vid])) / 255
    };
}

Two things that tripped me up

I was nearly ready to use a TSI, for each of these issues, but rewatching Apple’s excellent Metal Fundamentals WWDC presentation, combined with several cycles through the four stages of flow, made that unnecessary.

1. 3-component vector types need to be packed.

Before I learned about the packed_ prefix, for shader data types, I was wondering why my colors buffers were being read as Vector4s, instead of Vector3s, regardless of the backing type of the vector.

2. float/half type constructors don’t convert chars to 0 to 1, without division.

This seems obvious now, but coming from a Unity background, I never had enough control to send anything but floating point values to shaders. I think I figured that, because textures, typically made of four bytes (when uncompressed), provide me with floating point values from 0 to 1 when sampled, I’d get a similar effect from converting from bytes to halfs or floats.

Instead, the interpolation of values much larger than 1, across the triangle, combined with the alpha of the ClearColor not being 1, which makes the background be white, for reasons I don’t understand yet, made me think that instead of triangles,the GPU was drawing lines. The image at the top of this post is a screenshot of this happening.

Flow in the brain

Leave a Reply

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