Présentation de l’api Metal sous mac OS

Metal est une API graphique de bas niveau qui est écris en C++ et qui est fonctionnel sur les systèmes d'exploitation d’apple (macOS, Apple Tv, iOS).

Cette API a été introduite dans les machines d’apple depuis iOS 8, et est surtout connu pour rendre les jeux beaucoup plus rapide à exécuter. Un exemple avec le jeu War Thunder , les développeurs du jeu on décidé de passer le jeu sous Metal en laissant de côté l’OpenGL car d’aprés eu “OpenGL restreignait la vitesse à laquelle les batailles s’affichaient et pouvait réduire le taux de rafraichissement dans les scènes impliquant un grand nombre de shaders, par exemple quand un joueur encaissait un tir d’artillerie”. Grâce à Metal, les scènes dynamiques  de ce genre s’affichent bien plus rapidement. Metal permet aux jeux qui en tirent profit des performances bien plus satisfaisantes qu’auparavant n’est pas une surprise : la dernière extension Legion de World of Warcraft a profité d’un sérieux coup de turbo en faisant appel à l’API Grâce à Metal, World of Warcraft est 61% plus rapide.

 

C’est pour dire à quel point cette API a l’air puissante, mais qu’en est t’il vraiment ?

 

Dans cette présentation je vais vous montrer comment installer Metal et également des explications sur son fonctionnement et des exemples de codes pour illustré tout ça.

 

Prêt ? C’est parti ! Accrochez ça va envoyer du Metal 😉

 

Installation :

 

Pour commencer et si ce n’est pas déjà fait vous devez vous procurer l’IDE de apple qu’est “xCode” , je vous laisse le lien qui permet de le télécharger : https://developer.apple.com/xcode/

 

Le “GameTemplate iOS” de xCode possède déjà des options avec Metal. Mais pour bien comprendre comment mettre en place une application Metal nous n’allons pas utilisez les options proposé par xCode.  

 

Dans un premier temps nous allons ouvrir notre xCode et nous allons créer un nouveau projet , cliquez sur “Create a new Xcode projet”

 

Et créer un projet iOS/Application/Single View App.

Entrer “HelloMetal” pour le Product Name et selectionnez le langage Swift.

Appuyer sur Next et sélectionnez un dossier dans lequel mettre votre projet.

 

Le plus facile est déjà est à présent derrière nous. Il y a 7 étapes pour mettre en place Metal dans notre projet :

 

  1. Créer une MTLDevice
  2. Créer une CAMetalLayer
  3. Créer une Vertex Buffer
  4. Créer une Vertex Shader
  5. Créer une Fragment Shader
  6. Créer une Render Pipeline
  7. Créer une Command Queue

Nous allons attaquer ces étapes une par une.

 

  1. Créer une MTLDevice :

La première chose à faire est de créer une référence avec le MTLDevice.

Pour cela ouvrez votre “ViewController.swift” et ajoutez l’import suivant : “import Metal

Grace a cette import vous pouvez maintenant utilisez la classe Metal comme une MTLDevice a l’intérieur de ce fichier.

Ensuite ajoutez la propriété “var device: MTLDevice!” a l’intérieur de votre classe ViewController.

Et on initialise la propriété dans le viewDidLoad() a la fin de la méthode. device = MTLCreateSystemDefaultDevice().

Vous devriez obtenir ceci et on a terminer avec la première étape.

import UIKit

import Metal

class ViewController: UIViewController {

var device: MTLDevice!

override func viewDidLoad() {

super.viewDidLoad()

device = MTLCreateSystemDefaultDevice()

 

}

override func didReceiveMemoryWarning() {

super.didReceiveMemoryWarning()

}

 

2) Créer une CAMetalLayer

Si nous voulons “dessinez” quelque chose sur l’écran avec l’api Metal, nous avons besoin d’utilisez une classe spécial qui hérite de CAMetalLayer.

