PARTIE I : Fondations système

Chapitre 1 : Anatomie d'un noyau, OS ou moteur

1.1 Introduction : deux mondes, un même mot

Le terme « noyau » (kernel) recouvre deux réalités radicalement différentes en ingénierie logicielle. Et cette ambiguïté-là piège quiconque se lance dans la conception d’un moteur de jeu from scratch.

D’un côté, le noyau de système d’exploitation (OS Kernel, Linux, Windows NT, les micro-noyaux comme seL4 ou QNX) est le composant logiciel qui gère directement le matériel : allocation de mémoire physique, ordonnancement des processus, pilotes de périphériques, gestion des interruptions. C’est le gardien des ressources matérielles : il opère en mode superviseur (Ring 0 sur x86), avec un accès illimité à tout l’espace d’adressage.1

De l’autre, le noyau d’un moteur de jeu (Engine Kernel/Core), lui, est la boucle centrale de simulation, le heartbeat du moteur, qui orchestre chaque tick de la logique de jeu : lecture des entrées, simulation physique, mise à jour de l’état du monde, rendu graphique. Ce « noyau » opère en espace utilisateur (Ring 3), soumis aux contraintes et aux abstractions imposées par le système d’exploitation sous-jacent.2

La distinction compte : le noyau OS gère le temps (ordonnancement, interruptions d’horloge, timers) quand le noyau moteur, lui, le consomme, à une cadence qu’il s’impose lui-même. Il faut avoir cette dualité en tête avant d’aborder l’allocation mémoire, le déterminisme ou la synchronisation réseau.

1.2 Le noyau OS : architectures et compromis

1.2.1 Monolithique ou micro-noyau

L’architecture d’un noyau OS se situe sur un spectre entre deux extrêmes :

  • Noyau monolithique (Linux, FreeBSD) : l’ensemble des services système (ordonnanceur, système de fichiers, pile réseau, pilotes) s’exécute dans un espace d’adressage unique en mode privilégié. L’avantage est la performance brute : les appels entre sous-systèmes sont de simples appels de fonctions, sans surcoût de changement de contexte ni de passage de messages. L’inconvénient est la fragilité : un bogue dans un pilote graphique peut provoquer un Kernel Panic qui arrête instantanément toute la machine.3

  • Micro-noyau (QNX, seL4, L4) : le noyau est réduit au minimum vital, gestion de la mémoire virtuelle, ordonnancement des threads et mécanisme de communication inter-processus (IPC) par passage de messages. Les pilotes, systèmes de fichiers et piles réseau s’exécutent en espace utilisateur, isolés les uns des autres. La robustesse est supérieure : un pilote défaillant peut être redémarré sans affecter le système. Le coût est un surcoût de latence lié au passage de messages IPC entre les services.4

  • Noyau hybride (Windows NT, macOS/XNU) : un compromis pragmatique où certains services critiques (pilotes graphiques, système de fichiers) sont intégrés dans l’espace noyau pour la performance, tandis que d’autres restent en espace utilisateur pour l’isolation.

Pour un moteur de jeu, ce choix d’architecture sous-jacente n’est pas neutre. Un noyau monolithique comme Linux a une latence d’appel système minimale et un accès direct aux primitives de mémoire partagée, mais il expose le moteur aux instabilités des pilotes tiers. Un RTOS (Real-Time Operating System) micro-noyau comme QNX garantit à l’inverse des latences bornées (indispensable pour les interfaces matérielles BCI), au prix d’un écosystème de pilotes plus restreint.

1.2.2 Le système d’exploitation en temps réel (RTOS)

Dans le contexte du FullDive, les RTOS sont un cas à part. Là où l’ordonnanceur d’un OS à temps partagé (Linux, Windows) vise l’équité (fairness) entre les processus, un RTOS garantit des bornes temporelles strictes (hard real-time) ou moyennes (soft real-time) sur le temps de réponse.

L’ordonnancement hard real-time (EDF)

Au-delà de la simple préemption, un moteur FullDive exige un déterminisme absolu sur les délais d’exécution. L’algorithme canonique pour ces garanties est l’EDF (Earliest Deadline First). Le théorème fondamental de Liu & Layland (1973) démontre que sur un processeur unique, EDF peut atteindre une utilisation de 100 % du CPU tout en respectant toutes les échéances, à condition que la somme des utilisations des tâches soit 1\le 1.

