Chapitre -125 · Le pont entre C++ & Objective-C

Tout ce dont tu as besoin pour ouvrir une MTKView et peut-être lancer ta première application

À partir de ce chapitre, nous ajouterons du code petit à petit au même projet, sur des dizaines de chapitres.

Ton fichier main : ton programme débute son exécution ici

Ci-dessous se trouve le seul et unique fichier écrit en Objective-C du projet, nous l'améliorerons sans oublier notre objectif. Grâce à lui, tu peux lancer une application macOS, on s'occupera un peu plus tard du rendu sur l'environnement IOS, iPadOS etc…

Le langage de ce début de chapitre sera rarement utilisé mais il est indispensable.

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*                                        +       +          */
/*      File: main.m            +++     +++                  */
/*                                        +       +          */ // bloc de commentaire
/*      By: Laboitederemdal                +       +         */ // pour personnaliser le tien :
/*                                       +           +       */ // → Configuration ZSH .zshrc
/*      Created: 16/04/2026 04:01:42      + + + + + +        */
/*      Updated: 27/08/2026                                  */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#import <Cocoa/Cocoa.h>

#import "AppViewController.h"

int main(int argc, const char* argv[])
{
    NSApplication* application = [NSApplication sharedApplication];

    ApplicationController* applicationController = [[ApplicationController alloc] init];

    application.delegate = applicationController;
    
    return (NSApplicationMain(argc, argv));
}

À partir de ce fichier tu peux déjà créer et personnaliser le menu de ton application à droite du menu Pomme grâce à NSMenu, NSMenuItem, NSSlider,…

Il n'existe pas de fichier header pour le fichier main, peu importe le langage utilisé. Aucun autre fichier n'en a besoin.

Le menu est présenté en temps voulu, dans le chapitre dédié afin de régler l'EDRBias & le Brightness.

La fenêtre, la vue Metal et notamment les entrées claviers/souris passent par AppKit — le framework UI de macOS, écrit en Objective-C. On ne peut pas y échapper. Mais on limite le contact aux fichiers .mm qui font le pont entre l'ObjC et notre code C++ pur.

Le principe du pont ObjC ↔ C++
AppViewController.mm gère uniquement la fenêtre et les callbacks système. Il instancie notre rendu C++ et lui passe le pointeur vers notre GPU MTL::Device.

Un point rapide sur les éléments utiles à notre projet ;

  1. Cocoa.h → framework Cocoa (sans wrapper C++) → il prend directement le code en ObjC de ton OS, et permet notamment d'utiliser l'objet/classe NSApplication avec toutes ses propriétés, toutes ses méthodes et tous ses protocoles.
  2. NSApplication → gère la boucle d'événement principale de toute application et conserve une liste de tous les objets NSWindow pour distribuer les événements aux objets NSResponder appropriés sauf si la touche Commande est enfoncée lorsqu'un événement de frappe se produit.
  3. sharedApplication → méthode (fonction associée à une classe) NSApplication qui initialise l'environnement d'affichage et connecte notre programme au serveur de fenêtres. Elle crée l'instance unique au premier appel, puis renvoie toujours la même : on récupère ici un pointeur vers l'application, que l'on nomme application.
  4. ApplicationController → alloue (allocate) de la mémoire pour ton objet Objective-C++ et l'initialise. C'est notre classe écrite en Objective-C++.
  5. .delegate = …; → propriété de NSApplication → notre objet devient le délégué de la boucle/de l'application. (setter). De cette manière, notre application reçoit directement les NSNotification et nous pouvons les gérer dans notre classe — et comme elle est écrite en ObjC++, elle nous ouvre la porte du C++!.
    Attention : NSApplication.delegate est une référence weak. C'est la variable locale applicationController qui maintient l'objet en vie — ce qui suffit ici, puisque la fonction ne se termine jamais.
  6. NSApplicationMain → fonction main() de l'application, elle démarre la boucle, un peu comme ton système d'exploitation pour afficher constamment à l'écran, c'est là que la magie opère - On passe de rien à une application fonctionnelle, on va pouvoir interagir avec l'ordinateur autrement que par le terminal, contrairement au C & C++, cette fonction ne renvoie rien, elle s'arrête lorsque l'on interrompt la boucle. Tout comme ton système d'exploitation (en partie).
  7. argc / argv → identique au C et autres, pour notre part, l'application se lancera sans prendre d'entrée (fichier image, argument texte, …) et affichera nos rendus. (argument count = 1) : le chemin de l'exécutable est toujours passé en premier argument. Aperçu dans le chapitre concernant la vitamine-C.