Pour commencer on va ajoutez à notre controlleur l’attribut  var metalLayer: CAMetalLayer!

Ensuite ajoutez ces lignes a la fin de votre methode ViewDidLoad():

metalLayer = CAMetalLayer()
metalLayer.device = device
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.framebufferOnly = true
metalLayer.frame = view.layer.frame
view.layer.addSublayer(metalLayer)

Pour le moment vous devriez obtenir ceci.

import UIKit

import Metal

class ViewController: UIViewController {

var device: MTLDevice!

var metalLayer: CAMetalLayer!

override func viewDidLoad() {

super.viewDidLoad()

device = MTLCreateSystemDefaultDevice()

metalLayer = CAMetalLayer()
metalLayer.device = device
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.framebufferOnly = true
metalLayer.frame = view.layer.frame
view.layer.addSublayer(metalLayer)

}

override func didReceiveMemoryWarning() {

super.didReceiveMemoryWarning()

}

 

3. Créer une Vertex Buffer

 

Dans Metal tout est triangle, par exemple si vous dessinez un triangle , l’API metal pour vous faire un rendu en 3D va décomposé votre triangle en une série de triangle. Nous allons voir déjà comment créer cela.

Pour commencer on va ajoutez une constante a notre classe que voici :

let vertexData:[Float] = [
 0.0, 1.0, 0.0,
 -1.0, -1.0, 0.0,
 1.0, -1.0, 0.0]

Nous avons besoin d’obtenir la taille des données du vertex en bytes. Pour obtenir cette information nous avons besoin de multiplier la taille du premier élément par tous les éléments que contient notre tableau.

 

Ajoutez une autre propriété à votre classe :

var vertexBuffer: MTLBuffer!

Puis ajoutez ces lignes de code a la fin de votre methode “ViewDidLoad()” :

let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0])
vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])

Pour le moment nous avons ceci.

import UIKit

import Metal

class ViewController: UIViewController {

var device: MTLDevice!

var metalLayer: CAMetalLayer!

let vertexData:[Float] = [
0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0]

var vertexBuffer: MTLBuffer!

override func viewDidLoad() {

super.viewDidLoad()

device = MTLCreateSystemDefaultDevice()

metalLayer = CAMetalLayer()
metalLayer.device = device
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.framebufferOnly = true
metalLayer.frame = view.layer.frame
view.layer.addSublayer(metalLayer)

let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0])
vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])

}

override func didReceiveMemoryWarning() {

super.didReceiveMemoryWarning()

}

 

4) Créer une Vertex Shader

Une vertex Shader est simplement un petit programme qui va être exécuté dans notre CPU et il est écrit en C++, il permet en autre d’obtenir la position des vertex créés , c’est ce que nous avons fait dans l’étape précédente !

Pour comprendre plus facilement ce que c’est , le mieux est encore d’en créer un !

Créons un nouveau fichier , allez dans File/new/File et choissisez iOS/Source/Metal File et cliquer sur Next. Nommer le Shaders.metal et cliquez sur create.

 

Ajoutez ce code a la fin de votre Shaders.metal :

vertex float4 basic_vertex(                           
 const device packed_float3* vertex_array [[ buffer(0) ]],
 unsigned int vid [[ vertex_id ]]) {                 
 return float4(vertex_array[vid], 1.0);              
}

Vous devriez obtenir ceci.

#include <metal_stdlib>

using namespace metal;

vertex float4 basic_vertex(const device packed_float3* vertex_array [[ buffer(0) ]],

unsigned int vid [[ vertex_id ]])

{

return float4(vertex_array[vid], 1.0);

}

 

Vous devriez obtenir ceci.

 

  1. Chaques vertex shaders doivent commencer par le mot clés vertex. la fonction doit retourner la position final du vertex.
  1. Le premier parametre est un pointeur sur un tableau de   packed_float3  (un groupe de vector de 3 floats).
  2. Le vertex shader doit égallement prendre un paramètre avec l’attribut vertex_id.
  3. Vous obtenez la position haute a l’intérieur du tableau de vertex basé sur l’id du vertex et vous retournez le resultat.

 

