How to efficiently draw primitives with iOS Metal?
Asked Answered
E

1

8

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.

Extracellular answered 17/11, 2017 at 20:43 Comment(0)
R
0

The error:

Execution of the command buffer was aborted due to an error during execution. Caused GPU Hang Error (IOAF code 3)

Is caused by accessing out-of-bound data.

Which explains why correcting the vertex count fixed the issue.

It is a good idea to turn on Shader Validation when this error occurs, as it often will point to the cause of the error.

If that doesn't point to the issue, take a good look at anything that could be using invalid memory, such as using a buffer past the actual length, using an incorrect stride or length for datatypes, or trying to draw more vertices than exist.

Resonate answered 1/8, 2022 at 6:56 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.