🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Implémenter le jeu de la vie de Conway

Conception

Avant de nous plonger dans le sujet, nous devons prendre en considération quelques choix de conception.

Un univers infini

Le jeu de la vie se déroule dans un univers infini, mais nous n'avons pas une mémoire et une puissance de calcul infinie. Pour contourner cette limitation plutôt ennuyeuse, il a généralement trois possibilités :

  1. Identifier dans quel sous-ensemble de l'univers il se passe des choses intéressantes, et agrandir cette zone si nécessaire. Dans le pire des cas, cette expansion se fera sans limites et donc la simulation deviendra de plus en plus lent et arrivera à cours de mémoire.
  1. Créer un univers à taille fixe, dans lequel les cellules sur ses bords auront moins de voisines que les cellules au centre. Le désavantage de cette approche est que les schémas infinis, comme les planeurs, qui atteignent probablement la fin de l'univers, seront éliminés.
  1. Créer un univers à taille fixe, mais en boucle, où les cellules sur les bords seront directement voisines de celles qui sont de l'autre côté de l'univers. Comme les voisines de recoupent d'un bout à l'autre de l'univers, les planeurs pourront continuer à vivre à l'infini.

Nous allons implémenter la troisième option.

Interfacer Rust et le JavaScript

⚡ C'est l'un des concepts les plus importants à comprendre et à retenir de ce tutoriel !

Le tas du JavaScript qui est géré par le ramasse-miettes — dans lequel sont stockés les objets Object, les tableaux Array, et les noeuds du DOM — se distingue de l'espace mémoire linéaire du WebAssembly, dans lequel vivent nos valeurs Rust. WebAssembly n'a actuellement pas d'accès direct au tas géré par le ramasse-miettes (du moins en avril 2018, cela peut changer à l'avenir avec la proposition des "Interface Types"). JavaScript, de l'autre côté, peut lire et écrire sur l'espace mémoire linéaire de WebAssembly, mais seulement via un ArrayBuffer de valeurs scalaires (comme le u8, i32, f64, etc ...). Les fonctions WebAssembly prennent elles aussi des valeurs scalaires et en retourne. Ce sont les éléments de base sur lesquels repose la communication entre WebAssembly et JavaScript.

wasm_bindgen définit une vision partagée pour travailler avec des structures composées pour passer outre ces limites. Cette crate passe une structure Rust dans une std::boxed::Box et enveloppe ce pointeur dans une classe JavaScript pour faciliter son utilisation, ou utilise des indices dans une table d'objets dans Rust qui représentent des objets JavaScript. wasm_bindgen est très utile, mais nous devons toujours garder en tête comment les données sont modélisées, et quelles sont les valeurs et les structures qui passent entre ces deux domaines. Considérez-la plutôt comme un outil permettant de choisir votre moyen pour s'interfacer.

Lorsqu'on conçoit une interface entre WebAssembly et JavaScript, nous voulons optimiser les propriétés suivantes :

  1. Réduire au maximum les copies de données sur et à partir de la mémoire linéaire de WebAssembly. Les copies inutiles provoquent des surcharges inutiles.
  1. Minimiser les sérialisations et les déserialisations. Pour la même raison que pour les copies, les sérialisations et la déserialisations provoquent des surcharges, et impose parfois aussi des copies, en plus. Si nous pouvons utiliser des manipulateurs opaques pour une structure de données, plutôt que d'avoir à la sérialiser d'un côté, de la copier dans un endroit connu dans la mémoire linéaire de WebAssembly, et la déserialiser de l'autre côté, alors très souvent on économise beaucoup de ressources. wasm_bindgen nous aide à définir et travailler avec des manipulateurs opaques d'objets JavaScript ou de structures Rust intégrées dans des Box.

