On a récemment réalisé que, lorsqu'il s'agit d'afficher quelque chose sur un écran raster, la technique dite de "double buffering" n'est pas forcement une évidence pour tout le monde. On a donc décidé de vous proposer un petit article qui explique le principe et montre à quel point il est facile d'implémenter cette technique avec des écrans Yoctopuce.
Le problème avec les écrans raster
Les écrans raster sont ceux qui sont basés sur une grille de pixels, ce sont les plus communs. Lorsque que vous devez afficher une information changeante, texte ou un dessin, sur un de ces écrans, vous opérez presque toujours de la même façon.
- Vous effacez l'ancien contenu de l'écran
- Vous redessinez le nouveau contenu de l'écran
- Au besoin vous faites une petite pause
- Vous recommencez
Tant que le temps nécessaire pour construire le contenu de l'écran est très inférieur au temps que met l'électronique pour rafraîchir l'écran physique ça ne passe généralement pas trop mal. Cependant, plus la construction du contenu de l'affichage devient complexe et longue, plus on court de risque de voir l'écran se rafraîchir avant qu'on ait fini d'en construire le contenu. Le résultat est généralement une image plus ou moins incomplète qui scintille à l'écran.
Voici un petit exemple écrit en Python qui affiche une étoile qui tourne sur un écran Yoctopuce.
from yocto_api import *
from yocto_display import *
# setup the Yoctopuce API
errmsg = YRefParam()
if YAPI.RegisterHub("usb", errmsg) != YAPI.SUCCESS:
sys.exit("init error" + errmsg.value)
# Finds the Display
disp = YDisplay.FirstDisplay()
if disp is None: sys.exit("No display found")
# display clean up
disp.resetAll()
# retreive the display size
w = disp.get_displayWidth()
h = disp.get_displayHeight()
# retreive the first layer
l1 = disp.get_displayLayer(1)
l1.selectFont("Medium.yfm")
# some constants
spikeCount = 5
delta = 2*math.pi / spikeCount
radius = (h/2) -5
angle = 0
while True:
l1.clear()
l1.drawText(w,0,YDisplayLayer.ALIGN.TOP_RIGHT,"%.1f"%(180*angle/math.pi)+"°")
l1.moveTo( w/2 + radius*math.cos(angle), h/2 + radius*math.sin(angle))
for i in range( 1,spikeCount+1):
a=angle+(2*i)*delta
l1.lineTo( w/2 + radius*math.cos(a), h/2 + radius*math.sin(a))
angle =angle+ delta /10
if angle>=2*math.pi : angle= angle-2*math.pi;
YAPI.Sleep(40,errmsg)
Le résultat fait scintiller l'écran de manière assez désagréable, voici une animation basée sur une capture des frames d'un Yocto-MaxiDisplay sur lequel tourne notre petit programme.
Sans double buffering
La solution: le double buffering
Pour éviter ce scintillement, la technique consiste à construire l'image à afficher dans un tampon en mémoire plutôt que directement dans la mémoire de l'écran. Et une fois que l'image est prête on transfère simplement le contenu du tampon de construction dans la mémoire de l'écran en une fois, d'où le terme "double buffering" en anglais. Cette copie est une opération très rapide: il s'agit juste de recopier une zone mémoire d'un tampon à l'autre,
Normalement, cette technique de double buffering suppose de disposer de primitives capable de dessiner dans une zone mémoire arbitraire plutôt que directement dans la mémoire vidéo, mais les écrans Yoctopuce sont un peu particuliers.
L'API des écrans Yoctopuce
Lorsque vous appelez des primitives de dessins avec l'API Yoctopuce, vous n'écrivez pas directement dans la mémoire vidéo mais dans un tampon qui représente une couche (layer) virtuelle. Et l'électronique de l'écran calcule en permanence une superposition de ces couches et affiche le résultat sur l'écran proprement dit, un peu à la manière des calques dans les logiciel de dessins modernes. Il se trouve que
- Chacune de ces couches peut être rendue invisible
- On peut parfaitement dessiner dans une couche invisible
- On peut interchanger le contenu de deux couches, qu'elles soit visibles ou non
On peut donc faire du double buffering en choisissant deux couches dont une sera invisible. Il suffit alors de dessiner dans la couche invisible, et une fois que c'est terminé, d'interchanger le contenu des deux couches. En pratique, cela ne rajoute que trois lignes à l'exemple précédent:
# retreive the first layer
l1 = disp.get_displayLayer(1)
l1.selectFont("Medium.yfm")
l1.hide() #DOUBLE BUFFERING EXTRA LINE 1
l2 = disp.get_displayLayer(2) #DOUBLE BUFFERING EXTRA LINE 2
# some constants
spikeCount = 5
delta = 2*math.pi / spikeCount
radius = (h/2) -5
angle = 0
while True:
l1.clear()
l1.drawText(w,0,YDisplayLayer.ALIGN.TOP_RIGHT,"%.1f"%(180*angle/math.pi)+"°")
l1.moveTo( w/2 + radius*math.cos(angle), h/2 + radius*math.sin(angle))
for i in range( 1,spikeCount+1):
a=angle+(2*i)*delta
l1.lineTo( w/2 + radius*math.cos(a), h/2 + radius*math.sin(a))
disp.swapLayerContent(1,2) #DOUBLE BUFFERING EXTRA LINE 3
angle =angle+ delta /10
if angle>=2*math.pi : angle= angle-2*math.pi;
YAPI.Sleep(40,errmsg)
Et voici le résultat:
Avec double buffering
Conslusion
Le résultat est sans appel, avec les écrans Yoctopuce le double-buffering est juste trop facile à implémenter pour qu'on puisse se permettre de s'en passer. Pour finir, signalons que dans le cas de l'API Yoctopuce, dessiner dans une couche invisible est souvent plus rapide que de dessiner dans une couche visible parce que les commandes envoyées aux couches invisibles sont bufferisées alors que celle envoyées aux couches visibles le sont en temps réel.