Vsync : la bonne façon de mettre à jour l'écran en SDL2 | Blog | Superflu Riteurnz

Vsync : la bonne façon de mettre à jour l'écran en SDL2 | Blog | Superflu Riteurnz Vsync : la bonne façon de mettre à jour l'écran en SDL2 | Blog | Superflu Riteurnz
  • Steam
  • Nintendo Switch
  • Itch.io
  • Google Play
Superflu et son assistante Sophie

Blog

Vsync : la bonne façon de mettre à jour l'écran en SDL2

2023-03-14

Si vous lisez un tuto sur la façon dont on gère l'affichage avec la bibliothèque SDL, il y a de fortes chances que celui-ci résume cela par une boucle : faire les calculs pour la « logique » du jeu (bouger les personnages, etc.), appeler les fonctions d'affichage, rafraîchir l'écran, puis mettre le programme en « sommeil » pour un temps.

Sauf que cette méthode est obsolète et qu'on peut faire beaucoup mieux… voyons ça.

Historiquement, la SDL

La bibliothèque Simple Direct Media Layer (SDL de son petit nom) existe depuis 1998, et sa facilité d'usage couplée à sa licence permissive (zlib) fait qu'elle reste largement utilisée et qu'on trouve pléthore de tutoriaux à son sujet.

Là où le bât blesse parfois, c'est que pas mal de ces tutos concernent la version 1.2 de la bibliothèque, une ancienne version qui est devenue dépréciée depuis la sortie de la 2.0, en août 2013 quand même. La SDL2 cassait pas mal de choses au niveau de l'API pour pouvoir apporter des améliorations substantielles, la principale et non des moindres étant la gestion de l'accélération graphique 2D au niveau matériel.

Afin de garder une certaine rétrocompatibilité, pas mal de fonction de la SDL1 ont été conservées. Mais il y a un certain nombre de mécanismes que vous ne devriez plus utiliser pour autant.

L'idée générale

Comme je le disais en intro, lorsque vous développez un jeu en SDL, votre programme se résume en général à une grosse boucle :

  • on gère la « logique » du jeu (calculs de collisions, chargement des niveaux, déplacements des images, etc.) ;
  • on appelle les fonctions d'affichage de la SDL : avec la SDL1, on appelait SDL_BlitSurface() sur des SDL_Surface ; en SDL2, on va plutôt utiliser SDL_RenderCopy() sur des SDL_Texture (voir le guide de migration de la 1.2 à la 2.0) ;
  • on « met à jour l'écran », c'est-à-dire qu'on indique à la SDL qu'on a fini de dessiner ce qu'on voulait, et qu'elle peut à présent afficher le contenu de l'écran ;
  • on met le jeu en « sommeil » pendant un temps, pour plusieurs raisons : d'abord, parce qu'il est inutile de rafraîchir l'écran des centaines de fois par seconde (les moniteurs sont en général calibrés sur 60 Hz, donc 60 rafraîchissements par seconde suffisent) ; d'autre part, parce que ça permet d'économiser du temps de processeur (et donc notamment de la batterie sur appareil mobile).

Sur ce dernier point, beaucoup de tutos indiquent encore d'utiliser SDL_Delay(), en calculant combien de temps le programme doit « dormir » pour que le prochain rafraîchissement d'écran tombe au bon moment pour respecter la fréquence souhaitée (60 Hz par exemple).

Sauf que cette méthode, si elle fonctionne plutôt bien au premier abord, pose un tout petit souci…

Le problème avec SDL Delay

Le gros problème, c'est que nos 60 Hz calculés au niveau du CPU (processeur) ne sont pas synchronisés avec les 60 Hz du GPU (carte graphique). Résultat ? Au moment où je demande au GPU d'afficher mon image, il peut être en plein milieu d'un rendu et donc ne mettre à jour qu'une partie de l'écran… Ce qui donne un effet assez moche sur le rendu du jeu.

On appelle ça du tearing (« déchirement » en anglais) : on dirait que toute l'image ne se rafraîchit pas au même moment et que certaines sections sont « en retard » sur les autres.

En pratique, ça donne quelque chose comme ça :

La solution : vsync

Bien sûr, comme SDL2 a été conçue autour d'un meilleur support de l'accélération matériel, elle nous donne un outil bien pratique pour résoudre ce problème : la synchronisation verticale, ou vsync de son petit nom.

En pratique, ça veut dire que les choses vont être simplifiées. Voici à quoi votre boucle va ressembler :

  • gérer la logique du jeu ;
  • appeler des fonctions d'affichage de la SDL ;
  • mettre à jour l'écran ;

… c'est tout ! Eh oui, en réalité, en utilisant la vsync, plus besoin de gérer ce fameux temps de « sommeil » : c'est la SDL qui va directement s'en charger, en s'arrêtant exactement le temps qu'il faut pour… être synchronisée sur la fréquence de rafraîchissement de l'écran.

Pour mettre cela en place, c'est tout simple, il suffit de passer un flag supplémentaire (SDL_RENDERER_PRESENTVSYNC) au renderer :


SDL_Windows* window = /* create window (...) */;
SDL_Renderer* renderer = SDL_CreateRenderer (window, -1, 
                                             SDL_RENDERER_ACCELERATED
                                             | SDL_RENDERER_PRESENTVSYNC);

Cette méthode présente plusieurs avantages :

  • bon déjà, bien sûr, ça règle le souci graphique montré ci-dessus, ce qui est quand même le principal ;
  • ça règle directement le rafraîchissement du jeu sur l'écran, vous n'avez plus à vous soucier de la fréquence : si votre écran se rafraîchit à 50 Hz, alors votre jeu tournera à 50 Hz, etc. ;
  • si jamais votre boucle de jeu est trop lente et que vous « manquez » un rafraîchissement, pas grave, la SDL attendra tout simplement le rafraîchissement suivant pour vous ;
  • SDL_Delay() faisait, en interne, un appel système, ce qui est notoirement lent : c'est toujours ça de moins à faire.

Un petit point d'interrogation quand même

Certains renderer peuvent potentiellement ne pas supporter la vsync… et je n'ai pas l'impression qu'il y ait un moyen de le savoir à l'exécution : le problème, c'est que dans ce cas, il n'y aura pas de pause et le programme va tourner en bouffant 100 % du CPU, ce qui est moyen.

Je ne sais pas trop quoi faire de cela : sur toutes les plateformes que j'ai testées, la vsync était supportée, ce qui semble a priori encourageant. Pour mes propres tests, je lance parfois le jeu « sans interface graphique », pour ne tester que la partie logique, via ce petit hack :


#ifdef SOSAGE_GUILESS
  putenv("SDL_VIDEODRIVER=dummy");
#endif

Dans ce cas, bien sûr, je n'appelle aucune fonction d'affichage ni de rendu, et je remplace donc la fonction de rendu (bloquante au niveau GPU) par le fameux SDL_Delay() dont je m'étais débarrassé pour le reste :


#ifndef SOSAGE_GUILESS
  SDL_RenderPresent (renderer);
#else
  SDL_Delay(17); // If no GUI, simulate GPU delay
#endif

Idéalement, j'aimerais bien pouvoir faire ça en détectant à l'exécution si la vsync est disponible, mais ça n'a pas l'air possible pour l'instant. Reste la possibilité de mettre un court SDL_Delay() juste pour s'assurer qu'il y ait toujours une pause, mais ça me semble moins propre.

Peut-être dans la SDL3 ? :)

i