En règle générale, une bonne conception d'interface JavaScript↔WebAssembly nécessite souvent que les grosses structures de données à durée de vie longue soient implémentées comme étant des types Rust qui vivent dans la mémoire linéaire de WebAssembly, et soient utilisées en JavaScript via des manipulateurs opaques. Le JavaScript appelle les fonctions WebAssembly exportées qui prennent en argument ces manipulateurs opaques, transforment leurs données, procèdent à des calculs lourds, consultent les données, et retournent finalement un petit résultat copiable. En retournant uniquement un petit résultat de l'opération, nous évitons de copier et/ou de tout sérialiser tout ce qui transite entre le tas géré par le ramasse-miettes de JavaScript et la mémoire linéaire de WebAssembly.

Interfacer Rust et JavaScript dans notre jeu de la vie

Commençons par évoquer les pièges à éviter. Nous ne devons pas copier tout l'univers à l'intérieur et à partir de la mémoire linéaire de WebAssembly à chaque tick. Nous ne devons pas allouer des objets pour chaque cellule dans l'univers, ni faire des appels transversaux entre les deux domaines pour lire et écrire chaque cellule.

Qu'est-ce que tout cela implique ? Que nous pouvons représenter l'univers comme un tableau à une dimension qui vit dans la mémoire linéaire de WebAssembly, et qui a un octet pour chaque cellule. 0 modélisera une cellule morte, et 1 sera une cellule vivante.

Voici à quoi ressemble un univers de 4 par 4 dans la mémoire :

Capture d'écran d'un univers 4 par 4

Pour trouver l'indice d'une cellule dans le tableau à partir d'une ligne et d'une colonne, nous pouvons utiliser cette formule :

indice(ligne, colonne, univers) = ligne * largeur(univers) + colonne

Nous pouvons exposer les cellules de l'univers au JavaScript de différentes manières. Pour commencer, nous allons implémenter std::fmt::Display sur Univers, qui nous permettra de générer une String en Rust des cellules qui représentera les cellules avec des caractères. Cette chaîne de caractères Rust est ensuite copiée à partir de la mémoire linéaire de WebAssembly dans une chaîne de caractères en JavaScript, stockée dans le tas géré par le ramasse-miettes de JavaScript, et est ensuite affichée dans l'élément HTML contenuTextuel. Plus tard dans ce chapitre, nous allons faire évoluer cette implémentation pour éviter de copier les cellules de l'univers entre les tas et les intégrer dans un <canvas>.

Une autre conception alternative acceptable serait que Rust retourne une liste de toutes les cellules qui changent d'état après chaque tick, au lieu de donner l'intégralité de l'univers au JavaScript. Ainsi, JavaScript n'aurait pas besoin d'itérer sur tout l'univers lorsqu'il s'occupe du rendu, mais uniquement sur le sous-ensemble concerné. Le désavantage est que cette conception basée sur les différences et un peu plus difficile à implémenter.

Implémentation de Rust

Dans le dernier chapitre, nous avons cloné un modèle initial de projet. Nous allons maintenant modifier ce projet.

Commençons par enlever l'import de alert et la fonction saluer dans wasm-jeu-de-la-vie/src/lib.rs, et remplacons-les par une définition d'un type pour les cellules :


#![allow(unused)]
fn main() {
#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cellule {
    Morte = 0,
    Vivante = 1,
}
}

Il est important d'avoir #[repr(u8)] pour que chaque cellule soit représentée par un seul octet. Il est aussi important que la variante Morte soit 0 et que la variante Vivante vaut 1, afin que nous puissions facilement compter les voisines vivantes d'une cellule en les additionnant.

Ensuite, définissons l'univers. L'univers a une largeur et une hauteur, et a un vecteur de cellules qui a une taille de largeur * hauteur.


#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub struct Univers {
    largeur: u32,
    hauteur: u32,
    cellules: Vec<Cellule>,
}
}

Pour accéder à la cellule à une ligne et colonne donnée, nous calculons l'emplacement dans le vecteur de cellules avec la ligne et la colonne comme nous l'avons décrit précédemment :


#![allow(unused)]
fn main() {
impl Univers {
    fn calculer_indice(&self, ligne: u32, colonne: u32) -> usize {
        (ligne * self.largeur + colonne) as usize
    }

    // ...
}
}