Command Directory ou directement dans tes Wrappers

Où explorer les fonctionnalités existantes de chaque objet des frameworks Apple - directement dans les headers ObjC (souvent commentés par Apple) - ou click-droit dans Xcode sur l’élément.

C'est ici que ton include #import <Cocoa/Cocoa.h> va chercher le code ObjC :

/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks

Création du projet

Xcode (pour les plus gros projets, recommandé)

Nouveau projet dans Xcode
⌘command
+
⇧shift
+
N
→ Multiplateforme → Jeu → Metal ou Metal 4 → Objective-C → Créez un git repository si vous voulez sauvegarder votre travail.

Personnalisé (make)

Parce que c'est juste un fichier qui facile la compilation sans utiliser Xcode, nous te proposons la méthode la + répondue dans le monde du coding.

Dans le chapitre → Makefile se trouve un exemple solide et expliqué (en Shell).

Structure des fichiers

Un fois sur le projet tu peux le réorganiser de cette manière :

MSLearn/
├── MSLearn.entitlements        ← optionnel
├── Application/                ← sources Objective-C / Objective-C++
│   ├── main.m
│   ├── AppViewController.h
│   ├── AppViewController.mm
│   └── macOS/
│       └── macOSInfo.plist
├── Frameworks/                 ← headers officiels Apple
│   ├── metal-cpp/
│   └── metal-cpp-extensions/
├── Renderer/                   ← logique de rendu + headers .hpp
│   ├── Renderer.cpp
│   └── Renderer.hpp
├── Shaders/                    ← shaders
│   └── Fullscreen.metal        ← fichier MSL
├── includes/                   ← headers .h (C++ / MSL)
│   └── SharedGPU/              ← des fichiers partagés entre les langages C++ & MSL
│       └─── Renderer_shared.h
├── Makefile
└── .gitignore                  ← du chapitre précédent, fichier caché
Fichier .entitlements

Paires clé-valeur qui accordent une autorisation exécutable pour utiliser un service ou une technologie.

Un fichier exemple de fichier macOSInfo.plist

→ Tu as peut-être dû supprimer le fichier Storyboard généré par Xcode ainsi que l'appel de celui-ci. Cette liste (voir photo) correspond au fichier .plist que nous allons rédiger manuellement, écrit en XML (langage de balisage).

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>$(PRODUCT_NAME)</string>
    <key>CFBundleShortVersionString</key>
    <string>$(MARKETING_VERSION)</string>
    <key>CFBundleSupportedPlatforms</key>
    <array>
        <string>MacOSX</string>
    </array>
    <key>CFBundleVersion</key>
    <string>$(CURRENT_PROJECT_VERSION)</string>
    <key>GCSupportsControllerUserInteraction</key>
    <true/>
    <key>LSApplicationCategoryType</key>
    <string>public.app-category.graphics-design</string>
    <key>LSMinimumSystemVersion</key>
    <string>26.0</string>
    <key>MetalCaptureEnabled</key>
    <true/>
    <key>NSHumanReadableCopyright</key>
    <string>Copyright © 2026 Laboitederemdal. All rights reserved.</string>
    <key>CFBundleIconFile</key>
    <string>AppIcon.icns</string>
    <key>NSPrincipalClass</key>
    <string>NSApplication</string>
    <key>CFBundlePackageType</key>
    <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
    <key>CFBundleDisplayName</key>
    <string>MSLearn</string>
</dict>
</plist>
Pourquoi NSPrincipalClass est obligatoire

Sans storyboard ni nib principal, c'est cette clé qui indique à macOS quelle classe instancier au lancement. Si tu l'oublies, l'application se lance sans jamais afficher de fenêtre.

Si tu utilises le Makefile, les variables $() du .plist ne sont pas connues par celui-ci, remplace simplement les valeurs par une chaîne de caractères comme suit :

XML
<key>CFBundleExecutable</key>
<string>MSLearn</string>
Autres fichiers

