Fredz's blog

Aller au contenu | Aller au menu | Aller à la recherche

vendredi 18 juin 2010

Genlock, comment ça fonctionne

J'ai parcouru le code et même si je n'ai pas tout compris, ça commence à s'éclairer un peu. Je ne vais parler que de la partie graphique, la gestion du port parallèle ne m'intéresse que dans la mesure où je peux la supprimer, en conservant néanmoins le pilotage des lunettes via ce port.

Programmation par IRQ

Visiblement tout ce qui concerne le page flipping et la synchronisation avec le retour vertical se passe dans la fonction qui est appelée à chaque fois que l'interruption reliée à la carte graphique NVIDIA (IRQ 16 par défaut) est déclenchée.

Il faudra faire en sorte que la valeur de cette interruption soit lue directement et pas définie par défaut, probablement en parsant /proc/driver/nvidia/cards/*. Il faudra voir si on a besoin de prendre en compte plusieurs interruptions s'il y a plusieurs cartes, ce serait quand même bien de gérer le SLI. Ce n'est pas le cas sur l'ancien driver NVIDIA et les drivers iZ3D et Tridef, à voir pour le driver NVIDIA 3D Vision.

Initialisation

Dans un premier temps, cette fonction entre dans un mode d'initialisation dans lequel la durée d'une trame est calculée, en mesurant le temps entre un début/fin de retour vertical et le suivant. La fonction passe ensuite en mode synchronisation.

Synchronisation

En mode synchronisation, il y a une évaluation de la différence entre le temps actuel et le temps évalué précédemment pour le prochain retour vertical. Si cette différence est inférieure à -1000 µs, ce n'est pas un retour vertical et on rend la main de suite. Si cette différence est supérieure à 1000 µs, on a sans doute raté un retour vertical et on l'affiche dans le log. Sinon il y a un code assez compliqué à base de compteurs auquel je n'ai rien compris pour l'instant pour les valeurs entre -1000 et -100 µs et 1000 et 100 µs.

Si on a un retour vertical, raté ou pas, on envoie la séquence de page flipping en programmant le CRTC VGA et on envoie le signal adéquat aux lunettes suivant qu'on est sur un retour vertical pair ou impair. La parité de la valeur du retour n'a de valeur qu'en interne, on ne peut pas s'en servir pour garantir une obturation des lunettes toujours dans le bon ordre après activation du module. Il faudra donc bien inclure un code de pilotage direct via le bit SDA du DDC en s'appuyant sur I2C pour les lunettes VGA DDC.

Pilotage du CRTC

Le pilotage du CRTC semble avoir quelques limites, déjà il faut séparer la détection du retour de la gestion du page flipping.

Page flipping

Pour l'instant il y a du code pour faire du page flipping via les registres standards du CRTC VGA. Les cartes NVIDIA disposant de deux CRTCs pour le TwinView, il faudra trouver les adresses du deuxième CRTC en lisant de la doc ou en étudiant nvclock ou les drivers nv et nouveau.

Ça permettrait de pouvoir au moins choisir lequel des deux écrans utiliser, étant donné que pour l'instant le page flipping s'active toujours sur le même, quelles que soient les options sélectionnées dans nvidia-settings ou la valeur choisie pour la variable head dans le code. Il est possible que les adresses pour le deuxième CRTC (choisi selon la valeur de la variable head) soient erronées dans le code après comparaison avec le code de nv. Mais il va falloir pousser un peu plus loin parce que ce n'est pas programmé du tout pareil.

Il faudra voir également si on peut faire du page flipping sur deux écrans, ce qui semble ne pas être possible en lisant les docs NVIDIA. En effet celles-ci indiquent qu'avec XVideo ou OpenGL on ne peut synchroniser sur le retour vertical que pour un seul écran. Il faudra voir si ça concerne le page flipping, la détection du retour vertical ou les deux.

Ça pourrait concerner le page flipping parce que selon les docs il y aurait un unique framebuffer en TwinView sur lequel les deux CRTCs s'appuient et on ne pourrait donc changer l'adresse que de l'un des deux. Ça me paraît bizarre étant donné que le code actuel ne fait du page flipping que sur un seul écran, l'autre affichant toujours la même chose.

Limite de résolution pour le page flipping

Une seconde limite tient à la résolution supportée pour le page flipping. Pour l'instant, la résolution horizontale est fixée à 1024 dans le code, il faudra essayer avec des modes supérieurs à 1024x768 pour voir si ça fonctionne.

Dans le code de page flipping, il est indiqué que le code supporte du 1024 ou inférieur en offset, donc c'est un peu inquiétant. Il est aussi indiqué qu'auparavant le code gérait des offsets supérieurs à 0x40000 (256K, la mémoire maxi en VGA) mais que ça a été supprimé à cause d'un accès à des registres NVIDIA spécifiques qui créent des instabilités.

Donc je ne sais pas du tout ce qui est supporté concernant les modes graphiques. Je ne comprends d'ailleurs pas comment il définit l'offset en ne se basant que sur le nombre de colonnes et le nombre d'octets par pixel. Si on a un framebuffer avec les données alignées, il faudrait aussi utiliser la résolution verticale pour le calcul. Il va falloir relire ce code pour voir ce que ça fait réellement.

Détection du retour vertical

La détection du retour vertical ne me semble pas être très optimale. En regardant un peu la doc sur la programmation des registres VGA, on voit qu'il existe un registre sur lequel on peut lire l'état du retour pour les cartes EGA (ouais, ça nous rajeunit pas :).

@@ 3C2h (R): Input Status #0 Register

        bit 4  Status of the switch selected by the Miscellaneous Output
               Register 3C2h bit 2-3. Switch high if set.
            5  (EGA Only) Pin 19 of the Feature Connector (FEAT0)
                          is high if set
            6  (EGA Only) Pin 17 of the Feature Connector (FEAT1)
                          is high if set
            7  (EGA Only ??) If set IRQ 2 has happened due to Vertical
                     Retrace. Should be cleared by IRQ 2 interrupt routine
                     by clearing port 3d4h index 11h bit 4.@@

Donc on pourrait peut-être se passer des évaluations de la durée d'une trame et juste regarder l'état du bit 7 de ce registre pour savoir si l'interruption est due à un retour vertical. À tester bien évidemment, voir aussi s'il n'existe pas un registre équivalent pour les cartes VGA ou NVIDIA. Ça semblerait logique, sinon c'est assez goret comme façon de détecter les retours verticaux.

Visiblement il n'y a qu'une IRQ, donc est-ce vraiment possible de détecter les retours pour deux écrans ? S'il y a deux CRTC, je ne vois vraiment pas pourquoi on ne pourrait pas. Après tout, OpenGL et XVidéo arrivent bien à tester ce retour pour l'un ou l'autre des écrans et c'est pris en compte immédiatement. Ça semble indiquer qu'il y a juste lecture d'un flag et pas une reprogrammation hard, donc pourquoi pas...

Calculs d'ajustement

Il y a également quelques calculs qui sont faits dans cette fonction indépendamment de l'état de l'application. Ça concerne probablement des ajustements pour la synchronisation des machines distantes, mais je n'ai pas compris comment ça fonctionnait. Ce sera probablement du code à supprimer, mais il faudrait quand même comprendre à quoi ça sert avant.

mardi 15 juin 2010

NVStereo, un module basé sur Genlock

Suite à mes tests avec Genlock, j'envisage d'écrire un nouveau module basé sur ce dernier qui s'appellera NVStereo. NV parce qu'il ne concernera dans un premier temps que les cartes graphiques NVIDIA et Stereo parce que je compte supprimer les fonctions de genlocking/framelocking et ne conserver que le code relatif à la stéréo en page flipping sur un unique écran.

Voici les modifications que je compte inclure dans NVStereo par ordre de priorité :

  1. suppression du code relatif au genlocking/framelocking
  2. suppression de la gestion du port parallèle pour le genlocking et le pilotage de lunettes
  3. inversion du rendu quand un retour vertical est manqué
  4. inversion du rendu par une application en user space en temps réel
  5. activation/désactivation du page flipping par une application en user space en temps réel
  6. détection de la parité des trames pour toujours alterner les trames dans un ordre prédéfini
  7. détection des changements de mode graphique et recalcul de la durée d'une trame
  8. support des lunettes LCD à obturation VGA DDC
  9. choix du CRTC pour la synchronisation avec le retour vertical
  10. support des lunettes GeForce 3D Vision si je trouve un moyen de tester les bouts de code existants

À terme, j'aimerais m'en servir comme base pour un futur driver 3D généraliste capable de fonctionner aussi bien avec OpenGL qu'avec XVideo et peut-être X Window de façon plus générale. Il faudra également trouver de la doc pour adapter le code à d'autres marques de cartes graphiques (ATI et Intel dans un premier temps) et procéder à quelques adaptations supplémentaires.

Concernant les cartes NVIDIA, le code de détection du retour vertical ne devrait pas avoir à subir de changements, mais il faudra essayer de faire en sorte que le page flipping puisse fonctionner sans écran virtuel et si possible en mode fenêtré. Pour X Window et/ou XVideo il faudrait tester avec un Pixmap en mémoire vidéo, qu'il faudra sans doute marquer d'un tag spécial pour que le driver sache de quel Pixmap il doit s'occuper. Il sera sans doute impossible de gérer autre chose que des modes plein écran avec le page flipping.

À priori le driver stéréo 3D NVIDIA fonctionne d'une manière similaire pour l'affichage de vidéos et d'images, en utilisant une surface vidéo avec une résolution horizontale double et une ligne supplémentaire contenant un tag spécifique ainsi que des paramètres pour le rendu.

Les paramètres sont à priori les suivants pour le tag :

  • unsigned int dwSignature;
  • unsigned int dwWidth; // à priori non utilisé pour l'instant (trouvé sur MTBS3D)
  • unsigned int dwHeight; // à priori non utilisé pour l'instant (trouvé sur MTBS3D)
  • unsigned int dwBPP;
  • unsigned int dwFlags;

Contenu de la signature :

  • #define NVSTEREO_IMAGE_SIGNATURE 0x4433564e //NV3D

Et pour les flags :

  • #define SIH_SWAP_EYES 0x00000001
  • #define SIH_SCALE_TO_FIT 0x00000002
  • #define SIH_SCALE_TO_FIT2 0x00000004 // à utiliser avec le précédent pour conserver l'aspect ratio (trouvé sur MTBS3D)

Ensuite cette surface est envoyée vers le back buffer avec un StretchRect et la vidéo est affichée en 3D lorsque le back buffer est présenté à l'écran. Il faudra voir s'il y a besoin d'utiliser un back buffer avec X Window/XVideo (éventuellement avec DBE) ou si on peut se contenter de pixmaps en mémoire vidéo.

Il faudra voir si la même chose est possible sous OpenGL avec des FBOs ou trouver une autre technique en étudiant le mode de fonctionnement des drivers stéréo NVIDIA sous Direct3D et/ou OpenGL.

Il faudra également ajouter du code pour la gestion de divers paramètres, comme la séparation et la convergence par exemple.

dimanche 13 juin 2010

Rendu YV12 vers une fenêtre off-screen

Le rendu vers une fenêtre off-screen ne fonctionne pas, j'ai testé sur des fenêtres non-mappées ou mappées créées avec un backing-store à Always. J'ai bien ajouté l'option BackingStore dans xorg.conf et xdpyinfo m'indique bien que le backing-store est activé. Si la fenêtre dans laquelle j'écris le XVimage est visible et non obscurcie par une autre fenêtre, ça fonctionne et dans ce cas si j'obscurcis ensuite la fenêtre son contenu est bien conservé.

Visiblement, il faut qu'une fenêtre soit visible et non obscurcie lorsqu'on écrit dedans pour que le backing-store fonctionne par la suite. Retour à la case départ donc, le mieux sera sans doute de modifier Xorg pour qu'il puisse utiliser XvShmPutImage vers un Pixmap, en espérant que ce soit supporté par les drivers de cartes graphiques.

Déception sur l'utilisation d'un thread

J'ai fait un test pour mettre le code d'affichage dans un thread, mais ce n'est pas du tout concluant. Je n'arrive pas à obtenir un rendu des images synchronisé avec le balayage de l'écran avec cette technique. De toute façon ça ne semblait pas être une très bonne solution de faire tout ça en espace utilisateur, il y aurait forcément eu des problèmes de synchro vu le type de scheduling du noyau (pas temps réel).

SoftGenLock/GenLock

Il va donc falloir trouver une autre solution pour synchroniser l'affichage de chaque image avec le retour vertical de l'écran. La solution qui me semble être la plus logique serait de faire comme dans Genlock (basé sur SofGenLock), écrire un module pour le noyau Linux (2.6) qui s'occupe de la synchronisation. Il va falloir rentrer dans leur code pour voir comment ils font et déterminer si c'est faisable.

Rendu YV12 off-screen

Il faudra également trouver une meilleure solution pour le rendu des XVimage vers des pixmaps. Pour l'instant, je fais le rendu vers une fenêtre et je copie le résultat vers un Pixmap, mais si cette fenêtre est visible il faut afficher l'image qui correspond au bon oeil, ce qui me semble compliqué à faire. Il faudra donc essayer de faire le rendu vers une partie non visible de l'écran, peut-être une fenêtre non mappée ou une fenêtre avec backing-store. L'idéal étant d'avoir deux fenêtres non visibles, dont on copie le contenu alternativement vers la fenêtre visible, une sorte de double buffer à trois fenêtres...

vendredi 11 juin 2010

Quelques idées avant d'oublier...

J'étais tellement content de mes progrès pour l'affichage rapide de vidéos que j'allais en oublier de noter quelques idées qui me sont venues dans la foulée...

OpenGL

Maintenant que je sais envoyer du YUV12 vers un Pixmap, je pourrais peut-être envisager d'utiliser OpenGL pour le rendu. L'avantage majeur serait d'avoir probablement de meilleurs résultats en ce qui concerne la synchronisation avec le retour vertical. D'autre part ça permettrait d'avoir une approche unifiée avec l'éventuel pilote stéréo 3D que je compte continuer à écrire.

XRenderComposite

Sinon, je peux peut-être aussi remplacer XCopyArea par XRenderComposite, des tests récents m'ayant montré que la différence de performance ne semblait pas être si négligeable si on n'utilisait pas un Pixmap en mémoire partagée.

Si l'extension MIT-SHM est présente, l'écart de perfs est seulement de 6 ou 7 % sinon, ça vaut donc quand même le coup d'utiliser XRenderComposite dans tous les cas. Reste à voir si XRenderComposite est bien supporté partout.

DepthPlayer : pourquoi faire compliqué quand on peut faire simple ?

En faisant quelques tests, j'ai trouvé une solution plus simple que de modifier Xorg pour pouvoir faire des XvShmPutImage vers un Pixmap. De toute façon il n'y avait aucune garantie sur le résultat et ça n'aurait probablement pas été portable sur toutes les cartes graphiques.

La nouvelle technique

Chaque trame du fichier vidéo est décodée vers un XvImage au format YV12, que j'affiche comme précédemment dans ma fenêtre principale avec un XvShmPutImage. Ensuite, il suffit de copier le contenu de cette fenêtre vers un Pixmap avec XCopyArea. J'ai testé, ça fonctionne correctement.

Il faut faire attention à bien créer un Pixmap avec la même profondeur de couleur que celle de la fenêtre quand on la crée avec XCreateWindow (24bpp ici), sinon ça provoque une erreur sur le XCopyArea. Il faudra s'en rappeler au moment d'intégrer le code dans MPlayer.

Ensuite, la boucle d'affichage consiste comme prévu à alterner des XCopyArea et des XFlush en utilisant glXGetVideoSyncSGI et glXWaitVideoSyncSGI pour la synchronisation avec le retour vertical.

Sauts de trames

Il n'y a quasiment pas de sauts de frames et le cas échéant ils sont de toute façon correctement détectés par glXGetVideoSyncSGI. Il devrait donc être possible de gérer ces sauts dans MPlayer pour éviter l'inversion des yeux.

il y a par contre quelques artefacts visuels assez légers, mais un petit peu gênants quand même. Par moment quand on ferme un oeil on voit pendant un très court moment - moins de quelques millisecondes à priori - une image superposée à l'autre. Ça le fait surtout au début puis ça semble le faire de moins en moins, à croire que glXWaitVideoSyncSGI met du temps à se mettre en route mais finit par se stabiliser.

Je ne sais pas trop à quoi c'est dû, sans doute au fait qu'il peut y avoir un délai plus important que la durée du retour vertical entre la fin de l'attente de glXWaitVideoSyncSGI et la fin du XCopyArea/XFlush.

Le XCopyArea/XFlush étant particulièrement rapide - environ 150 microsecondes pour une image de 500x500 - j'imagine que ça peut être dû au scheduler qui ne donne pas forcément la main tout de suite à l'appel suivant le glXWaitVideoSyncSGI. Peut-être que ça pourra se régler en utilisant des threads avec une priorité élevée.

Synchronisation au lancement de l'application

Il existe un autre problème, l'affichage n'est pas forcément correctement synchronisé au bon oeil au lancement de l'application. Puisque l'appel à glXGetVideoSyncSGI fournit le nombre de retours verticaux, on devrait pouvoir afficher l'image appropriée à chaque oeil en testant si la valeur est paire ou impaire, mais ça ne fonctionne pas.

Ça veut dire qu'il ne faudra pas compter sur l'activator eDimensional si on veut que les vidéos s'affichent correctement et donc qu'il faudra implémenter les appels DDC pour contrôler directement les lunettes. Mais c'est quand même pas normal, il faudra essayer de trouver pourquoi ça ne fonctionne pas correctement.

C'est un peu inquiétant aussi, ça veut dire que glXGetVideoSyncSGI rate peut-être des trames de temps en temps, ce qui pourrait arriver pendant la lecture. Si c'est vraiment le cas, je n'ai aucun moyen de corriger ça pour l'instant.

Utilisation CPU

Côté performances, l'application prend entre 0.7 et 1.3 % de CPU, ce qui est vraiment négligeable. Xorg prend entre 2 et 3 %, à comparer aux 1 à 2 % quand l'application n'est pas lancée. Ça va donc laisser pas mal de temps à MPlayer pour décoder les trames et c'est un gain assez net par rapport à l'ancienne méthode à base de XvShmPutImage uniquement.

Prochaine étape, probablement l'intégration de cette technique dans MPlayer...

jeudi 10 juin 2010

Lecture de vidéos en stéréo 3d sous Linux

Mon objectif est de pouvoir lire des vidéos en stéréo 3d sous Linux avec différents types de lunettes à obturation LCD sur des écrans supportant une fréquence de balayage élevée (idéalement 120 Hz).

J'écris ces quelques lignes pour avoir une trace de mes idées, il y a probablement pas mal de choses erronées ou incohérentes dans ce qui suit, mais ça m'aide à essayer d'y voir un peu plus clair.

Formats vidéo

Pour mes premiers tests je suis parti sur une modification de MPlayer en essayant de me concentrer sur le format side-by-side en pleine résolution dans un premier temps. D'autres formats doivent pouvoir être facilement supportés par la suite : side-by-side half et above-and-below half et full. À priori MPlayer ne sait pas gérer le dualstream et aucune implémentation ne permet encore de décoder le format Blu-ray 3D (ssif), je mets ça de côté pour plus tard.

Types de lunettes

Ça ne « fonctionne » pour l'instant qu'avec des lunettes SimuleyesVR en mode WLC (White Line Code) ou des lunettes eDimensional avec l'ED activator en mode page flipping sous Wine. À terme le but est de supporter le maximum de types de lunettes à obturation (WLC/BLC, VGA DDC, 3D Vision, etc.).

J'ai déjà quelques bouts de code qui permettent d'envoyer un signal DDC avec certaines cartes graphiques NVIDIA (NV40 et NV50), mais rien pour l'instant pour d'autres types de lunettes. Quand j'aurais fait plus de progrès sur ce sujet, je collerai le tout dans une librairie.

Types d'écrans

Je fais les tests sur un écran CRT (ViewSonic P90f) connecté en VGA qui supporte du 120 Hz en 1024x768. À priori cette application ne devrait être destinée qu'à des écrans ou projecteurs supportant nativement des fréquences élevées, les autres types d'écrans (TV 3D) pouvant supporter nativement des formats side-by-side. Ça devrait limiter l'usage de l'application aux écrans CRT et aux projecteurs DLP.

État des lieux

Pour l'instant, l'application consiste en une modification du driver xv de MPlayer et est capable d'afficher les trames gauche et droite l'une après l'autre en mode fenêtré ou plein écran. Je ne peux pas lire de vidéos en 1080p (3840x1080) car elles dépassent la taille maximale d'une image X Video (2048x2048), mais ce problème doit pouvoir se régler en créant deux images séparées avant de les envoyer au driver xv.

Côté performances, la synchronisation avec le rafraîchissement de l'écran (120 Hz) « marchotte » pour des vidéos en 360p avec quelques sauts de trames, mon PC n'est pas assez puissant pour des résolutions supérieures (Athlon XP 2400+). J'ai modifié la fonction de la touche pause pour inverser les yeux quand ces sauts se produisent.

La boucle d'affichage est un hack assez moche qui consiste pour l'instant en 5 appels successifs à la fonction d'affichage (XvShmPutImage) avec des XSync intercalés.

J'envisage de faire les modifications suivantes pour améliorer la synchronisation et la vitesse de lecture :

  • décoder une trame vers deux images de type Xvimage (gauche et droite)
  • copier chaque image vers un pixmap en mémoire vidéo (avec XvShmPutImage)
  • utiliser un thread d'affichage qui affiche successivement ces deux pixmaps en synchronisation avec le retour vertical (avec des XCopyArea ou des XRenderComposite)

Pour ce faire, il faut régler les problèmes suivants :

  • modifier X.org pour que XvShmPutImage puisse écrire vers un Drawable de type Pixmap. Pour l'instant il n'est capable de le faire que vers un Drawable de type Window. Des patches ont été proposés pour corriger le problème mais ils n'ont pas encore été intégrés et il n'est pas sûr qu'ils soient assez complets pour gérer ce problème correctement.
  • vérifier que le driver NVIDIA est capable de gérer l'écriture vers un Pixmap, sinon il faudra sans doute essayer avec le driver libre Nouveau et le modifier si besoin est.
  • trouver une méthode de synchronisation avec le retour vertical. Je pars sur l'idée d'utiliser les fonctions glXGetVideoSyncSGI et glXWaitVideoSyncSGI qui nécessitent un contexte OpenGL courant. Voir aussi XF86VidSetViewPort, XSync ne semblant pas être très adapté pour cette tâche (round-trip complet de et vers le serveur). Ça semble fonctionner correctement selon mes premiers tests, même avec une charge CPU annexe assez importante et sans avoir de busy-wait.
  • vérifier que l'on peut effectivement utiliser un thread pour la partie rendu, X Window n'étant pas réentrant il y aura probablement des choses à gérer à ce niveau, probablement en utilisant les fonctions XInitThreads, XLockDisplay et XUnlockDisplay.
  • faire en sorte que le thread ait une priorité suffisamment haute et qu'il utiliser un scheduler adapté pour ne pas rater de trames.
  • trouver un moyen de détecter les sauts de trames, probablement avec un mix de glXGetVideoSyncSGI et d'un timer.
  • voir si l'utilisation de DBE (Double Buffer Extension) pourrait apporter quelque chose.
  • en cas de difficulté avec la synchronisation, étudier X Synchronization Extension et le KMS page flip ioctl récemment introduit dans le kernel (2.6.33).

Pistes écartées

J'ai déjà testé plusieurs pistes que j'ai écartées par la suite :

  • utilisation d'OpenGL : ça nécessite de convertir les trames de la vidéo qui sont en YV12 vers du RGB pour l'affichage. Il est possible d'implémenter cette conversion avec un fragment shader mais c'est beaucoup plus lent que X Video. C'est la technique qu'utilise sView (en utilisant libswscale) mais ça va à deux à l'heure sur mon PC.
  • utilisation de deux fenêtres : j'ai essayé d'envoyer des images YV12 vers deux fenêtres indépendantes en modifiant leur ordre d'affichage avec des XRaiseWindow/XLowerWindow et XRestackWindows mais je n'ai pas réussi à synchroniser avec le retour vertical. Et je ne pense pas que ce soit une solution assez bas niveau pour obtenir des résultats concluants (passe par le window manager).

Voilà, c'est tout pour aujourd'hui, maintenant dodo... :)