Pour calculer le prochain état d'une cellule, nous devons compter combien de cellules sont vivantes dans son voisinage. Ecrivons donc une méthode compter_voisines_vivantes pour cela !


#![allow(unused)]
fn main() {
impl Universe {
    // ...

    fn compter_voisines_vivantes(&self, ligne: u32, colonne: u32) -> u8 {
        let mut compteur = 0;
        for delta_ligne in [self.hauteur - 1, 0, 1].iter().cloned() {
            for delta_colonne in [self.largeur - 1, 0, 1].iter().cloned() {
                if delta_ligne == 0 && delta_colonne == 0 {
                    continue;
                }

                let ligne_voisine = (ligne + delta_ligne) % self.hauteur;
                let colonne_voisine = (colonne + delta_colonne) % self.largeur;
                let indice = self.calculer_indice(ligne_voisine, colonne_voisine);
                compteur += self.cellules[indice] as u8;
            }
        }
        compteur
    }
}
}

La méthode compter_voisines_vivantes utilise les deltas et les modulos pour éviter de traiter les cas particuliers des bords de l'univers avec le if. Lorsqu'on applique un delta de -1, nous ajoutons self.hauteur - 1 et nous laissons le modulo faire son travail, plutôt que d'essayer d'enlever 1. ligne ou colonne peut valoir 0, et si nous essayons de leur soustraire 1, nous serons alors en dehors des valeurs acceptées par les entiers non-signés.

Maintenant, nous avons tout ce dont nous avons besoin pour calculer la prochaine génération ! Chaque règle du jeu suit des transformations simples suivant des conditions qui peuvent tenir dans une expression match. De plus, comme nous souhaitons que le JavaScript contrôle lorsque les ticks se produisent, nous allons intégrer cette méthode dans un bloc #[wasm_bindgen], pour qu'il soit exposé au JavaScript.


#![allow(unused)]
fn main() {
/// Méthodes publiques, exportées en JavaScript.
#[wasm_bindgen]
impl Univers {
    pub fn tick(&mut self) {
        let mut generation_suivante = self.cellules.clone();

        for ligne in 0..self.hauteur {
            for colonne in 0..self.largeur {
                let indice = self.calculer_indice(ligne, colonne);
                let cellule = self.cellules[indice];
                let voisines_vivantes = self.compter_voisines_vivantes(ligne, colonne);

                let prochain_etat = match (cellule, voisines_vivantes) {
                    // Règle 1 : toute cellule vivante avec moins de deux
                    // voisines vivantes meurt, comme si cela était un effet de
                    //  sous-population.
                    (Cellule::Vivante, x) if x < 2 => Cellule::Morte,
                    // Règle 2 : toute cellule vivante avec deux ou trois
                    // voisines vivantes survit jusqu'à la prochaine génération.
                    (Cellule::Vivante, 2) | (Cellule::Vivante, 3) => Cellule::Vivante,
                    // Règle 3 : toute cellule vivante avec plus de trois
                    // voisines vivantes meurt, comme si cela était un effet de
                    // surpopulation.
                    (Cellule::Vivante, x) if x > 3 => Cellule::Morte,
                    // Règle 4 : toute cellule morte avec exactement trois
                    // voisines vivantes devient une cellule vivante, comme si
                    // cela était un effet de reproduction.
                    (Cellule::Morte, 3) => Cellule::Vivante,
                    // Les cellules qui ne répondent à aucune de ces conditions
                    // restent dans le même état.
                    (statut, _) => statut,
                };

                generation_suivante[idx] = prochain_etat;
            }
        }

        self.cellules = generation_suivante;
    }

    // ...
}
}

Pour l'instant, l'état de l'univers est modélisé par un vecteur de cellules. Pour rendre cela lisible pour un humain, implémentons un rendu textuel basique. L'idée est d'écrire l'univers ligne par ligne textuellement, ainsi nous allons écrire le caractère Unicode (le "carré moyen noir") pour chaque cellule vivante. Et pour les cellules mortes, nous allons écrire (le "carré moyen blanc").

En implémentant le trait Display de la bibliothèque standard de Rust, nous pouvons ajouter un moyen de formater la structure de manière à ce qu'elle soit adaptée pour l'utilisateur. Cela va aussi nous fournir automatiquement une méthode to_string.


#![allow(unused)]
fn main() {
use std::fmt;

impl fmt::Display for Univers {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for ligne in self.cellules.as_slice().chunks(self.largeur as usize) {
            for &cellules in ligne {
                let symbole = if cellule == Cellule::Morte { '◻' } else { '◼' };
                write!(f, "{}", symbole)?;
            }
            write!(f, "\n")?;
        }

        Ok(())
    }
}
}

Enfin, nous définissons un constructeur qui initialise l'univers avec un schéma intéressant avec des cellules vivantes et mortes, ainsi qu'une méthode rendu :


#![allow(unused)]
fn main() {
/// Méthodes publiques, exportées en JavaScript.
#[wasm_bindgen]
impl Univers {
    // ...

    pub fn new() -> Univers {
        let largeur = 64;
        let hauteur = 64;

        let cellules = (0..largeur * hauteur)
            .map(|i| {
                if i % 2 == 0 || i % 7 == 0 {
                    Cellule::Vivante
                } else {
                    Cellule::Morte
                }
            })
            .collect();

        Univers {
            largeur,
            hauteur,
            cellules,
        }
    }

    pub fn rendu(&self) -> String {
        self.to_string()
    }
}
}

Avec tout cela, la partie Rust de notre jeu de la vie est complète !

Recompilez-la en WebAssembly en lançant wasm-pack build dans le dossier wasm-jeu-de-la-vie.

Le rendu avec JavaScript

Pour commencer, ajoutons une balise <pre> à wasm-jeu-de-la-vie/www/index.html, dans lequel afficher l'univers, juste avant la balise <script> :

<body>
  <pre id="canvas-jeu-de-la-vie"></pre>
  <script src="./bootstrap.js"></script>
</body>

De plus, nous voulons que le <pre> soit centré au milieu de la page Web. Nous pouvons utiliser les boites flex pour faire cela. Ajoutez la balise <style> suivante dans le <head> de wasm-jeu-de-la-vie/www/index.html :

<style>
  body {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }
</style>

En haut de wasm-jeu-de-la-vie/www/index.js, corrigeons notre import pour importer le Univers plutôt que la vieille fonction saluer :

import { Univers } from "wasm-jeu-de-la-vie";

Ensuite, obtenez la balise <pre> que nous venons juste d'ajouter et instancier un nouvel univers :

const pre = document.getElementById("canvas-jeu-de-la-vie");
const univers = Univers.new();

Le JavaScript exécute dans une boucle requestAnimationFrame. A chaque itération, il écrit l'univers courant dans le <pre>, et fait ensuite appel à Univers::tick.

const boucleDeRendu = () => {
  pre.textContent = univers.rendu();
  univers.tick();

  requestAnimationFrame(boucleDeRendu);
};

Pour initier le processus de rendu, tout ce que nous avons à faire est de faire le premier appel à la première itération de la boucle de rendu :

requestAnimationFrame(boucleDeRendu);

Assurez-vous que votre serveur de développement continue de s'exécuter (lancez npm run start dans wasm-jeu-de-la-vie/www) et voici ce à quoi http://localhost:8080/ devrait ressembler :

Capture d'écran de l'implémentation du jeu de la vie avec le rendu textuel

Afficher dans un canvas directement à partir de la mémoire

Générer (et allouer) un String en Rust et le convertir en String JavaScript valide par wasm-bindgen génère des copies inutiles des cellules de l'univers. Comme le code JavaScript connait déjà la largeur et la hauteur de l'univers, et peux lire la mémoire linéaire de WebAssembly qui contient les cellules, nous allons modifier la méthode rendu pour retourner un pointeur vers le début du tableau des cellules.

De plus, au lieu d'afficher du texte Unicode, nous allons utiliser l'API de canvas. Nous utiliserons alors cette conception dans la suite du tutoriel.

