Article technique

Compiler et valider les shaders Metal hors ligne dans un Cloud Mac CI

Compiler et valider les shaders Metal hors ligne dans un Cloud Mac CI

Une fonctionnalité graphique peut fonctionner correctement sur une machine de développement, puis provoquer une erreur de compilation Metal seulement à la fin de l’archivage en CI. Le problème vient généralement non pas d’un « manque de performances » de la machine, mais du fait que les shaders ne sont pas gérés comme des entrées de build indépendantes. Une approche plus fiable consiste à utiliser la chaîne d’outils Xcode active sur le Cloud Mac pour générer hors ligne les versions appareil et simulateur de metallib, avant de lancer le build complet du projet. Les erreurs de syntaxe, les SDK incorrects et les fonctions manquantes sont ainsi détectés plus tôt, avec des journaux d’échec plus courts.

Pourquoi isoler les shaders dans un contrôle dédié

Xcode traite automatiquement les fichiers .metal ajoutés à une cible, mais les erreurs se retrouvent souvent noyées dans de longs journaux comprenant la résolution des dépendances, la copie des ressources et les étapes de signature. Ce contrôle dédié ne remplace pas le build Xcode : il le précède afin de répondre à trois questions :

  1. Tous les shaders peuvent-ils être compilés avec la chaîne d’outils active ?
  2. Les bibliothèques peuvent-elles être générées séparément pour l’appareil et le simulateur ?
  3. Les fonctions réellement référencées par l’application peuvent-elles être chargées depuis la bibliothèque ?

Ne copiez pas directement un metallib généré sur une machine de développement dans un répertoire d’artefacts conservé à long terme. Après une modification de la chaîne d’outils, du SDK ou des options de compilation, l’ancien fichier peut rester présent sans correspondre au code source actuel.

Il est recommandé de ne versionner dans le dépôt que les sources .metal et les scripts de compilation, puis de placer les fichiers .air et .metallib dans un répertoire de build pouvant être nettoyé. Si l’équipe utilise un cache, sa clé doit au minimum inclure l’empreinte des sources, le chemin de Xcode, la version du SDK et la plateforme cible.

Figer la chaîne d’outils utilisée pour le build

Plusieurs versions de Xcode peuvent être installées sur un Cloud Mac. Le script ne doit pas dépendre de la dernière version sélectionnée dans l’interface graphique : il doit vérifier explicitement DEVELOPER_DIR. Le chemin dépend de l’installation réelle sur le nœud et ne doit pas être réutilisé entre plusieurs tâches sans validation préalable.

set -euo pipefail

: "${DEVELOPER_DIR:?DEVELOPER_DIR is required}"

xcodebuild -version
xcrun --sdk iphoneos --show-sdk-version
xcrun --sdk iphonesimulator --show-sdk-version
xcrun --sdk iphoneos --find metal
xcrun --sdk iphoneos metal --version

mkdir -p build/metal/metadata
{
  echo "developer_dir=$DEVELOPER_DIR"
  xcodebuild -version
  echo "iphoneos=$(xcrun --sdk iphoneos --show-sdk-version)"
  echo "iphonesimulator=$(xcrun --sdk iphonesimulator --show-sdk-version)"
  xcrun --sdk iphoneos metal --version
} > build/metal/metadata/toolchain.txt

Le fichier toolchain.txt doit être archivé avec les journaux d’échec. Il permet de distinguer rapidement une régression du code source d’un changement de version de Xcode sur l’exécuteur. Ne consignez pas uniquement xcodebuild -version, car le SDK effectivement utilisé influe lui aussi sur le résultat.

Compiler séparément les artefacts pour appareil et simulateur

Commencez par compiler chaque fichier source en un fichier .air distinct, puis regroupez-les dans une bibliothèque. La compilation fichier par fichier permet de rattacher directement une erreur au fichier concerné, au lieu de n’obtenir qu’un échec générique lors de l’édition de liens.

set -euo pipefail

