Chapitre -1 · incrémenter les bases

Tu ne sais pas coder ? Ce n'est pas grave !

Comment fonctionne un programme

Tu utiliseras plusieurs langages de programmation différents au cours de ton parcours, dans ce chapitre nous abordons le Shell, langage de programmation & interpréteur de commandes permettant d’intéragir avec le système d’exploitation via des commandes écritent par l'utilisateur souvent dans Terminal.app

Vérifier ton environnement ZSH

Dans ton terminal, les commandes sont safes, essaie-les !

# Version de Clang (compilateur C & C++)
clang++ --version
# → Apple clang version 17.0.0 (doit être ≥ 15)

# Version par défaut du C & C++ utilisé par clang
clang++ -dM -E -x c++ /dev/null | grep __cplusplus
# → #define __cplusplus 201402L

# SDK macOS actif
xcrun --sdk macosx --show-sdk-path
# → /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.2.sdk

# Compilateur Metal disponible
xcrun -sdk macosx metal --version
# → Apple metal version 32023.921

# Dernière version du langage C++ & supportée par ton clang
clang++ -std=c++26 -x c++ /dev/null -dM -E | grep __cplusplus
# → #define __cplusplus 202400L

# Voir les frameworks Metal disponibles
xcrun --find metal
xcrun --find metallib

# Ton GPU — nom et famille
system_profiler SPDisplaysDataType | grep -E "Chipset|Metal"

# Infos sur la puce et la ram
sysctl -n machdep.cpu.brand_string
sysctl -n hw.memsize | awk '{print $1/1073741824 " GB RAM"}'
Les manuels en Shell, C & C++ principalement, directement dans ton terminal.
# Shell — terminal
## Le premier manuel à consulter est probablement le manuel du manuel :
man man

Dans la description du manuel du manuel on nous informe que c'est un utilitaire qui permet de trouver et d'afficher les pages de DOCUMENTATION. Les sections du manuel sont :

  1. General Commands Manual ← fonctions shell
  2. System Calls Manual
  3. Library Functions Manual ← fonctions C & C++
  4. Kernel Interfaces Manual
  5. File Formats Manual
  6. Games Manual
  7. Miscellaneous Information Manual
  8. System Manager's Manual
  9. Kernel Developer's Manual

Imaginons que nous avons besoin d'informations à propos de la fonction C++ 'fabsf'

# Shell
man 3 fabsf
FABS(3)                     Library Functions Manual                        FABS(3)

NAME
     fabs – floating-point absolute value function

SYNOPSIS
     #include <math.h>    ← directive de préprocesseur à inclure dans ton fichier qui utilise fabsf()

     double
     fabs(double x);

     long double
     fabsl(long double x);

     float
     fabsf(float x);    ← prototypes & variantes

DESCRIPTION
     The fabs() functions compute the absolute value of a floating-point number x.
     Les fonctions fabs() calculent la valeur absolue d'un nombre à virgule flottante x.

SPECIAL VALUES
     fabs(±0) returns 0.
     
     fabs(±infinity) returns +infinity.

VECTOR OPERATIONS
     If you need to apply the fabs() function to SIMD vectors or arrays, using the following functions
     provided by the Accelerate.framework may give significantly better performance:

     #include <Accelerate/Accelerate.h>

     vFloat vfabsf(vFloat x);
     void vvfabsf(float *y, const float *x, const int *n);
     void vvfabs(double *y, const double *x, const int *n);

SEE ALSO    ← d'autres functions dont existe le manuel : man 3 floor
     abs(3), ceil(3), floor(3), rint(3), math(3)

STANDARDS   ← norme
     The fabs() functions conform to ISO/IEC 9899:2011.

macOS 26.5                      December 11, 2006                       macOS 26.5