Sauf que le passage aux architectures multi-cœurs (SMP) introduit l’anomalie de Dhall : dans un ordonnancement EDF global (GEDF), une tâche à très faible utilisation peut faire échouer une tâche critique et ruiner les garanties temporelles.

Architecture LplKernel : Partitioned-EDF

Pour contourner l’anomalie de Dhall, LplKernel s’oriente vers un modèle Partitioned-EDF (ou semi-partitionné). Les tâches critiques (comme la boucle de rendu VR ou l’acquisition BCI) sont épinglées (pinned) sur des cœurs physiques dédiés. La granularité du recalibrage temporel est assurée par le timer de l’APIC en mode TSC-Deadline, avec une précision au cycle d’horloge près, indispensable pour éviter le jitter dans le traitement des signaux neuronaux.

CaractéristiqueOS à Temps Partagé (Linux)RTOS (QNX, FreeRTOS)
ObjectifÉquité entre les processusRespect des délais stricts
OrdonnancementCFS (Completely Fair Scheduler)Priorité fixe avec préemption
Latence d’interruption~10-100 µs (variable)~1-10 µs (bornée)
Changement de contexte~1-10 µs (variable)~0.5-2 µs (déterministe)
Cas d’usageServeurs, desktops, mobilesAutomobile, médical, aérospatial

Pour le matériel BCI (OpenBCI, Galea), la communication avec les ADC (convertisseurs analogique-numérique) requiert une cadence d’échantillonnage stable. Un jitter (gigue) sur la lecture des échantillons EEG de seulement 2 ms peut introduire des artefacts spectraux parasites dans les bandes Mu (8-12 Hz) et Beta (13-30 Hz), et fausser complètement la classification des états cognitifs.5 C’est pourquoi les systèmes d’acquisition BCI professionnels s’appuient généralement sur des RTOS ou, à défaut, sur des threads noyau hautement prioritaires avec isolation complète sur un cœur CPU dédié (CPU pinning).

1.3 Le noyau moteur : la boucle de simulation

1.3.1 La boucle de jeu fondamentale

Tout moteur de jeu repose sur la Game Loop : une boucle infinie qui répète inlassablement trois opérations.

while (running) {
    ProcessInput();
    UpdateSimulation(dt);
    Render();
}

La question, la vraie, est : quelle valeur donner à dt ? De cette réponse dépend tout le reste : le moteur est déterministe ou non, et donc capable ou non de tenir une synchronisation réseau fiable.

1.3.2 Le pas de temps fixe (Fixed Timestep)

Dans une boucle naïve, dt est le temps écoulé depuis la dernière itération, un pas de temps variable (variable timestep). Cette approche est catastrophique pour le déterminisme : la simulation physique produit des résultats légèrement différents selon le framerate, ce qui rend impossibles les techniques de synchronisation réseau comme le Lockstep ou le Rollback Netcode.

La solution canonique est le pas de temps fixe : la simulation logique avance par incréments constants (par exemple, dt = 1/60 s = 16.667 ms), indépendamment du framerate de rendu. L’implémentation standard utilise un accumulateur de temps :

const double FIXED_DT = 1.0 / 60.0;  // 60 ticks par seconde
double accumulator = 0.0;
double previousTime = GetTime();

while (running) {
    double currentTime = GetTime();
    double frameTime = currentTime - previousTime;
    previousTime = currentTime;

    // Limiter le frameTime pour éviter la "spirale de la mort"
    if (frameTime > 0.25) frameTime = 0.25;

    accumulator += frameTime;

    ProcessInput();

    // Simulation à pas fixe
    while (accumulator >= FIXED_DT) {
        SimulateFixedStep(FIXED_DT);
        accumulator -= FIXED_DT;
    }

    // Interpolation pour le rendu (entre deux états de simulation)
    double alpha = accumulator / FIXED_DT;
    Render(alpha);
}