sources=(Shaders/*.metal)
if [ ! -e "${sources[0]}" ]; then
  echo "No Metal source files found" >&2
  exit 1
fi

for sdk in iphoneos iphonesimulator; do
  out="build/metal/$sdk"
  rm -rf "$out"
  mkdir -p "$out/air"

  for src in "${sources[@]}"; do
    name="$(basename "$src" .metal)"
    xcrun --sdk "$sdk" metal -c "$src" \
      -o "$out/air/$name.air"
  done

  xcrun --sdk "$sdk" metallib "$out"/air/*.air \
    -o "$out/AppShaders.metallib"

  test -s "$out/AppShaders.metallib"
  shasum -a 256 "$out/AppShaders.metallib" \
    > "$out/AppShaders.metallib.sha256"
done

Les deux ensembles d’artefacts doivent être conservés dans des répertoires distincts afin que la tâche exécutée en second n’écrase pas le premier. Si le code source utilise des macros de préprocesseur C/C++ pour différencier les plateformes, centralisez leurs paramètres dans le script et veillez à ce que la Build Phase Xcode utilise le même ensemble de définitions.

Élément contrôlé Cible appareil Cible simulateur
SDK iphoneos iphonesimulator
Fichiers intermédiaires Fichiers .air distincts Fichiers .air distincts
Bibliothèque produite Archivée séparément Archivée séparément
Validation finale Appareil réel ou archive de production Test de fumée sur simulateur

Valider le contrat des fonctions avec un test de chargement

Un fichier non vide prouve seulement que le compilateur a produit une sortie, pas que les fonctions recherchées par l’application existent toujours. Après le renommage d’une fonction kernel ou fragment, les chaînes correspondantes dans le code Swift peuvent ne pas avoir été mises à jour. La cible de test doit donc charger la bibliothèque et rechercher les points d’entrée essentiels.

import Metal
import XCTest

final class ShaderLibraryTests: XCTestCase {
    func testRequiredFunctionsExist() throws {
        let device = try XCTUnwrap(MTLCreateSystemDefaultDevice())
        let url = try XCTUnwrap(
            Bundle(for: Self.self).url(
                forResource: "AppShaders",
                withExtension: "metallib"
            )
        )
        let library = try device.makeLibrary(URL: url)

        for name in ["imageVertex", "imageFragment", "toneMapKernel"] {
            XCTAssertNotNil(
                library.makeFunction(name: name),
                "Missing Metal function: \(name)"
            )
        }
    }
}

La liste des fonctions doit provenir des noms réellement utilisés par l’application lors de la création des pipelines. Le bundle de test doit également vérifier qu’il contient l’artefact iphonesimulator, et non un fichier destiné à l’appareil qui subsisterait par hasard dans l’espace de travail. Le test de fumée minimal consiste à créer un MTLDevice, à charger la bibliothèque et à rechercher les fonctions. Si des contraintes liées aux formats de texture ou aux groupes de threads entrent en jeu, ajoutez aussi un test de création de pipeline.

Quels éléments conserver en cas d’échec

Pour diagnostiquer un échec Metal en CI, un petit ensemble d’informations comparables est plus utile que l’intégralité du répertoire de travail :

  • DEVELOPER_DIR, ainsi que les versions de Xcode et des deux SDK ;
  • les commandes de compilation réellement exécutées et leur sortie d’erreur standard ;
  • l’empreinte du code source de chaque fichier .metal ;
  • la taille et le SHA-256 des deux fichiers metallib ;
  • le nom de la fonction dont le chargement a échoué, la cible de test et la destination d’exécution ;
  • l’identifiant du commit utilisé pour cette tâche.

L’empreinte des sources peut être générée avec find Shaders -name '*.metal' -print0 | sort -z | xargs -0 shasum -a 256. Une modification du hachage des artefacts ne doit pas, à elle seule, provoquer un échec, car une mise à niveau de la chaîne d’outils peut également modifier les fichiers binaires. La bonne stratégie consiste à comparer d’abord les empreintes de la chaîne d’outils, puis à utiliser comme contrôles les résultats de la recompilation, du chargement et de la création des pipelines.

Lors de l’exécution de ce type de tâche sur les nœuds physiques dédiés de MacMLab, un même espace de travail peut encore être contaminé par des jobs successifs. Nettoyez le répertoire de sortie cible avant chaque build, puis n’archivez à la fin de la tâche que les métadonnées, les journaux et les bibliothèques finales. Vous éviterez ainsi d’intégrer d’anciens fichiers .air dans une nouvelle bibliothèque et disposerez d’un point de départ reproductible lors du prochain échec.

Questions fréquentes

Pourquoi compiler les shaders séparément alors que Xcode le fait déjà ?

Cette étape détecte plus tôt les erreurs de syntaxe, de SDK et de cible, avec des journaux plus courts. La compilation normale de Xcode reste active.

Un même fichier metallib convient-il à l’appareil et au simulateur ?

Il ne faut pas le supposer. Produisez deux bibliothèques avec les SDK iphoneos et iphonesimulator, puis testez chacune sur sa cible.

Un changement de hachage metallib indique-t-il forcément une erreur ?

Non. Comparez d’abord DEVELOPER_DIR, le SDK et la version du compilateur, puis utilisez le test de chargement pour décider si l’artefact est valide.

Nœud physique dédié

Déployez vos tâches de développement et de build sur un Mac dédié dans le cloud

Choisissez le modèle, la région du nœud et la durée de location selon vos besoins. Chaque instance correspond à une machine physique dédiée, dans un environnement non virtualisé.

Choisir une offre Mac dans le cloud