5) Créer une Fragment Shader

Après avoir créé notre vertex shader , un autre shader est appelé pour chaque éléments , par exemple les pixels sur un écran. C’est le role du Fragment Shader.

Ajoutez dans votre Shaders.Metal  cette ligne de code :

fragment half4 basic_fragment() {
 return half4(1.0);              

}

  1. Tout les fragment shaders doivent commencer par le mot clés fragment. La fonction doit retourner la couleur final du fragment.  
  2. La nous retourner (1,1,1,1) pour la couleur Blanc.

 

Pour le moment vous devriez obtenir ceci.

#include <metal_stdlib>

using namespace metal;

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);

}

 

 

 

6) Créer une Render Pipeline

Maintenant que nous avons créer un vertex et un fragment shader nous avons besoin de les faires “travailler ensemble” à l’intérieur d’un objet que l’on appel le Render Pipeline.

Pour commencer nous allons ajoutez à notre ViewController.swift la propriété suivante :


var pipelineState: MTLRenderPipelineState!

Maintenant ajoutez ces lignes de code a la fin de ViewDidLoad() :

let defaultLibrary = device.newDefaultLibrary()!
let fragmentProgram = defaultLibrary.makeFunction(name: "basic_fragment")
let vertexProgram = defaultLibrary.makeFunction(name: "basic_vertex")
   
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = vertexProgram
pipelineStateDescriptor.fragmentFunction = fragmentProgram
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
   
pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)

  1. Vous aves accés a tout les shaders précompilé inclus dans votre projet avec l’objet MTLLibrary, vous l’avez en appelant device.newDefaultLibrary()!
  2. Vous muté la configuration de votre pipeline, il contient alors les shaders que vous voulez utiliser et le format des pixels de couleur
  3. Finalement vous compiler la configuration du pipeline a l’intérieur d’une instance de pipeline pour l’utiliser

 

Pour le moment vous devirez obtenir ceci.

import UIKit

import Metal

class ViewController: UIViewController {

var device: MTLDevice!

var metalLayer: CAMetalLayer!

let vertexData:[Float] = [

0.0, 1.0, 0.0,

-1.0, -1.0, 0.0,

1.0, -1.0, 0.0]

var vertexBuffer: MTLBuffer!

var pipelineState: MTLRenderPipelineState!

 

 

override func viewDidLoad() {

super.viewDidLoad()

 

device = MTLCreateSystemDefaultDevice()

metalLayer = CAMetalLayer()

metalLayer.device = device

metalLayer.pixelFormat = .bgra8Unorm

metalLayer.framebufferOnly = true

metalLayer.frame = view.layer.frame

view.layer.addSublayer(metalLayer)

 

let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0])

vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])

 

let defaultLibrary = device.makeDefaultLibrary()!

let fragmentProgram = defaultLibrary.makeFunction(name: "basic_fragment")

let vertexProgram = defaultLibrary.makeFunction(name: "basic_vertex")

 

let pipelineStateDescriptor = MTLRenderPipelineDescriptor()

pipelineStateDescriptor.vertexFunction = vertexProgram

pipelineStateDescriptor.fragmentFunction = fragmentProgram

pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm

 

pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)

}

override func didReceiveMemoryWarning() {

super.didReceiveMemoryWarning()

 

}

 

 

 

7) Créer une Command Queue

La dernière étape de la mise en place de Metal consite a créer une MTLCommandQueue.

 

Nous avons besoin de faire ceci pour dire à notre CPU d'exécuter les tâches une par une.

Pour créer une command Queue nous avons besoin d'ajouter cette propriété , toujours dans notre controlleur :

var commandQueue: MTLCommandQueue!

et d’ajoutez cette ligne a la fin de notre ViewDidLoad()

