I am trying to draw (and frequently update) a Polyline with the iOS Metal framework and Swift 4 / iOS 11 / XCode 9. For the final project I want to be able to "draw" a line with my finger, capturing the touch events. I am basically adapting the code from this tutorial Metal Tutorial Swift 3 Part 1, just changing the parts I will describe here. Especially the fragment and vertex shader stayed the same:
vertex float4 basic_vertex(
const device packed_float3* vertex_array [[ buffer(0) ]],
unsigned int vid [[ vertex_id ]]) {
return float4(vertex_array[vid],1.0);
}
fragment half4 basic_fragment() {
return half4(1.0);
}
Basically I am just extending the vertexData
array (which I renamed to stroke
) on each incoming touch event:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let t = touches.first {
let pos = t.location(in: view).applying(transformMatrix)
stroke = []
addStrokePoint(point: pos)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let t = touches.first {
let pos = t.location(in: view).applying(transformMatrix)
addStrokePoint(point: pos)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let t = touches.first {
let pos = t.location(in: view).applying(transformMatrix)
addStrokePoint(point: pos)
}
}
func addStrokePoint(point: CGPoint) {
stroke += [Float(point.x), Float(point.y), 0.0]
}
I know this is not a particular efficient or nice way to define a "stroke", I just wanted to get it up and running quickly. For each additional touch point I am converting the coordinates (px,py)
to normalized device coordinates (x,y)
with a transformation matrix:
transformMatrix = CGAffineTransform(a: 2.0/view.layer.frame.width, b: 0.0, c: 0.0, d: -2.0/view.layer.frame.height, tx: -1.0, ty: 1.0)
For rendering, I am drawing the vertices as primitives of type .lineStrip
which is supposed to "Rasterize a line between each pair of adjacent vertices, resulting in a series of connected lines (also called a polyline)."
func render() {
if stroke.count > 2 {
let dataSize = stroke.count * MemoryLayout.size(ofValue: stroke[0])
vertexBuffer = device.makeBuffer(bytes: stroke, length: dataSize, options: [])
guard let drawable = metalLayer?.nextDrawable() else { return }
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 104.0/255.0, blue: 0.0, alpha: 1.0)
let commandBuffer = commandQueue.makeCommandBuffer()
let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
renderEncoder?.setRenderPipelineState(pipelineState)
renderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder?.drawPrimitives(type: .lineStrip, vertexStart: 0, vertexCount: stroke.count)
renderEncoder?.endEncoding()
commandBuffer?.present(drawable)
commandBuffer?.commit()
}
}
The code compiles and runs on my iPhone 6s device and the line is following my finger quite smoothly. But it seems as if there is some invalid or old data still in the buffer (?) because the rendering flickers and seemingly random additional lines are drawn to the screen.
Additionally, at some point during runtime I get the following error:
Execution of the command buffer was aborted due to an error during execution. Caused GPU Hang Error (IOAF code 3)
So, obviously I am doing something wrong here. My first guess is that I don't update the vertex data correctly, but can't figure out what is wrong. My second guess is that there could be some race condition while extending the stroke
and the render()
function call, which could happen simultaneously.
Any help is appreciated!
Edit: Well, that was stupid. The vertex count was wrong, it should be:
renderEncoder?.drawPrimitives(type: .lineStrip, vertexStart: 0, vertexCount: stroke.count / 3)
Because each vertex consists of 3 Floats.