Pour l'instant nous n'utiliserons pas de Storyboard (.storyboard)

Le fichier .entitlements, tu peux le garder.

Configuration de l'application

Celle-ci est actuellement vide, on déclare alors ce dont on a besoin et ce qui est nécessaire.

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*                                        +       +          */
/*      File: AppViewController.h            +++     +++     */
/*                                        +       +          */
/*      By: Laboitederemdal                +       +         */
/*                                       +           +       */
/*      Created: 16/04/2026 04:59:56      + + + + + +        */
/*      Updated: 27/08/2026                                  */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#import <MetalKit/MetalKit.h> // MTKView / MTKViewDelegate

@interface ApplicationController : NSObject <NSApplicationDelegate, NSWindowDelegate, MTKViewDelegate>

@property (nonatomic, strong, readonly) MTKView* _Nonnull mtkView;
// se terminera
- (void)applicationWillTerminate:(NSNotification* _Nonnull)notification;
// a terminé son lancement
- (void)applicationDidFinishLaunching:(NSNotification* _Nonnull)notification;
// doit se terminer après la fermeture de la dernière fenêtre
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication* _Nonnull)sender;

- (void)drawInMTKView:(MTKView* _Nonnull)view;

- (void)mtkView:(MTKView* _Nonnull)view drawableSizeWillChange:(CGSize)size;

@end

Un nouveau point rapide :

  1. NSApplicationDelegate / NSWindowDelegate → protocole (signatures de méthodes publiques). Il s'agit donc d'un ensemble de méthodes accessibles depuis l'extérieur d'une classe, par lesquelles on peut agir sur un objet, ou plus généralement communiquer avec lui.
  2. NSNotification → classe appartenant à Foundation → communication entre l'OS et notre app.
  3. NSObject → classe racine de la plupart des hiérarchies Objective-C, à partir de laquelle les sous-classes héritent d'une interface de base au système d'exécution et de la capacité de se comporter comme des objets Objective-C.
  4. Prototypes des méthode application… → méthodes de cycle de vie Appkit. Les redéclarer ici n'est pas obligatoire (les protocoles les fournissent déjà), mais ça rend le contrat public de ApplicationController explicite : main.m importe ce header et en crée une instance. Le compilateur vérifie que tu respectes ce contrat et les protocoles adoptés..
  5. draw() → "ton terrain de jeu", on va dessiner ici après avoir nous aussi créé des fonctions et shaders.
  6. drawableSizeWillChange → appelée à chaque fois que la taille du drawable change.
  7. MTKViewDelegate → protocole qui fournit drawInMTKView & drawableSizeWillChange pour une MTKView (MTK::View côté C++).

Sur ce site, tu verras également comment mettre en place l'autre manière d'utiliser Metal — via un CAMetalLayer (Core Animation Layer) — qui demande davantage de configuration.

La principale différence entre Cocoa et AppKit tient au fait que Cocoa est un framework d'application complet (un parapluie), tandis qu'AppKit en est la sous-partie spécialisée dans l'interface utilisateur sur macOS.

MetalKit.h importe AppKit en entier ; MetalKit.hpp importe AppKit.hpp.

Le préfixe NS vient de NeXTSTEP : on le retrouve aussi bien dans Foundation (NSString, NSArray, NSNotification) que dans AppKit (NSWindow, NSView, NSApplication). Ce n'est donc pas un marqueur de framework.

AppKit.hpp n'expose qu'une petite fraction de AppKit.h : le strict nécessaire pour créer une fenêtre et une vue. Ce n'est pas une limitation — ce qui manque, on l'écrit dans notre fichier .mm.

L'implémentation

Il a été décidé de créer une fonction pour créer la fenêtre et une autre pour créer la vue en supplément des fonctionnalités fournies par l'API et l'OS.

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*                                        +       +          */
/*      File: AppViewController.mm            +++     +++    */
/*                                        +       +          */
/*      By: Laboitederemdal                +       +         */
/*                                       +           +       */
/*      Created: 16/04/2026 05:43:06      + + + + + +        */
/*      Updated: 27/08/2026                                  */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#import "../Renderer/Renderer.hpp"

#import "AppViewController.h"