Ci-dessous se trouve un programme écrit en langage C. Après cette page, nous afficherons tout et n'importe quoi à l'écran dans notre propre programme…

  1. L'extension du fichier C est présente → .c
  2. On a décidé de nommer le fichier "ft_putchar" ainsi que la fonction
  3. L'include est entre chevrons (< >)
  4. Inclure l'en-tête 'unistd.h' permet d’utiliser write()
  5. On ne renvoit aucune valeur (void)
  6. On appelle une autre fonction, tout comme main() appelle notre fonction
  7. On remarque l'ordre de déclaration des deux fonctions
  8. Fonction main = première fonction = point d'entrée
  9. "&" fait référence à l'adresse mémoire de la variable charactère nommé ici 'c'
  10. Entre les braces, tu peux tout faire
  11. main renvoie 0 si tout s'est bien passé

Concrêtement, main() s'execute, appelle 3 fois notre fonctionnalité créé avec le même paramètre 'Y' = 89 dans la table ASCII, là où '\n' = 10, sont tous les deux un caractère de 1 byte envoyé à l'écran un par un (en millisecondes), qui appelle elle-même la fonctionnalité write décrite en dessous et s'arrête sans erreur.

// ft_putchar.c
#include <unistd.h>

void ft_putchar(char c)
{
    write(1, &c, 1);
}

int main(void)
{
    ft_putchar('Y');
    ft_putchar("\n");
    ft_putchar(89);

    return 0;

    // Compilation : clang ft_putchar.c
    // Utilisation : ./a.out
    // Résultat : Y
}                 Y
  1. ssize_t : nombre entier signé
  2. size_t : nombre entier non-signé
  3. char : 1 octet
  4. int : nombre entier de 4 octets allant de ≈ -2 milliards à +2 milliards

On appelle write() avec les même paramètres que le prototype, c'est alors qu'elle tente d'écrire une taille en octets "nbyte" (ici 1 = 8 bits) à partir du tampon pointé par le tampon de stockage vers le fichier associé au descripteur de fichier ouvert (file descriptor). Ici le pointeur n'a pas de types (ex : char, int, float, …) et est constant, il peut donc être tout.

// prototype de la fonction write dans unistd.h
ssize_t write(int fd, const void *buffer, size_t nbyte);
man 2 write      # Le manuel en anglais

Descripteur de fichier (fd)

0 Entrée standard stdin : flux associé au clavier. scanf(), std::cin,.. utilisent implicitement ce flux.
1 Sortie standard stdout : flux standard de sortie. printf(), write(), .. utilisent implicitement ce flux de données.
2 Erreur standard stderr : flux associé à la sortie standard d'erreur. Normalement redirigé sur la console de l'application également.

En MSL, en C++ et de nombreux autres langages tu retrouveras + ou - les même façons de faire :

Type(s) de donnée(s) que tu veux que la fonction retourne > son nom > son/ses paramètre(s) ou rien > ton code entre les braces > le retour ou non d'un ou de data types

Mais ce langage très niche est probablement le meilleur depuis longtemps et pour longtemps

C'est une chance inouïe que d'avoir de tel outils tant développé alors casse les codes

* Bash : un type spécifique de shell. Son nom complet est Bourne Again Shell. C’est l’un des shells les plus utilisés sur Linux

TTY = teletypewriter

Tous les Bash sont des shells, mais tous les shells ne sont pas Bash.

Quelques commandes communes.. (safe)

cd                                          # revient à la racine (Command Directory)
mkdir nom_du_dossier_que_tu_veux_creer      # Créer un nouveau dossier nommé
cd nom_du_dossier_que_tu_viens_de_creer     # Va dans ton dossier
cd …                                        # Reviens en arrière
touch nom_dun_fichier_que_tu_veux.creer.bo  # Créer un fichier nommé
vim nom_du_fichier_que_tu_viens_de.creer.bo # Éditeur vim
│
└──> dans vim ─> Insert du texte        i
            ├──> Taper une commande (:) esc
            ├──> Enregistre             :w
            ├──> Quitte                 :q
            ├──> Les deux               :wq
            ├──> Sans sauvegarder       :q!
            ├──> Supprime la ligne      dd

