One of our readers contacted me about an apparently weird behavior he was seeing: when running the code from our tutorial the MTLLibrary
returned nil after a few hundred draw calls. That made me realize I was not taking into consideration the fact that some of the Metal
objects are transient and some are not, according to the Metal documentation. Thanks Mike for bringing this to my attention!
To deal with this matter, we need to re-organize the code, again! And that is always a good thing to do. We need to get the non-transient Metal
objects (devices, queues, data buffers, textures, states and pipelines) out of drawRect(_:) and put them in a method that only runs once when the view loads. The command buffers and encoders are the only two transient objects designed for a single use, so we can thus create them with each draw call.
We will pick up where we left off in part 5 of the series. To start, let’s create a new method – an initializer – that only runs once when the view is loaded:
required init(coder: NSCoder) {
super.init(coder: coder)
device = MTLCreateSystemDefaultDevice()
createBuffers()
registerShaders()
}
Next, delete the render() method as well as its call inside drawRect(_:)
as we don’t need it anymore. Then move all the code from sendToGPU() to drawRect(_:)
and delete sendToGPU()
as we don’t need this one either. This way we moved all the non-transient objects away from drawRect(_:)
and only kept the command buffer
and encoder
inside, which are the only two transient objects.
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
if let rpd = currentRenderPassDescriptor, drawable = currentDrawable {
rpd.colorAttachments[0].clearColor = MTLClearColorMake(0.5, 0.5, 0.5, 1.0)
let command_buffer = device!.newCommandQueue().commandBuffer()
let command_encoder = command_buffer.renderCommandEncoderWithDescriptor(rpd)
command_encoder.setRenderPipelineState(rps)
command_encoder.setVertexBuffer(vertex_buffer, offset: 0, atIndex: 0)
command_encoder.setVertexBuffer(uniform_buffer, offset: 0, atIndex: 1)
command_encoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
command_encoder.endEncoding()
command_buffer.presentDrawable(drawable)
command_buffer.commit()
}
}
Finally, let’s create a new class named MathUtils and move both structs
to it so we have a cleaner view class.
import simd
struct Vertex {
var position: vector_float4
var color: vector_float4
}
struct Matrix {
var m: [Float]
init() {
m = [1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
}
func translationMatrix(var matrix: Matrix, _ position: float3) -> Matrix {
matrix.m[12] = position.x
matrix.m[13] = position.y
matrix.m[14] = position.z
return matrix
}
func scalingMatrix(var matrix: Matrix, _ scale: Float) -> Matrix {
matrix.m[0] = scale
matrix.m[5] = scale
matrix.m[10] = scale
matrix.m[15] = 1.0
return matrix
}
func rotationMatrix(var matrix: Matrix, _ rot: float3) -> Matrix {
matrix.m[0] = cos(rot.y) * cos(rot.z)
matrix.m[4] = cos(rot.z) * sin(rot.x) * sin(rot.y) - cos(rot.x) * sin(rot.z)
matrix.m[8] = cos(rot.x) * cos(rot.z) * sin(rot.y) + sin(rot.x) * sin(rot.z)
matrix.m[1] = cos(rot.y) * sin(rot.z)
matrix.m[5] = cos(rot.x) * cos(rot.z) + sin(rot.x) * sin(rot.y) * sin(rot.z)
matrix.m[9] = -cos(rot.z) * sin(rot.x) + cos(rot.x) * sin(rot.y) * sin(rot.z)
matrix.m[2] = -sin(rot.y)
matrix.m[6] = cos(rot.y) * sin(rot.x)
matrix.m[10] = cos(rot.x) * cos(rot.y)
matrix.m[15] = 1.0
return matrix
}
func modelMatrix(var matrix: Matrix) -> Matrix {
matrix = rotationMatrix(matrix, float3(0.0, 0.0, 0.1))
matrix = scalingMatrix(matrix, 0.25)
matrix = translationMatrix(matrix, float3(0.0, 0.5, 0.0))
return matrix
}
}
Run the program to make sure you are still seeing the glorious triangle as we have seen it in the previous part. The source code is posted on Github as usual.
Until next time!