Search code examples
ipadswift3metalmetalkit

Setting up Metal in Swift 3 on an iOS device


I've been trying to convert Apple's MetalBasicTessellation project to work in swift 3 on an iPad Air 3, but thus far have been unsuccessful. Frustratingly, the project comes with an iOS implementation (written in objectiveC, and a swift playground), but no swift 3 implementation.

I have gotten the code to compile, but fails to run on my iPad with the following error:

2017-05-14 14:25:54.268400-0700 MetalBasicTessellation[2436:570250] -[MTLRenderPipelineDescriptorInternal validateWithDevice:], line 1728: error 'tessellation is only supported on MTLFeatureSet_iOS_GPUFamily3_v1 and later'

I am pretty sure that the iPad Air 2 is compliant, but I have the feeling the error is due to an improperly configured MetalKitView. I have reverse-engineered what I could from the project's objective-c and playground files, but I have gone as far as I am able to understand with my current expertise.

//
//  ViewController.swift
//  MetalBasicTessellation
//
//  Created by vladimir sierra on 5/10/17.
//  
//

import UIKit
import Metal
import MetalKit

class ViewController: UIViewController {

  @IBOutlet weak var mtkView: MTKView!

  // Seven steps required to set up metal for rendering:

  // 1. Create a MTLDevice
  // 2. Create a CAMetalLayer
  // 3. Create a Vertex Buffer

  // 4. Create a Vertex Shader
  // 5. Create a Fragment Shader

  // 6. Create a Render Pipeline
  // 7. Create a Command Queue

  var device: MTLDevice! // to be initialized in viewDidLoad
  //var metalLayer: CAMetalLayer! // to be initialized in viewDidLoad
  var vertexBuffer: MTLBuffer! // to be initialized in viewDidLoad
  var library: MTLLibrary!

  // once we create a vertex and fragment shader, we combine them in an object called render pipeline. In Metal the shaders are precompiled, and the render pipeline configuration is compiled after you first set it up. This makes everything extremely efficient

  var renderPipeline: MTLRenderPipelineState! // to be initialized in viewDidLoad
  var commandQueue: MTLCommandQueue! // to be initialized in viewDidLoad

  //var timer: CADisplayLink! // function to be called every time the device screen refreshes so we can redraw the screen



  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    /*
    if let window = view.window {
      let scale = window.screen.nativeScale // (2 for iPhone 5s, 6 and iPads;  3 for iPhone 6 Plus)
      let layerSize = view.bounds.size
      // apply the scale to increase the drawable texture size.
      view.contentScaleFactor = scale
      //metalLayer.frame = CGRect(x: 0, y: 0, width: layerSize.width, height: layerSize.height)
      //metalLayer.drawableSize = CGSize(width: layerSize.width * scale, height: layerSize.height * scale)
    } */
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    device = MTLCreateSystemDefaultDevice() // returns a reference to the default MTLDevice

    //device.supportsFeatureSet(MTLFeatureSet_iOS_GPUFamily3_v2)



    // set up layer to display metal content
    //metalLayer = CAMetalLayer()          // initialize metalLayer
    //metalLayer.device = device           // device the layer should use
    //metalLayer.pixelFormat = .bgra8Unorm // normalized 8 bit rgba
    //metalLayer.framebufferOnly = true    // set to true for performance issues
    //view.layer.addSublayer(metalLayer)   // add sublayer to main view's layer

    // precompile custom metal functions

    let defaultLibrary = device.newDefaultLibrary()! // MTLLibrary object with precompiled shaders


    let fragmentProgram = defaultLibrary.makeFunction(name: "tessellation_fragment")
    let vertexProgram = defaultLibrary.makeFunction(name: "tessellation_vertex_triangle")

    // Setup Compute Pipeline
    let kernelFunction = defaultLibrary.makeFunction(name: "tessellation_kernel_triangle")
    var computePipeline: MTLComputePipelineState?
    do {
      computePipeline = try device.makeComputePipelineState(function: kernelFunction!)
    } catch let error as NSError {
      print("compute pipeline error: " + error.description)
    }

    // Setup Vertex Descriptor
    let vertexDescriptor = MTLVertexDescriptor()
    vertexDescriptor.attributes[0].format = .float4
    vertexDescriptor.attributes[0].offset = 0
    vertexDescriptor.attributes[0].bufferIndex = 0;
    vertexDescriptor.layouts[0].stepFunction = .perPatchControlPoint
    vertexDescriptor.layouts[0].stepRate = 1
    vertexDescriptor.layouts[0].stride = 4*MemoryLayout<Float>.size

    // Setup Render Pipeline
    let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
    renderPipelineDescriptor.vertexDescriptor = vertexDescriptor
    //renderPipelineDescriptor.fragmentFunction = defaultLibrary.makeFunction(name: "tessellation_fragment")
    renderPipelineDescriptor.fragmentFunction = fragmentProgram
    //renderPipelineDescriptor.vertexFunction = defaultLibrary.makeFunction(name: "tessellation_vertex_triangle")
    renderPipelineDescriptor.vertexFunction = vertexProgram