commandQueue = device.makeCommandQueue()

 

Pour terminer cette installation vous devriez obtenir ceci.

import UIKit

import Metal

class ViewController: UIViewController {

var device: MTLDevice!

var metalLayer: CAMetalLayer!

let vertexData:[Float] = [

0.0, 1.0, 0.0,

-1.0, -1.0, 0.0,

1.0, -1.0, 0.0]

var vertexBuffer: MTLBuffer!

var pipelineState: MTLRenderPipelineState!

var commandQueue: MTLCommandQueue!

 

override func viewDidLoad() {

super.viewDidLoad()

 

device = MTLCreateSystemDefaultDevice()

metalLayer = CAMetalLayer()

metalLayer.device = device

metalLayer.pixelFormat = .bgra8Unorm

metalLayer.framebufferOnly = true

metalLayer.frame = view.layer.frame

view.layer.addSublayer(metalLayer)

 

let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0])

vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])

 

let defaultLibrary = device.makeDefaultLibrary()!

let fragmentProgram = defaultLibrary.makeFunction(name: "basic_fragment")

let vertexProgram = defaultLibrary.makeFunction(name: "basic_vertex")

 

let pipelineStateDescriptor = MTLRenderPipelineDescriptor()

pipelineStateDescriptor.vertexFunction = vertexProgram

pipelineStateDescriptor.fragmentFunction = fragmentProgram

pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm

 

pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)

commandQueue = device.makeCommandQueue()

}

override func didReceiveMemoryWarning() {

super.didReceiveMemoryWarning()

 

}

 

 

 

Exemple de code :

Super on vient de terminer la configuration de notre installation.

Je vous l’accorde cette configuration est plutôt longue, mais nous n’avons pas fait tout cela en vain. Car les exemples de code avaient déjà commencé dans la parti de l’installation. Quand je vous disais que dans Metal tout était triangle. Et bien tout ce nous avons fait jusqu'à présent nous as permis de créer un triangle ! Donc maintenant cette étape va nous permettre de le faire affichez ! Et tout cela en 5 étapes :

 

  1. Créer un lien d’affichage
  2. Créer un Render Pass Descriptor
  3. Créer un Command Buffer
  4. Créer a Render Command Encoder
  5. Envoyer votre Command Buffer

 

  1. Créer un lien d’affichage

Nous avons besoin d’une fonction qui sera appelé à chaque fois que l’affichage vas s'actualiser, tout simplement pour refaire afficher notre dessins à l’écran.

Pour commencer initialiser cette propriété dans le controlleur :

var timer: CADisplayLink!

Et muter la a la fin de notre fonction ViewDidLoad() :

timer = CADisplayLink(target: self, selector: #selector(ViewController.gameloop))
timer.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)

Cela permet à chaque fois que nous l’affichage va se rafraîchir d’appeler la méthode gameloop() dans notre controlleur

Maintenant on va créer la méthode gameloop() :

func render() {
 // TODO
}

func gameloop() {
 autoreleasepool {
   self.render()
 }
}

 

Votre début et fin de fonction devrai comporter à présent ceci.

var device: MTLDevice!

var metalLayer: CAMetalLayer!

let vertexData:[Float] = [

0.0, 1.0, 0.0,

-1.0, -1.0, 0.0,

1.0, -1.0, 0.0]

var vertexBuffer: MTLBuffer!

var pipelineState: MTLRenderPipelineState!

var commandQueue: MTLCommandQueue!

var timer: CADisplayLink!

timer = CADisplayLink(target: self, selector: #selector(ViewController.gameloop))

timer.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)

}

override func didReceiveMemoryWarning() {

super.didReceiveMemoryWarning()

 

}

 

func render() {

}

 

@objc func gameloop() {

autoreleasepool {

self.render()

}

}

 

 

2) Créer un Render Pass Descriptor