man vim                                     # Ouvre le manuel de vim
man touch                                   # Ouvre le manuel de la commande 'touch'
ls -la ./*                                  # Liste les fichiers dans le dossier actuel et les suivants
                                            # flag a ─> avec fichiers cachés tel que .env, .vimrc, …
open fichier_que_tu_veux_ouvrir_avec_xcode  # Ouvre l'éditeur externe Xcode
cat fichier_avec_contenu                    # Affiche l'entièreté du fichier
xattr -l *.png                              # Affiche les attribus des fichiers .png
find . -name ".DS_Store" -print -delete     # Trouve les fichiers et les supprimes récurssivement
clear                                       # Nettoie le terminal
pwd                                         # Affiche le chemin du dossier dans lequel tu es
awk
sed
grep                                        # Ultra puissant
cp src goal/dest                            # Copie 'source' dans 'destination' dans le dossier goal
mv src goal/dest                            # Déplace (Move) 'source' vers 'dest …
tty                                         # identifiant interne de ta session terminal
who                                         # identifiant interne de tes sessions terminal
whoami                                      # Qui es-tu

+ de commandes dans le chapitre sur git
git clone lien_HTTPS nom_souhaité           # Clone le repository depuis GitHub
git status
git add *                                   # Ajoute le contenu du dossier au prochain commit
git commit -m "message d'amélioration bzzz" # Commit avant de push, avec le message informatif
git push

# Note importante; les fichiers sont supprimés définitivement.
rm -r dossier_poubelle                      # Supprime tous les fichiers de manière récursive

chmod +x script.sh                          # Rendre le fichier script executable
chmod 777 script.sh                         # Donner toutes les permissions au fichier

pip3 install --upgrade pip

!!!RTFM is good!!!!
➤ Vim Cheat Sheet pour les nerds

Direction (directionnary), Lire (read), Écrire (write), Executable, attributs étendus (xattr)

On peut également executer un script Shell depuis un fichier :

#!/usr/bin/env zsh (ou bash)
cd && cd learnmsl.dev && python3 app.py
# Utilisation
./script.sh

# Ou avec argument (ne s'applique pas ici)
./script.sh John

Exemples en production :

#!/bin/zsh
SRC="/Users/laboitederemdal/MSLearn"
DEST_BASE="/Users/laboitederemdal/backupLEARNMSL"

# Trouve le prochain numéro de chapitre
i=1 # Très important en shell, de ne pas mettre d'espace quand il y a "="
while [ -d "$DEST_BASE/chapitre_$i" ];
do
i=$((i + 1))
done

DEST="$DEST_BASE/chapitre_$i"
mkdir -p "$DEST"

echo "Backup vers $DEST"

# Trouve et copie les fichiers en excluant Frameworks (je ne veux garder que nos fichiers)
find "$SRC" -type d -name "Frameworks" -prune -o \
    -type f \( -name "*.m" -o -name "*.mm" -o -name "*.h" \
        -o -name "*.hpp" -o -name "*.cpp" -o -name "*.metal" \)
            -print | while read file; do
    
    rel_path="${file#$SRC/}"
    dest_path="$DEST/$rel_path"

    # Recrée l'arborescence relative
    mkdir -p "$(dirname "$dest_path")"
    cp "$file" "$dest_path"
done # Fin de la commande 'find'

touch "$DEST/Makefile"

echo "Backup terminé & Makefile file créé."

# Autoriser le script à s'exécuter
chmod +x backup.sh
# Usage
./backup.sh

Ce que ton ordinateur manipule

Avant de parler GPU, avant même de parler C++, il faut comprendre ce que manipule réellement un ordinateur. Tout — absolument tout — est un nombre.

Le binaire — pourquoi 0 et 1

Un transistor a deux états : passant ou bloquant. Tension présente ou absente. Un bit. Pas de philosophie là-dedans — c'est de l'électronique.

8 bits = 1 octet (byte). Un octet peut représenter 2⁸ = 256 valeurs (0 à 255). Un entier 32-bit (int) = 4 octets = 2³² valeurs possibles.

→ Calculateur de types à découvrir dans 'Setup' ou dans ta calculatrice MacOS.

Hexadécimal — le raccourci des programmeurs

Écrire du binaire à la main est illisible. L'hexadécimal regroupe 4 bits par chiffre (0–9, A–F). Un octet = 2 chiffres hex → le 59

// Même valeur, trois représentations :
89          // décimal
0b01011001  // binaire (préfixe 0b)
0x59        // hexadécimal (préfixe 0x)

// En shader Metal, les couleurs sont souvent en hex :
float4 orange = float4(0xFF/255.0f, 0x5F/255.0f, 0x1F/255.0f, 1.0f);
//                       R=255         G=95         B=31       A=1

Un aperçu externe provenant du cours "CS50’s Introduction to Computer Science" plus détaillé ici

Un aperçu des couleurs représentée en hex du site web Adobe ici (à enregistrer dans tes favoris!)

Types de données — la taille compte

En C++ et MSL, chaque variable a un type qui dit deux choses : combien d'octets elle occupe, et comment interpréter ces octets.

Type C++/MSL Taille Plage Usage GPU typique
bool 1 bit true / false conditions
uint 32 bits 0 → 4 294 967 295 vertex_id, indices
int 32 bits -2.1G → +2.1G offsets, compteurs
float 32 bits ±3.4×10³⁸ positions, couleurs
half 16 bits ±65 504 textures HDR, perf
float4 4×32 = 128 bits — positions, couleurs RGBA
    Data Types
  1. float = nombre à virgule avec 6 chiffres de précision après la virgule allant de 0 à 340282346638528859811704183484516925440.000000, nombre arithmétique.
  2. double =
  3. Library Functions Manual ← fonctions C & C++

L'ordinateur n'est pas magique, en l'occurence on ne peut pas additionner deux

// 64-bit floats have 53 digits of precision, including the whole-number-part. double a = 0011111110111001100110011001100110011001100110011001100110011010; // imperfect representation of 0.1 double b = 0011111111001001100110011001100110011001100110011001100110011010; // imperfect representation of 0.2 double c = 0011111111010011001100110011001100110011001100110011001100110011; // imperfect representation of 0.3 double a + b = 0011111111010011001100110011001100110011001100110011001100110100; // Note that this is not quite equal to the "canonical" 0.3!
half sur GPU — très utile
Le type half (16 bits) n'existe pas en C++ standard mais existe en MSL. Sur Apple Silicon, les opérations half sont souvent 2× plus rapides que float car le GPU peut en traiter deux en parallèle. À utiliser pour les couleurs et les données de texture.

Une fonction — lire la signature

Une fonction () prend des entrées, fait un calcul, et retourne une sortie. La signature dit tout :

// Exemples de signatures — lire de gauche à droite

void   setup();                       // retourne rien, prend rien
float  distance(float3 a, float3 b);  // retourne un float, prend 2 vecteurs
float4 vertex_main(uint vid);         // retourne vec4, prend un uint

Le mot-clé return

Une fonction qui ne retourne pas void doit obligatoirement terminer par un return du bon type. En MSL, le vertex shader doit retourner une float4 — c'est la position en clip space.

vertex float4 vertex_main(uint vid [[vertex_id]])
{
    // … calculs …
    return float4(x, y, z, w); // obligatoire
}

Pointeurs — le concept qu'on ne peut pas éviter

Un pointeur est une variable qui contient l'adresse mémoire d'une autre variable, pas sa valeur. En metal-cpp, tout objet GPU est un pointeur.

// Une variable normale : contient une valeur
float x = 3.14f;

// Un pointeur : contient l'adresse où est stocké l'objet
MTL::Device* _device = MTL::CreateSystemDefaultDevice();
//         l'étoile = c'est un pointeur

// Accès aux méthodes via → (flèche) sur un pointeur
MTL::CommandQueue* _commandQueue = device->newCommandQueue();
Pourquoi tout est pointeur en metal-cpp
Les objets Metal vivent sur le tas (heap), pas sur la pile (stack). Leur durée de vie est gérée manuellement via retain() / release(). Un pointeur nul (nullptr) = l'objet n'existe pas encore — toujours vérifier après création.