Le garde-fou if (frameTime > 0.25) limite les dégâts : si une frame prend exceptionnellement longtemps (par exemple, lors d’un chargement d’asset), l’accumulateur pourrait gonfler de plusieurs secondes de retard et forcer le moteur à enchaîner des dizaines de ticks de simulation d’affilée pour « rattraper ». C’est la spirale de la mort (death spiral) : le temps de simulation dépasse le temps réel, et les performances s’effondrent.6

1.3.3 Découplage simulation/rendu

Le pas de temps fixe amène une propriété architecturale de fond : la simulation et le rendu sont découplés. La simulation tourne à une fréquence fixe (60 Hz, 120 Hz), tandis que le rendu peut tourner à n’importe quelle fréquence (30 FPS sur mobile, 144 FPS sur un écran gaming, 90 Hz imposé par un casque VR).

Pour éviter les saccades visuelles entre deux ticks de simulation, le moteur effectue une interpolation de l’état visuel en utilisant le facteur alpha (le ratio d’avancement dans le tick courant). L’état rendu est ainsi une combinaison linéaire entre l’état précédent et l’état courant :

positionrendu=positiont1×(1α)+positiont×α\text{position}_{rendu} = \text{position}_{t-1} \times (1 - \alpha) + \text{position}_{t} \times \alpha

Ce découplage sépare les préoccupations : la logique de simulation peut être testée isolément (sans rendu), rejouée à l’identique (replay), et synchronisée sur le réseau. Trois propriétés qu’un moteur multijoueur déterministe ne peut pas s’offrir autrement.

1.4 Le modèle client-serveur et le tick autoritaire

1.4.1 L’architecture autoritaire

Dans un jeu multijoueur, tout tourne autour d’une question d’autorité : qui décide de l’état du monde ?

Deux postures sont possibles. Le client autoritaire simule localement et informe le serveur : simple, mais vulnérable à la triche, le client peut mentir sur sa position, ses dégâts, etc. Le serveur autoritaire est la seule source de vérité : les clients envoient leurs inputs (commandes), le serveur les applique dans sa simulation et renvoie l’état résultant. La triche est drastiquement réduite, mais la latence réseau introduit un retard perceptible.7

Le modèle retenu pour LplKernel est le serveur autoritaire, où le serveur exécute la simulation à pas fixe et diffuse l’état à tous les clients. Les clients effectuent une prédiction locale (ils simulent immédiatement l’effet de leurs propres inputs) puis réconcilient leur état avec celui du serveur lorsqu’ils reçoivent une mise à jour.

1.4.2 Trois modèles de synchronisation

ModèlePrincipeLatence PerçueComplexitéUsage Typique
Lockstep DéterministeTous les clients exécutent les mêmes inputs au même tick. Attente mutuelle.Élevée (dépend du joueur le plus lent)MoyenneRTS (StarCraft, AoE)
Serveur Autoritaire + PrédictionLe client prédit localement, le serveur corrige.Faible (masquée par la prédiction)HauteFPS, Action (Overwatch)
Rollback NetcodeSimulation locale immédiate. Si un input distant arrive en retard, rollback + resimulation.Très faibleTrès hauteJeux de combat (GGPO)

Le Lockstep impose une simulation parfaitement déterministe. Plus précisément : une même séquence d’inputs doit produire un état bit-à-bit identique sur toutes les machines. Cette exigence a des conséquences architecturales profondes qui seront détaillées au Chapitre 3 (arithmétique à virgule fixe) et au Chapitre 6 (infrastructure réseau).

Le Rollback Netcode, lui, courant dans les jeux de combat compétitifs, réunit les deux avantages : il permet une simulation locale immédiate (zéro latence perçue) tout en gérant les désynchronisations en « revenant en arrière » (rollback) pour rejouer les ticks avec les inputs corrigés. Il exige à la fois un déterminisme strict et la capacité de sauvegarder ou restaurer des snapshots d’état à très haute fréquence, une opération qui pèse lourd sur la gestion mémoire (Chapitre 2).

1.4.3 Le contrat de timer déterministe

Quel que soit le modèle de synchronisation retenu, le tick autoritaire requiert un signal temporel fiable et prédictible : un contrat de propriété du timer qui détermine quelle source matérielle génère les interruptions périodiques.

LplKernel implémente ce contrat via une couche d’abstraction clock_* indépendante du backend matériel :