La prochaine étape est de créer un MTLRenderPassDescriptor , c’est un objet qui va configurer chaque texture qu’il doit être rendu à la vue comme la couleur et d’autre configuration nécessaire au rendu.

Ajoutez simplement ces lignes dans votre méthode render() :

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: 5.0/255.0, alpha: 1.0)

En premier nous appelons la méthode nextDrawable() dans le Metal layer que nous avons créé plus tôt, cette methode va nous retourner les textures que nous avons besoin pour faire afficher quelque chose a l’écran.

Ensuite on configure le render pass descriptor qui va utiliser ses textures.

Votre fonction render() devrait ressembler à ceci maintenant.

func render() {

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:

5.0/255.0, alpha: 1.0)

}

 

3) Créer un Command Buffer

La prochaine étape est de créer un Command Buffer. Rien ne se passera tant que nous appelons pas le command Buffer, ce qui nous donne un contrôle pour exécuter ou non des textures à rendre ou autres.

Créer un command buffer est facile il vous suffis d'ajouter ces lignes de code a la fin de votre render() :

let commandBuffer = commandQueue.makeCommandBuffer()

Un command buffer contient un ou plus commande de rendu. Nous allons en créer une a la prochaine étape.

Et votre render() devrait ressembler à ceci maintenant.

func render() {

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:

5.0/255.0, alpha: 1.0)

let commandBuffer = commandQueue.makeCommandBuffer()

}

 

 

4) Créer a Render Command Encoder

Pour créer un render command, nous avons besoin d’un objet appeler un render command encoder. Ajoutez ces lignes a la fin de votre render :

let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, at: 0)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
renderEncoder.endEncoding()

 

Nous avons créé un command encoder est spécifier le pipeline et le vertex que nous avons créé plus tôt.

Notre méthode render() devrait maintenant ressembler à ceci.

func render() {

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:

5.0/255.0, alpha: 1.0)

let commandBuffer = commandQueue.makeCommandBuffer()

let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor:

renderPassDescriptor)

renderEncoder.setRenderPipelineState(pipelineState)

renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, at: 0)

renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount:

renderEncoder.endEncoding()

}

 

 

5) Envoyer votre Command Buffer

La dernière étape consiste à envoyer le command buffer. Ajoutez ces lignes a la fin de notre méthode render() :

commandBuffer.present(drawable)
commandBuffer.commit()

The first line is needed to make sure the new texture is presented as soon as the drawing completes. Then you commit the transaction to send the task to the GPU.

Phew! That was a ton of code, but at long last you are done! Build and run the app and bask in your triangular glory:

La première ligne est la pour vérifier que les nouvelles textures son présente une fois que le dessin a était créer. Alors nous envoyons une transaction pour faire appel au CPU.

Et c’est terminé , cela fait beaucoup de code , mais c’est bel est bien terminé ! Compilez et exécutez votre application !

Si vous avez une erreur de la part votre IDE au moment du build c’est tout simplement car les simulations d'exécutions ne fonctionne pas avec Metal , vous avez besoin de connecter votre appareil à votre machine et à le connecter en tant qu’appareil de simulation pour pouvoir exécuter votre code , avec un iphone 5 ou supérieur, ou un ipad de seconde génération ou plus.

Vous devriez obtenir ceci :

 

Conclusion :

La présentation de Metal sous Mac OS est terminé , j'espère que cela vous plait et que cela vous donne envie de continuer à apprendre avec cette API. Sachez que pour moi cela m’a donné envie d’aller plus loins le fait d'écrire cette présentation, c’est une façon totalement différente de créer des choses sur mac et c’est plutôt intéressant dans l’ensemble.

Les possibilités ont l’air d'être très poussé et une fois maîtrisé cette API peut vous amener à faire de grande chose !

 

Lien google drive qui contient tout le code source présenté dans cette présentation : https://drive.google.com/open?id=1bEgxW2IBsSBFMbzNJ3dCW5e8XEUGw7iw

Jerome Paoli.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *