Tutoriel Emscripten et SDL2

Dans ce tutoriel, nous allons voir comment compiler une application  SDL2  minimaliste avec emscripten et LLVM, afin de la faire tourner de façon native dans un navigateur web. En effet, le but d’emscripten  est de produire du code JavaScript, à partir de code C ou C++. Cliquez sur l’image pour voir tourner le résultat.

Application

  • Choix d’utiliser SDL2 et les primitives graphiques 2D simples: Pas d’OpenGL, qui est pourtant supporté, de façon à garder un code court, et pour ne pas rentrer dans les détails des différentes saveurs d’OpenGL. L’objet de ce tutoriel est de montrer l’utilisation d’emscripten, pas d’apprendre l’OpenGL :)
  • L’utilisation de SDL2 au lieu de SDL: Bien que le support de SDL2 soit plus récent que celui de SDL, j’ai pu constater de bien meilleures performances avec SDL2.
  • Ce tutoriel a été réalisé sous GNU/Linux (Mint Linux 17.2 en l’occurrence). Il reste valable dans ses grandes lignes avec  Windows ou Mac OS, mais la procédure d’installation d’emscripten diffère d’une plate-forme à l’autre: pour plus d’informations, se référer à la documentation en ligne. Windows est de loin l’OS avec laquelle la procédure d’installation/utilisation est la plus complexe. Le recours à une machine virtuelle est probablement la façon la plus simple de faire tourner emscripten “sous Windows”.

Source du Programme

Commençons par jeter un œil au code source de l’application. Apres quelques inclusions et définition de variables globales (lignes 1-12), nous avons une fonction de rendu graphique render (lignes 14-32), la boucle principale (mainloop_func, lignes 34-53) et le point d’entrée du programme (55-80).

#include <assert.h>
#include <SDL2/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

SDL_Window * window;
SDL_Renderer * renderer;

int done = 0; // Main loop flag (in desktop mode)
int frameCount = 0;

void render()
{
    int w,h;
    SDL_GetWindowSize(window, &w,&h);

    // Clear background
    SDL_SetRenderDrawColor(renderer, 10, 25, 50, 255);
    SDL_RenderClear(renderer);

    // Draw some lines
    frameCount +=1;
    for (int r=frameCount %20; r<w; r+=20)
    {
        SDL_SetRenderDrawColor(renderer, 64-r/20, 64-r/10, 255-r/3, 255);
        SDL_RenderDrawLine(renderer, 0, 0, r, h);
        SDL_RenderDrawLine(renderer, w, h, w-r,0);
    }
    SDL_RenderPresent(renderer);
}

void mainloop_func(void)
{
    // Draw our Scene
    render();
    // Check for SDL Events
    SDL_Event event;
    while( SDL_PollEvent( &event ) )
    {
        switch(event.type)
        {
        case SDL_QUIT:
            done = 1;
            break;
        default:
            printf("[%u] Event Type=%d\n", SDL_GetTicks(), event.type);  //to stdout
            //SDL_Log("[%u] Event Type=%d", SDL_GetTicks(), event.type); //to JS console
            break;
        }
    }
}

int main(int argc, char* argv[])
{
    // Initialize SDL Video subsystem and create a 640x480 window
    assert(SDL_Init(SDL_INIT_VIDEO) == 0);
    SDL_CreateWindowAndRenderer(640, 480, 0, &window, &renderer);
#ifdef EMSCRIPTEN
    // Setup periodic call of our mainloop. Framerate is controlled by the web browser.
    emscripten_set_main_loop(mainloop_func, 0, 0);
    // Exit main() but keep web application running
    emscripten_exit_with_live_runtime();
#else
    // In desktop mode, we call continuously our main loop function, and wait 10ms.
    while(done==0)
    {
        mainloop_func();
        SDL_Delay(10);
    }
#endif
    // This is actually never executed with EMSCRIPTEN enabled
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}


Notez l’utilisation du symbole EMSCRIPTEN pour délimiter les portions de code qui different d’une application desktop, d’une applicaiton web générée avec emscripten.

Pour recompiler cette application avec gcc:

gcc main.cpp -o demo `sdl2-config --cflags --libs`

Il faudra bien entendu installer la librairie SDL2, typiquement via le  package libsdl2-dev. Une fois compilé, on obtient un ficher binaire exécutable demo.

Installation d’emscripten

Voyons maintenant comment installer emscripten, afin de recompiler cette application pour le web.

Sous Linux, l’installation d’emscripten -dite portable- se fait sans la nécessité d’être administrateur: elle se fait dans un répertoire au choix dans le home de l’utilisateur. Cette méthode a pour avantage de ne pas interférer avec le système (pas de package installé par exemple). La procédure, est plutôt simple:

1. Quelques packages doivent être installés au préalable: D’une distribution à l’autre, certains packages sont présents dans le système, mais il se peut qu’il en manque. En général, les messages d’erreur donne suffisamment d’information sur le package qui pourrait manquer.  Il faut au minimum installer cmake, nodejs. Sous Debian/Ubuntu/Mint:

sudo apt-get install cmake nodejs

2. Télécharger l’archive emsdk-portable.tar.gz et la décompresser, par exemple dans le home de l’utilisateur, et aller dans ce répertoire:

tar zxf emsdk_portable.tgz
cd emsdk_portable

3. Exécuter les commandes suivantes:

./emsdk update
./emsdk install latest
./emsdk activate latest

La première commande va télécharger quelques outils  essentiels. La seconde télécharge et compile le dernier SDK disponible, ce qui prend un bon moment. La troisième va créer quelques scripts qui dépendent de l’endroit ou vous avez mis votre répertoire emsdk-portable. Si vous déplacez ce répertoire, il faudra exécuter à nouveau cette commande.

A l’issue de l’installation,  les fichiers et répertoires suivants sont créés:

  • ~/..emscripten : fichier de configuration pour l’utilisateur
  • ~/..emscripten_cache/
  • ~/. .emscripten_ports/
  • ~/.emscripten_sanity

J’ai dû modifier le fichier ~/.emscripten de façon à ce que la variable NODE_JS soit définie ainsi:

NODE_JS = 'nodejs'

En effet, en installant le package nodejs installe un binaire nodejs, et non pas node comme semble l’attendre emscripten par défaut. D’autres options peuvent être modifiées selon les besoin, pour le moment nous n’avons rien d’autre a changer.

Enfin, dans le répertoire emsdk-portable, le script emsdk_env permet de configurer les variables d’environnement nécessaires à la compilation avec emscripten. Pour l’utiliser:

source /chemin/vers/emsdk-portable/emsdk_env.sh

Compilation avec emscripten

Une fois installé, et et les variables d’environnement correctement définies, il suffit de compiler notre projet avec em++:

em++ main.cpp -o demo.html -s USE_SDL=2 -O2

Lors de la premiere execution de em++ avec l’option USE_SDL=2, le support SDL2 n’étant pas encore installé, les librairies manquantes seront téléchargées et configurées, dans le répertoire .emscripen_ports. 

Si tout s’est biern passé, 3 fichiers ont été générés

  • demo.html (102Kb)
  • demo.html.mem (~40Kb)
  • demo.js (~1.4Mb)

Il suffit d’ouvrir demo.html avec un navigateur, pour admirer résultat.

Différences avec application Desktop

Voyons maintenant ce qui diffère entre une application pour desktop et une application “emscripten”

  • La boucle principale: c’est le changement majeur a apporter au projet pour qu’il fonctionne dans un navigateur. En effet, c’est ce dernier qui contrôle l’exécution du code,  et il n’est plus possible de garder la main dans une boucle principale faute de quoi, la page web va freezer, et le navigateur proposer l’arrêt du script.
  • Système de fichier. Emscripten possède des mécanismes pour gerer un systeme de fichier virtuel. Il permet en particulier de précharger des fichiers, de facon a les rendre accessible de facon classique (fopen). –preload-file permet par exemple de spécifier le nom d’un fichier qui sera intégré dans le systeme de fichier.
  • La console. La page générée par emscripten contient la fenetre principale, en dessous de laquelle se trouvent une console, qui affiche ce qui est envoyé vers la sortie standard (stdout)
printf("Event Type=%d", event.type);

Pour que le texte soit envoyé vers la console JavaScript, on pourra utliser SDL_Log, qui a une syntaxe identique:

SDL_Log("Event Type=%d", event.type);

Conclusion

Les performance sont réduites par rapport au desktop, et probablement aussi comparé à du code JavaScript dédié, mais elles sont tout de même fort honorables. De plus le choix des APIs graphiques permet d’accéder à de l’accélération matérielle. Si l’on choisit d’écrire du code compatible WebGL par exemple, la perte de performance sera réduite.

  • Support OpenGL (ES et “legacy”), Web Gl: Il existe en effet 3 modes de compatibilité selon les APIs que l’on utilise: Open GL 1 (obsolète, mais largement encore utilisé), Open GL ES2, ou WebGL…
  • Support audio encore limité. Il est possible de jouer un fichier OGG par exemple.

Les librairies supportées sont disponibles sur un git dédié:

https://github.com/emscripten-ports

 

Liens Utiles

 

Leave a Reply

Your email address will not be published. Required fields are marked *

software design & creative process