#pragma mark - RenderWindow                                 // Ignoré par le compilateur
#pragma region RenderWindow {                               // indication visuelle

@interface RenderWindow : NSWindow                          // Déclaration de notre classe ObjC

@property (weak) ApplicationController* appCoordinator;     // Un pointeur vers notre classe ObjC++
                                                            // observe son propriétaire → weak
@end

@implementation RenderWindow                                // Implémentation de notre classe ObjC
{
}

- (instancetype)initWithContentRect:(NSRect)rect
                          styleMask:(NSWindowStyleMask)mask // Paramètres de la fonction (NSRect inclus)
                            backing:(NSBackingStoreType)backing
                              defer:(BOOL)flag
                             screen:(NSScreen*)screen
{
    self = [super initWithContentRect:rect styleMask:mask backing:backing defer:flag screen:screen];
    return self;
}

@end

#pragma endregion RenderWindow }

#pragma mark - ApplicationController
#pragma region ApplicationController {

@implementation ApplicationController
{
    RenderWindow*             _window;      // possède RenderWindow → strong
    std::unique_ptr<Renderer> _renderer;    // Notre classe C++
}

- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender
{
    return (YES);
}

- (void)applicationDidFinishLaunching:(NSNotification*)notification
{
    [self createWindow];                      // des fonctions à nous, qui sont appelées
    [self createView];
}

- (void)applicationWillTerminate:(NSNotification*)notification
{
    _renderer.reset();                        // Détruit (release) "std::unique_ptr"
}

- (void)createWindow
{
    NSWindowStyleMask mask = NSWindowStyleMaskClosable | NSWindowStyleMaskTitled |
                                NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
    NSRect contentRect = NSMakeRect(0, 0, 1280, 720);
    NSScreen* screen = [NSScreen mainScreen];
    contentRect.origin.x = 0;
    contentRect.origin.y = (screen.frame.size.height / 2) - (contentRect.size.height / 2);
    _window = [[RenderWindow alloc] initWithContentRect:contentRect styleMask:mask
                                                backing:NSBackingStoreBuffered defer:NO screen:screen];
    _window.releasedWhenClosed = NO;
    _window.minSize = NSMakeSize(640, 360);
    _window.delegate = self;
    _window.appCoordinator = self;
    screen = _window.screen;
    NSString* title = [NSString stringWithFormat:@"LearnMSL.dev : MSL x C++ (%@ @ %ldHz, EDR max: %.2f)",
                       screen.localizedName, (long)screen.maximumFramesPerSecond,
                       screen.maximumPotentialExtendedDynamicRangeColorComponentValue];
    _window.title = title;
    [_window makeKeyAndOrderFront:_window];
    [_window setIsVisible:YES];
    [_window makeMainWindow];
}

- (void)createView
{
    id<MTLDevice> device = MTLCreateSystemDefaultDevice();
    _mtkView = [[MTKView alloc] initWithFrame:_window.contentLayoutRect device:device];
    _mtkView.preferredFramesPerSecond = 120;
    _mtkView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
    _mtkView.colorPixelFormat = MTLPixelFormatRGBA16Float;
    _mtkView.depthStencilPixelFormat = MTLPixelFormatDepth32Float;
    _mtkView.colorspace = CGColorSpaceCreateWithName(kCGColorSpaceExtendedLinearDisplayP3);
    _mtkView.framebufferOnly = YES;
    _mtkView.paused = NO;
    _mtkView.delegate = self;
    _mtkView.enableSetNeedsDisplay = NO;
    NSTrackingArea* trackingArea = [[NSTrackingArea alloc]
                                    initWithRect:_mtkView.bounds
                                    options:(NSTrackingMouseMoved | NSTrackingActiveInKeyWindow |
                                                    NSTrackingInVisibleRect | NSTrackingActiveAlways)
                                    owner:self userInfo:nil];
    [_mtkView addTrackingArea:trackingArea];
    NSScreen* screen = _window.screen ?: [NSScreen mainScreen];
    CGFloat scale = screen.backingScaleFactor ?: 1.0;
    CGSize sizePts = _mtkView.bounds.size;
    _mtkView.drawableSize = CGSizeMake(sizePts.width * scale, sizePts.height * scale);
    _window.contentView = _mtkView;
    _renderer = std::make_unique<Renderer>((__bridge MTL::Device *)_mtkView.device,
                                           NSBundle.mainBundle.resourcePath.UTF8String,
                                           (NS::UInteger)_mtkView.drawableSize.width,
                                           (NS::UInteger)_mtkView.drawableSize.height,
                                           (MTL::PixelFormat)_mtkView.colorPixelFormat,
                                           (MTL::PixelFormat)_mtkView.depthStencilPixelFormat);
}