Dans wasm-jeu-de-la-vie/www/index.html, remplaçons le <pre> que nous avons ajouté précédemment par un <canvas> dans lequel nous allons faire notre rendu (il devrait toujours se trouver dans la <body>, avant le <script> qui charge notre JavaScript) :

<body>
  <canvas id="canvas-jeu-de-la-vie"></canvas>
  <script src='./bootstrap.js'></script>
</body>

Pour obtenir les informations de l'implémentation Rust nécessaires, nous avons besoin d'ajouter plus d'accesseurs pour obtenir la largeur, la hauteur de l'univers, et le pointeur à son tableau de cellules. Ils seront eux aussi exposés au JavaScript. Faites ces ajouts à wasm-jeu-de-la-vie/src/lib.rs :


#![allow(unused)]
fn main() {
/// Méthodes publiques, exportées en JavaScript.
#[wasm_bindgen]
impl Univers {
    // ...

    pub fn largeur(&self) -> u32 {
        self.largeur
    }

    pub fn hauteur(&self) -> u32 {
        self.hauteur
    }

    pub fn cellules(&self) -> *const Cellule {
        self.cellules.as_ptr()
    }
}
}

Ensuite, dans wasm-jeu-de-la-vie/www/index.js, ajoutons aussi l'import de Cellule de wasm-jeu-de-la-vie, et définissons quelques constantes que nous utiliserons lorsque nous ferons le rendu dans le canvas :

import { Univers, Cellule } from "wasm-jeu-de-la-vie";

const TAILLE_CELLULE = 5; // px
const COULEUR_GRILLE = "#CCCCCC";
const COULEUR_MORTE = "#FFFFFF";
const COULEUR_VIVANTE = "#000000";

Maintenant, ré-écrivons le reste du code JavaScript pour ne plus avoir à écrire avec textContent dans le <pre> mais dessiner dans <canvas> à la place :

// Construit l'univers, et obtient sa largeur et son hauteur
const univers = Univers.new();
const largeur = univers.largeur();
const hauteur = univers.hauteur();

// Applique une taille au canvas pour accueillir toutes nos cellules et une
// bordure de 1px autour d'elles.

const canvas = document.getElementById("canvas-jeu-de-la-vie");
canvas.height = (TAILLE_CELLULE + 1) * largeur + 1;
canvas.width = (TAILLE_CELLULE + 1) * hauteur + 1;

const ctx = canvas.getContext('2d');

const boucleDeRendu = () => {
  univers.tick();

  dessinerGrille();
  dessinerCellules();

  requestAnimationFrame(boucleDeRendu);
};

Pour dessiner la grille entre les cellules, nous dessinons un jeu de lignes espacées régulièrement horizontalement, et un jeu de lignes espacées régulièrement verticalement. Ces lignes s'entrecroisent pour former la grille.

const dessinerGrille = () => {
  ctx.beginPath();
  ctx.strokeStyle = COULEUR_GRILLE;

  // Lignes verticales.
  for (let i = 0; i <= largeur; i++) {
    ctx.moveTo(i * (TAILLE_CELLULE + 1) + 1, 0);
    ctx.lineTo(i * (TAILLE_CELLULE + 1) + 1, (TAILLE_CELLULE + 1) * hauteur + 1);
  }

  // Lignes horizontales.
  for (let j = 0; j <= hauteur; j++) {
    ctx.moveTo(0,                                  j * (TAILLE_CELLULE + 1) + 1);
    ctx.lineTo((TAILLE_CELLULE + 1) * largeur + 1, j * (TAILLE_CELLULE + 1) + 1);
  }

  ctx.stroke();
};

Nous pouvons accéder directement à la mémoire linéaire de WebAssembly via memory, qui est défini dans le module brut wasm_jeu_de_la_vie_bg. Pour dessiner les cellules, nous obtenons le pointeur vers les cellules de l'univers, construisons un Uint8Array qui sert de surcouche tampon pour les cellules, itère sur chaque cellule, et dessine un rectangle blanc ou noir, respectivement si la cellule est morte ou vivante. En travaillant avec des pointeurs et des surcouches, nous évitons de copier les cellules entre les deux domaines à chaque tick.