APIRôle
clock_initialize()Sélectionne le backend timer selon le profil et les capacités matérielles
clock_get_tick_hz()Retourne la fréquence de tick effective (ex: 100 Hz)
clock_get_tick_count()Retourne le compteur monotone de ticks depuis le boot
clock_read_walltime()Lecture horloge murale (RTC, polling uniquement)

Politique par profil : En Phase 3 du noyau, les deux profils (client et serveur) utilisent PIT IRQ0 comme propriétaire du tick à 100 Hz. Cette fréquence de bring-up est verrouillée intentionnellement pour la continuité déterministe. La RTC fournit l’heure murale en mode polling uniquement : elle n’est jamais propriétaire du tick scheduler.

Migration APIC : Le backend APIC est disponible en tant que chemin expérimental :

  1. Late-init : mapping MMIO du LAPIC après init du paging/PMM.
  2. Calibration : mesure de fréquence du timer APIC via référence PIT.
  3. Handoff : transfert de propriété du tick vers l’APIC en mode périodique, masquage de PIT IRQ0.

Le design-clé est que le code moteur/runtime ne doit jamais référencer directement les symboles PIT/RTC/PIC : seules les APIs clock_* sont contractuelles. Lors de la migration vers APIC ou SMP, le backend change mais l’interface reste identique.

1.5 Synthèse : le cahier des charges architectural

L’analyse des noyaux OS et moteur révèle un ensemble de contraintes non négociables pour l’architecture de LplKernel :

ContrainteOrigineConséquence Architecturale
Pas de temps fixeDéterminisme réseauBoucle de simulation découplée du rendu
Latence bornéeMatériel BCI (EEG)Thread d’acquisition isolé, CPU pinning
Autorité serveurAnti-triche, cohérencePrédiction client + réconciliation
Snapshot rapideRollback NetcodeAllocateurs mémoire déterministes (Ch. 2)
Bit-exactLockstepArithmétique à virgule fixe (Ch. 3)
Isolation des pannesStabilité productionArchitecture modulaire (Ch. 4)

Ces contraintes s’appliquent à tous les chapitres suivants. Le chapitre 2 abordera la première d’entre elles en profondeur : comment allouer et gérer la mémoire de manière à satisfaire simultanément les exigences de performance, de déterminisme et de latence bornée.

Notes de bas de page du chapitre 1


Footnotes

  1. L’architecture x86 définit 4 niveaux de privilège (Ring 0 à Ring 3). Le noyau OS s’exécute en Ring 0 avec un accès complet au matériel, tandis que les applications utilisateur s’exécutent en Ring 3 avec des permissions restreintes.

  2. La distinction entre « kernel » OS et « kernel » moteur est un piège récurrent dans la littérature technique anglophone, où le même terme est utilisé sans qualification.

  3. Le noyau Linux, bien que monolithique, supporte les modules chargeables (Loadable Kernel Modules) qui permettent d’ajouter ou de retirer des pilotes sans recompiler le noyau. Cela atténue certains inconvénients du monolithisme, sans pour autant offrir l’isolation d’un micro-noyau.

  4. Les micro-noyaux modernes comme seL4 ont été formellement vérifiés, c’est-à-dire qu’une preuve mathématique garantit l’absence de certaines classes de bogues (déréférenciation de pointeur nul, débordement de tampon, etc.).

  5. Le théorème de Nyquist-Shannon impose que la fréquence d’échantillonnage soit au minimum le double de la fréquence maximale du signal d’intérêt. Pour capturer la bande Beta (jusqu’à 30 Hz), le minimum théorique est donc de 60 Hz ; en pratique, on échantillonne bien plus haut, pour la résolution spectrale et le rejet des artefacts. La carte OpenBCI Cyton opère ainsi à 250 Hz sur 8 canaux.

  6. Gaffer on Games, « Fix Your Timestep! » : article de référence sur l’implémentation correcte d’une boucle à pas de temps fixe avec accumulateur et interpolation.

  7. Gabriel Gambetta, « Fast-Paced Multiplayer » : série d’articles détaillant l’architecture client-serveur avec prédiction et réconciliation, considérée comme une référence dans l’industrie.