- (void)drawInMTKView:(nonnull MTKView *)view
{
    static CFTimeInterval lastTime = CACurrentMediaTime();
    CFTimeInterval now = CACurrentMediaTime();
    float delta = (float)(now - lastTime);
    lastTime = now;
    delta = fminf(delta, 0.1f);
    _renderer->draw((__bridge MTK::View *)view, now, delta);
}

- (void)mouseMoved:(NSEvent *)event
{
}

- (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size
{
    _renderer->resizeViewWindow((NS::UInteger)size.width, (NS::UInteger)size.height);
}

@end

#pragma endregion ApplicationController }

Pour mieux comprendre les fonctions "obligatoires" de chaque protocoles à mettre en place → Objective-C++ vs C++ : synthaxe & documentation. (Tu y arriveras dans quelques chapitres.)

Un nouveau point rapide :

  1. applicationShouldTerminateAfterLastWindowClosed → le protocole qui renvoie si oui (YES) ou non (NO) l'application doit être fermée.
  2. applicationDidFinishLaunching → méthode de délégué appelée une fois le lancement terminé. C'est le bon endroit pour créer la fenêtre et la vue.
  3. applicationWillTerminate → appelée juste avant la fermeture. On y libère le renderer, avant que le processus ne disparaisse.
  4. MTLCreateSystemDefaultDevice() → fonction C (pas un protocole) qui renvoie le GPU par défaut du système. Sur les anciens Macs Intel à double GPU, l'appel provoquait le basculement vers le GPU haute performance ; sur Apple Silicon il n'y a qu'un seul GPU, donc pas d'ambiguïté.
  5. CACurrentMediaTime() → horloge absolue de Core Animation → pour mesurer le temps écoulé entre deux frames, parce qu'on veut des animations !
  6. __bridge → (~cast) liaison de pointeurs entre objets Objective-C et objets Core Foundation (toll-free bridging), sans transfert de propriété : le compilateur ne touche pas au compteur de références. C'est exactement ce qu'il nous faut pour passer une MTKView ou un MTLDevice à metal-cpp.
  7. mouseMoved() → puisque NSTrackingMouseMoved est activé, la méthode doit exister dans notre classe pour éviter une erreur de selector à l'exécution.
  8. NSTrackingArea → les options « actives » sont exclusives : on en choisit une seule (NSTrackingActiveInKeyWindow ici). Passer NSTrackingActiveAlways en plus est contradictoire.

Cycle de vie

macOS · AppKit
           main.m ----- NSApplication → boucle d'events macOS
Objective-C · pont             | delegate
      ------------- ApplicationController → cycle de vie, resize
      | strong       |         |         | delegate
      |             |          |      MTKView → surface de dessin - EDR - 120 images par secondes
      |            | weak      |
RenderWindow - - -             | unique_pointeur
→ inputs                       |
C++ · Metal                    |
                            Renderer → draw - commandQueue
                               |
                          GPU · Metal → commandBuffer - drawable

Nous aurons besoin d'un pont Objective-C (le langage très utilisé par Apple et obligatoire pour notre projet) afin de gérer l'audio, les manettes de jeu, l'écran tactile, …)

N'hésite pas à personnaliser ta fenêtre. Par exemple, une ligne suffit pour la centrer :

// dans notre fonction createWindow() après l'allocation
[_window center];

Tu pourrais également lancer l'application en plein écran par défaut.

Nous l’avons placée au milieu à gauche. Quasiment tout est là pour configurer la fenêtre, ce qui n'est pas expliqué ici le sera après. De cette manière tu peux déjà ouvrir une fenêtre — une application !

La classe RenderWindow (classe privée), définie entièrement dans AppViewController.mm n'a pas de header du tout, personne d'autre n'en a besoin.

Les méthodes d'input → pas de prototype ?

Ce sont des overrides de NSResponder — elles existent déjà dans la superclasse NSWindow. Tu ne crées pas une nouvelle interface, tu écrases un comportement existant. Le compilateur n'a pas besoin qu'on lui redéclare ce qu'il connaît déjà. Si tu compares avec les Wrappers C++, le principe est le même.

Automatic Reference Counting

Code de gestion mémoire injecté par le compilateur à la compilation (retain/release), que tu aurais écrit à la main en MRC (Manual Reference Counting).

ApplicationController* controller = [[ApplicationController alloc] init];

// Ce que le compilateur ARC ajoute — alloc/init renvoie déjà +1,
// ARC n'ajoute donc pas de retain, seulement le release de fin de portée
ApplicationController* controller = [[ApplicationController alloc] init];  // +1
[controller retain];
...
[controller release];   // inséré automatiquement

Chaque objet Obj-C a un retain count interne :

strong      +1 au retain count                  Propriétaire
weak        0, mis à nil si l'objet meurt       Delegate, référence inverse
assign      0, pas mis à nil (dangereux)        Types primitifs uniquement
copy        +1 sur une copie                    NSString, blocs

Nous verrons retain() qui count automatiquement dans notre C++ (notamment le pointeur vers MTLDevice)

Retain cycle
A ──strong──▶ B     retain count A = 1 (tenu par B)
▲              │    retain count B = 1 (tenu par A)
└───strong─────┘    → jamais libérés, même si plus personne d'autre ne les référence

ApplicationController  ──strong───▶ RenderWindow
          ▲                              │
          └────────────────weak──────────┘

ARC ne concerne que le code ObjC, ne t'en inquiète pas trop.

En MSL & C++ on gère manuellement la mémoire, nous avons tout de même un RAII maison qui simule ARC.

RAII

Resource Acquisition Is Initialization ou « l'acquisition de ressource est une initialisation » est un idiome de programmation, principalement utilisé en C++, liant la gestion des ressources (mémoire, fichiers, mutex) à la durée de vie d'un objet. La ressource est acquise au constructeur et libérée automatiquement dans le destructeur, garantissant une libération sûre.

Le minimum en C++

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*                                        +       +          */
/*      File: Renderer.hpp            +++     +++            */
/*                                        +       +          */
/*      By: Laboitederemdal                +       +         */
/*                                       +           +       */
/*      Created: 16/04/2026 10:33:22      + + + + + +        */
/*      Updated: 27/08/2026                                  */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#ifndef RENDERER_HPP
#define RENDERER_HPP

#include <string>                     // std::string (utilisé dans le constructeur)
#include <simd/simd.h>                // all simd

#include <MetalKit/MetalKit.hpp>      // fichier C++ → wrapper C++

class Renderer
{
public:
    Renderer(MTL::Device* device,     // constructeur
             const std::string& resourcePath,
             NS::UInteger width, NS::UInteger height,
             MTL::PixelFormat colorPixelFormat, MTL::PixelFormat depthPixelFormat);
    ~Renderer();                      // destructeur

    void draw(MTK::View* view, double timeStamp, float delta);
    void resizeViewWindow(NS::UInteger width, NS::UInteger height);

private:
    void update(float delta);

    MTL::Device*                m_device;
    MTL::Library*               m_shaderLibrary;
    MTL::CommandQueue*          m_commandQueue;
    MTL::PixelFormat            m_pixelFormat;
    MTL::PixelFormat            m_depthPixelFormat;

    uint64_t                    frame;
};

#endif /* RENDERER_HPP */

C'est presque terminé :

  1. Construction de notre classe principale → elle reçoit la taille de la fenêtre, le chemin du dossier des assets externes, les formats de pixels dont on a besoin et le pointeur vers notre GPU.
  2. Library → les shaders compilés, dans le .metallib.
  3. GPU → il alloue la mémoire sur la puce (un M2 Pro dans mon cas), lit les fonctions de vertex et de fragment shader écrites en MSL, détient la file de commandes, …
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*                                        +       +          */
/*      File: Renderer.cpp            +++     +++            */
/*                                        +       +          */
/*      By: Laboitederemdal                +       +         */
/*                                       +           +       */
/*      Created: 16/04/2026 10:33:18      + + + + + +        */
/*      Updated: 27/08/2026                                  */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#define NS_PRIVATE_IMPLEMENTATION
#define CA_PRIVATE_IMPLEMENTATION
#define MTL_PRIVATE_IMPLEMENTATION
#define MTK_PRIVATE_IMPLEMENTATION

#include "Renderer.hpp"

Renderer::Renderer(MTL::Device* device,
                   const std::string& resourcePath,
                   NS::UInteger width, NS::UInteger height,
                   MTL::PixelFormat colorPixelFormat, MTL::PixelFormat depthPixelFormat)
: m_device(device->retain()),
m_shaderLibrary(m_device->newDefaultLibrary()),
m_commandQueue(m_device->newCommandQueue()),
m_pixelFormat(colorPixelFormat), m_depthPixelFormat(depthPixelFormat),
frame(0)
{
}

Renderer::~Renderer()
{   // ← pointeurs, on libère les cases mémoire allouées.
    m_commandQueue->release();
    m_shaderLibrary->release();
    m_device->release();
}

void Renderer::update(float delta)
{
}

void Renderer::draw(MTK::View* view, double timeStamp, float delta)
{
    NS::AutoreleasePool* autoreleasePool = NS::AutoreleasePool::alloc()->init();
    
    frame += 1;

    MTL::CommandBuffer* commandBuffer = m_commandQueue->commandBuffer();
    
    MTL::RenderPassDescriptor* renderPassDescriptor = view->currentRenderPassDescriptor();
    {
        MTL::RenderCommandEncoder* renderCommandEncoder = commandBuffer->renderCommandEncoder(renderPassDescriptor);
        
        renderCommandEncoder->endEncoding();
    }
    
    commandBuffer->presentDrawable(view->currentDrawable());
    commandBuffer->commit();
    commandBuffer->waitUntilCompleted();
    
    autoreleasePool->release();
}

void Renderer::resizeViewWindow(NS::UInteger width, NS::UInteger height)
{
}
Deux pièges dans ce premier Renderer

newDefaultLibrary() renvoie nullptr tant qu'aucun .metallib n'est embarqué dans le bundle — c'est le cas si tu n'as pas encore de fichier .metal. D'où les tests dans le destructeur : appeler release() sur un pointeur nul plante.

waitUntilCompleted() bloque le CPU jusqu'à la fin du travail GPU. C'est pratique pour valider un premier rendu et pour déboguer, mais ça sérialise CPU et GPU. On l'enlèvera dans le chapitre consacré à la synchronisation.

⬡CheckPoint Compilation — Tu peux effectuer ta première compilation !

Et pour terminer :

  1. newDefaultLibrary() → charge le .metallib compilé depuis les shaders du projet.
  2. newCommandQueue() → crée la file de commandes GPU. C'est par elle que tous les commandBuffer seront soumis au GPU.
  3. NS::AutoreleasePool → zone mémoire temporaire qui libère automatiquement les objets autoreleased à la fin de chaque frame. Indispensable dans la boucle de rendu metal-cpp.
  4. MTL::CommandBuffer → enregistre une séquence de commandes GPU pour une seule frame (une seule image). Soumis au GPU via commit().
  5. MTL::RenderPassDescriptor → décrit les attachements de rendu (color, depth) d'un pass. Fourni par la MTKView via currentRenderPassDescriptor().
  6. MTL::RenderCommandEncoder → encode les draw calls, les états du pipeline, les buffers et les textures d'un render pass.
Évidemment

Tu peux juste copier-coller le code et changer à ta façon, c'est relativement intuitif.

Normalement tu auras une fenêtre noire à l'exécution, parce que la texture du pass est vide (00000…). Pour la changer :

/// C++ — Renderer.cpp — update:18/05/26
MTL::RenderPassDescriptor* renderPassDescriptor = view->currentRenderPassDescriptor();
renderPassDescriptor->colorAttachments()->object(0)->setClearColor(MTL::ClearColor(0.9, 0.9, 0.9, 1.0));
{
    MTL::RenderCommandEncoder* renderCommandEncoder = commandBuffer->renderCommandEncoder(renderPassDescriptor);
⬡CheckPoint Compilation

Il y a quelques exemples de couleurs dans le chapitre → Couleurs.