// Importe la mémoire de WebAssembly au début du fichier.
import { memory } from "wasm-jeu-de-la-vie/wasm_jeu_de_la_vie_bg";

// ...

const calculerIndice = (ligne, colonne) => {
  return ligne * largeur + colonne;
};

const dessinerCellules = () => {
  const pointeurCellules = univers.cellules();
  const cellules = new Uint8Array(memory.buffer, pointeurCellules, largeur * hauteur);

  ctx.beginPath();

  for (let ligne = 0; ligne < hauteur; ligne++) {
    for (let colonne = 0; colonne < largeur; colonne++) {
      const indice = calculerIndice(ligne, colonne);

      ctx.fillStyle = cellules[indice] === Cellule.Morte
        ? COULEUR_MORTE
        : COULEUR_VIVANTE;

      ctx.fillRect(
        colonne * (TAILLE_CELLULE + 1) + 1,
        ligne * (TAILLE_CELLULE + 1) + 1,
        TAILLE_CELLULE,
        TAILLE_CELLULE
      );
    }
  }

  ctx.stroke();
};

Pour démarrer le processus de rendu, nous allons utiliser le même code que ci-dessus pour démarrer la première itération de la boucle de rendu :

dessinerGrille();
dessinerCellules();
requestAnimationFrame(boucleDeRendu);

Notez que nous faisons appel à dessinerGrille() et à dessinerCellules() ici avant de faire appel à requestAnimationFrame(). La raison à cela est que l'état initial de l'univers est dessiné avant que nous procédions à nos modifications. Si nous avions simplement appelé requestAnimationFrame(boucleDeRendu) à la place, nous nous serions retrouvé dans une situation dans laquelle la première séquence serait dessinée après le premier appel à univers.tick(), qui est le second "tick" dans la vie de ces cellules.

Cela fonctionne !

Recompilez le WebAssembly et la glue de liaison en lançant cette commande dans le dossier racine wasm-jeu-de-la-vie :

wasm-pack build

Assurez-vous que votre serveur de développement fonctionne toujours. Si ce n'est plus le cas, relancez-le dans le dossier wasm-jeu-de-la-vie/www :

npm run start

Si vous rafraîchissez http://localhost:8080/, vous devriez être accueilli par une simulation de la vie captivante !

Capture d'écran de l'implémentation du jeu de la vie

Ceci dit, il existe aussi un algorithme très intéressant pour implémenter le jeu de la vie qui s'appelle hashlive. Il utilise une méthode de gestion de la mémoire poussée et peut devenir exponentiellement plus rapide pour calculer les prochaines générations au fur et à mesure qu'il s'exécute ! Sachant cela, vous vous demandez peut-être pourquoi nous n'avons pas implémenté hashlife dans ce tutoriel. Ce n'est pas le but de ce document, car nous nous concentrons sur l'intégration de Rust en WebAssembly, mais nous vous encourageons vivement d'en apprendre plus sur hashlife par vous-même !