    //renderPipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm // normalized 8 bit rgba
    renderPipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat

    renderPipelineDescriptor.isTessellationFactorScaleEnabled = false
    renderPipelineDescriptor.tessellationFactorFormat = .half
    renderPipelineDescriptor.tessellationControlPointIndexType = .none
    renderPipelineDescriptor.tessellationFactorStepFunction = .constant
    renderPipelineDescriptor.tessellationOutputWindingOrder = .clockwise
    renderPipelineDescriptor.tessellationPartitionMode = .fractionalEven
    renderPipelineDescriptor.maxTessellationFactor = 64;

    // Compile renderPipeline
    do {
      renderPipeline = try device.makeRenderPipelineState(descriptor: renderPipelineDescriptor)
    } catch let error as NSError {
      print("render pipeline error: " + error.description)
    }

    // Setup Buffers
    let tessellationFactorsBuffer = device.makeBuffer(length: 256, options: MTLResourceOptions.storageModePrivate)
    let controlPointPositions: [Float] = [
      -0.8, -0.8, 0.0, 1.0,   // lower-left
      0.0,  0.8, 0.0, 1.0,   // upper-middle
      0.8, -0.8, 0.0, 1.0,   // lower-right
    ]
    let controlPointsBuffer = device.makeBuffer(bytes: controlPointPositions, length:256 , options: [])

    // Tessellation Pass
    let commandBuffer = commandQueue.makeCommandBuffer()

    let computeCommandEncoder = commandBuffer.makeComputeCommandEncoder()
    computeCommandEncoder.setComputePipelineState(computePipeline!)

    let edgeFactor: [Float] = [16.0]
    let insideFactor: [Float] = [8.0]
    computeCommandEncoder.setBytes(edgeFactor, length: MemoryLayout<Float>.size, at: 0)
    computeCommandEncoder.setBytes(insideFactor, length: MemoryLayout<Float>.size, at: 1)
    computeCommandEncoder.setBuffer(tessellationFactorsBuffer, offset: 0, at: 2)
    computeCommandEncoder.dispatchThreadgroups(MTLSizeMake(1, 1, 1), threadsPerThreadgroup: MTLSizeMake(1, 1, 1))
    computeCommandEncoder.endEncoding()

    let renderPassDescriptor = mtkView.currentRenderPassDescriptor
    let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor!)
    renderCommandEncoder.setRenderPipelineState(renderPipeline!)
    renderCommandEncoder.setVertexBuffer(controlPointsBuffer, offset: 0, at: 0)
    renderCommandEncoder.setTriangleFillMode(.lines)
    renderCommandEncoder.setTessellationFactorBuffer(tessellationFactorsBuffer, offset: 0, instanceStride: 0)
    renderCommandEncoder.drawPatches(numberOfPatchControlPoints: 3, patchStart: 0, patchCount: 1, patchIndexBuffer: nil, patchIndexBufferOffset: 0, instanceCount: 1, baseInstance: 0)
    renderCommandEncoder.endEncoding()

    commandBuffer.present(mtkView.currentDrawable!)
    commandBuffer.commit()
    commandBuffer.waitUntilCompleted()
    /*
    // finally create an ordered list of commands forthe GPU to execute
    commandQueue = device.makeCommandQueue()

    timer = CADisplayLink(target: self, selector: #selector(ViewController.gameloop)) // call gameloop every time the screen refreshes
    timer.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)

    */



  }

  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
  }

  /*
  func render() {
    guard let drawable = metalLayer?.nextDrawable() else { return } // returns the texture to draw into in order for something to appear on the screen
    //objectToDraw.render(commandQueue: commandQueue, renderPipeline: renderPipeline, drawable: drawable, clearColor: nil)
  }

  // this is the routine that gets run every time the screen refreshes
  func gameloop() {
    autoreleasepool {
      self.render()
    }
  } */


}

The entire git can be found here

Would some kind metal-guru-soul lend a hand? Documentation out there is pretty sparse.


Solution

  • The docs for MTLFeatureSet_iOS_GPUFamily3_v1 say:

    Introduced with the Apple A9 GPU and iOS 9.0.

    (Emphasis added.)

    Meanwhile, the iOS Device Compatibility Reference: Hardware GPU Information article says the iPad Air 2 has an A8 GPU.

    I don't believe your device is capable.

    In general, the configuration of the MTKView will not affect the feature set that's supported. That's inherent in the device (the combination of hardware and OS version). You can query whether a device supports a given feature set using the supportsFeatureSet(_:) method of MTLDevice. Since a device can be (and usually is) acquired independently of any other object such as an MTKView, the feature set support can't depend on such other objects.