Exercices

  • Initialiser l'univers avec un simple vaisseau spatial.
  • Au lieu de coder en dur l'univers initial, générez-en un aléatoire, dans lequel chaque cellule a cinquante pour cent de chance d'être vivante ou morte.

    Astuce : utilisez la crate js-sys pour importer la fonction JavaScript Math.random.

    Réponse

    Premièrement, ajoutez js-sys comme dépendance dans wasm-jeu-de-la-vie/Cargo.toml :

    # ...
    [dependencies]
    js-sys = "0.3"
    # ...
    

    Ensuite, utilisez la fonction js_sys::Math::random pour générer un nombre aléatoire :

    
    #![allow(unused)]
    fn main() {
    extern crate js_sys; // (facultatif à partir de Rust 2018)
    
    // ...
    
    if js_sys::Math::random() < 0.5 {
        // Vivante ...
    } else {
        // Morte ...
    }
    }
    
  • Représenter chaque cellule avec un octet facilite l'itération sur les cellules, mais cela gaspille de la mémoire. Chaque octet a huit bits, mais nous n'avons besoin d'un seul bit pour représenter si chaque cellule est vivante ou morte. Remaniez la représentation des données pour que chaque cellule utilise uniquement un seul bit en mémoire.

    Réponse

    En Rust, vous pouvez utiliser la crate fixedbitset et son type FixedBitSet pour représenter les cellules au lieu d'utiliser Vec<Cell> :

    
    #![allow(unused)]
    fn main() {
    // Assurez-vous d'avoir aussi ajouté la dépendance dans Cargo.toml !
    extern crate fixedbitset; // (facultatif en Rust 2018)
    use fixedbitset::FixedBitSet;
    
    // ...
    
    #[wasm_bindgen]
    pub struct Univers {
        largeur: u32,
        hauteur: u32,
        cellules: FixedBitSet,
    }
    }
    

    Le constructeur de l'Univers peut être corrigé comme ceci :

    
    #![allow(unused)]
    fn main() {
    pub fn new() -> Univers {
        let largeur = 64;
        let hauteur = 64;
    
        let taille = (largeur * hauteur) as usize;
        let mut cellules = FixedBitSet::with_capacity(taille);
    
        for i in 0..taille {
            cellules.set(i, i % 2 == 0 || i % 7 == 0);
        }
    
        Univers {
            largeur,
            hauteur,
            cellules,
        }
    }
    }
    

    Pour modifier une cellule à la prochaine tick de l'univers, nous utilisons la méthode set de FixedBitSet :

    
    #![allow(unused)]
    fn main() {
    generation_suivante.set(indice, match (cellule, voisines_vivantes) {
        (true, x) if x < 2 => false,
        (true, 2) | (true, 3) => true,
        (true, x) if x > 3 => false,
        (false, 3) => true,
        (statut, _) => statut
    });
    }
    

    Pour passer un pointeur vers le départ des bits en JavaScript, vous pouvez convertir le FixedBitSet en une slice et ensuite convertir la slice en pointeur :

    
    #![allow(unused)]
    fn main() {
    #[wasm_bindgen]
    impl Univers {
        // ...
    
        pub fn cellules(&self) -> *const u32 {
            self.cellules.as_slice().as_ptr()
        }
    }
    }
    

    En JavaScript, la construction d'un Uint8Array à partir de la mémoire de WebAssembly est la même que précédemment, excepté que la longueur du tableau n'est plus largeur * hauteur, mais largeur * hauteur / 8 puisque nous avons un bit par cellule au lieu d'un octet :

    const cellules = new Uint8Array(memory.buffer, pointeurCellules, largeur * hauteur / 8);
    

    Pour un indice et un Uint8Array donné, vous pouvez obtenir le nième bit avec la fonction suivante :

    const bitVautTrue = (n, tableau) => {
      const octet = Math.floor(n / 8);
      const masque = 1 << (n % 8);
      return (tableau[octet] & masque) === masque;
    };
    

    En ayant tout cela, la nouvelle version de dessinerCellules ressemble à ceci :

    const dessinerCellules = () => {
      const pointeurCellules = univers.cellules();
    
      // On a modifié cela !
      const cellules = new Uint8Array(memory.buffer, pointeurCellules, largeur * hauteur / 8);
    
      ctx.beginPath();
    
      for (let ligne = 0; ligne < hauteur; ligne++) {
        for (let colonne = 0; colonne < largeur; colonne++) {
          const indice = calculerIndice(ligne, colonne);
    
          // On a modifié cela !
          ctx.fillStyle = bitVautTrue(indice, cellules)
            ? COULEUR_VIVANTE
            : COULEUR_MORTE;
    
          ctx.fillRect(
            colonne * (TAILLE_CELLULE + 1) + 1,
            ligne * (TAILLE_CELLULE + 1) + 1,
            TAILLE_CELLULE,
            TAILLE_CELLULE
          );
        }
      }
    
      ctx.stroke();
    };