Journal Mes péripéties en VR

Posté par  . Licence CC By‑SA.
63
10
oct.
2023

Il y a environ 3 ans, j'ai eu accès à un Oculus Quest premier du nom. La réalité virtuelle est assez impressionnante, mais tout passe par le magasin d'application, le SoC est plutôt limité et je n'aime pas Facebook.
Je décide donc de voir comment l'utiliser sur un PC, il y avait la solution Oculus Link (avec câble à l'époque), Virtual Desktop, et ALVR les deux sans fil. Aucune solution ne marche sous Linux, quelle déception ! Heureusement ALVR est un projet open source, je peux donc regarder comment ça marche et le porter sous Linux.

Mon périple commence alors, ALVR est un driver pour SteamVR, qui est une implémentation de OpenVR. ALVR est à l'époque composé d'un mélange de C++/rust sur la partie PC et C++/java sur le casque. La première étape est donc de faire compiler la partie PC sous Linux, travail pas très intéressant qui consiste principalement à remplacer des APIs Windows par des fonctions standard C++ pour les threads, timers, mutex etc. Je laisse pour le moment de côté la partie image, il y a du DirectX partout et je verrai plus tard comment faire l'équivalent.

Mais que fait exactement un driver SteamVR ? Il communique la position du casque et des manettes à tout instant, il donne l'état des boutons et sticks, traite les commandes de retour haptique (vibrations) et optionnellement s'occupe d'afficher l'image. Pour la question du portage, seule l'image va être un sujet intéressant.
Tout d'abord pourquoi le driver peut ne pas s'en occuper ? Parce que les casques avec fil sont simplement des écrans du point de vue du PC, le driver n'a qu'à décrire comment identifier la bonne sortie, la déformation à appliquer et SteamVR fait tout tout seul. Pour un casque autonome ce n'est a priori pas ce qu'on veut, il n'y a rien de branché et il faut que le driver envoie l'image lui-même.
C'est le moment de lire un peu de docs, l'interface IVRVirtualDisplay est supposément prévue pour ce cas, mais est en réalité mal conçue et ne permet pas d'avoir les informations d'orientation associées à une image. Dans le cas d'un casque autonome le processus est d'encoder l'image (environ 10ms), l'envoyer par le réseau (5ms), la décoder (20ms) et l'afficher à la vsync suivante, pendant ce temps l'utilisateur a peut-être bougé la tête et il est primordial de recaler l'image pour l'afficher là où elle devait être. ALVR utilise donc une autre interface: IVRDriverDirectModeComponent où SteamVR donne les différentes composantes de l'image, avec leur position. L'usage le plus courant est d'avoir une partie fixe par rapport à la tête et une fixe par rapport au monde, du fait des limitations d'encodage, de débit et de décodage, tout ça est aplati en une seule couche que l'on considère fixe par rapport au monde et que la partie casque va reprojeter à la volée.

Tout à l'air très facile, il suffit d'implémenter cette interface ! Étrangement les docs sont très peut précises, il semble que ça ait été écrit pour gérer les premier casques Oculus et pas vraiment pour tout le monde. Je fais quelques supposition sur comment les identifiants de texture peuvent marcher sous Linux, bidouille des choses et arrive à quelque chose qui compile. Et là, rien ne marche, après de longues heures à voir pourquoi SteamVR plante, je vois qu'il plante avant d'appeler mon code, IVRDriverDirectModeComponent ne fonctionne tout simplement pas sous Linux. Bon, tant pis, on va voir si sans les informations de position c'est utilisable quand même, et donc je réécris pour IVRVirtualDisplay, pour arriver à la même conclusion.
Aucune des interfaces pour obtenir les images ne marche sous Linux ! Le casque est-il voué à ne marcher que sous Windows ou en mode autonome ? Non, on est sous Linux et on a donc accès à tout.

Commence alors une série d'expérimentations plus ou moins farfelues, mon idée est de faire croire à SteamVR qu'un écran est branché. La première approche est de me baser sur evdi pour créer un écran virtuel et accéder aux images en espace utilisateur. Pas mal d'efforts sont passés dans ce prototype, entre autre j'ai appris à écrire des EDID pour faire reconnaître l'écran, un encodeur basé sur ffmpeg. J'ai d'ailleurs découvert que les modèles de casque sont en dur dans le kernel pour éviter que le bureau s'affiche dessus. Le résultat est que finalement quelque chose s'affiche sur le casque, initialement en nuances de bleu, mais c'est prometteur.
Même si c'est un bon début, les performances sont atroces, il faut installer un module kernel, et assez souvent tout se bloque. Il reste encore beaucoup d'angles d'attaque, stratégie suivante: faire croire au GPU que l'écran existe vraiment et utiliser kmsgrab de ffmpeg pour récupérer l'image sans copie. En utilisant edid_override et force dans /sys/debug/dri, on arrive à faire marcher kmsgrab. Le 9 mars 2021 l'afichage avec encodage matériel fonctionne pour la première fois. Il y a quelques limitations: il faut un GPU AMD ou Intel, un port HDMI libre, et exécuter quelques commandes en root. C'est tout de même une amélioration par rapport à un module exprès, et on a des performances correctes. Le PC a quand même tendance à bloquer assez souvent, avec l'affichage de tout le bureau corrompu, peut-être que bidouiller des valeurs dans /sys/debug n'est pas sans risque.

Mais alors comment faire pour obtenir les images sans trop mentir à la carte graphique ? Il suffit de mentir à quelqu'un d'autre ! Dans les logs de SteamVR au démarrage on voit qu'il cherche un écran correspondant aux dimensions données par le driver, et pour cela il utilise plusieurs méthodes. Un des messages est "Tried to find direct display through Vulkan WSI: (nil)", la prochaine stratégie est donc que cette méthode retourne un résultat. Vulkan prévoit un système de "layers", initialement prévus pour des couches de validation, ils permettent d'interposer des fonctions entre l'application et le pilote. Quelqu'un a même écrit un layer pour fournir le support de l'extension "headless", en s'inspirant fortement de ce code est né un layer qui fournit l'extension VK_KHR_display et fournit ces images sous forme de descripteurs de fichiers dans une socket. L'intégration de ce layer n'est pas trop difficile : on remplace un exécutable de SteamVR par un wrapper qui force l'inclusion du layer Vulkan, puis exécute le vrai.
Le 16 avril 2021 la pull request pour utiliser la méthode Vulkan est publiée, on peut considérer que c'est la première version pratiquement utilisable.

Maintenant que l'on a une méthode robuste pour accéder aux images, on peut voir que le résultat est quand même assez mauvais: dès que l'on bouge la tête, l'image semble bouger de manière saccadée. En effet les images calculées par l'application le sont en fonction de la position prédite au moment où elle doit apparaître à l'écran, et le layer Vulkan n'a aucun moyen de savoir pour quelle position une image donnée a été vraiment calculée. Suivent donc plusieurs tentatives pour deviner cette information, à base de temps, d'informations fournie par l'interface de statistiques de SteamVR, d'heuristiques diverses mais rien n'y fait, impossible d'associer une orientation à une image.
Comment rester sur un résultat si décevant ? L'information est bien présente quelque part, il suffit d'aller la chercher. Après tout il s'agit de chercher une aiguille dans une botte de foin, ce qu'il n'est qu'une question d'outil adapté tel un lance-flamme ou un aimant. En décompilant le binaire vrcompositor avec ghidra, on suppose que les positions (pose dans la nomenclature OpenVR) sont la même structure que dans l'API publique, on trouve quelques résultats. Ensuite, à l'éxécution du programme, quand le code du layer est appelé il suffit de remonter la pile d'appels avec libunwind, trouver la fonction que l'on veut via son nom et inspecter la mémoire pour trouver quelque chose qui ressemble à notre objet. C'est - à ce jour - probablement le hack le plus horrible et dont je suis le plus fier: pose.cpp, on cherche une struct avec une certaine valeur pour les booléens, un code de retour 200 et une matrice ayant les propriétés d'une matrice de rotation. La suite est triviale en comparaison : on envoie la matrice en même temps que l'image, le driver récupère l'information et rejoint le chemin utilisé par le code originel. L'image est maintenant parfaitement fluide.

Mes contributions à ALVR s'arrêtent pratiquement là, d'autres contributeurs ont corrigé des bugs, peaufiné les réglages d'encodeur, ajouté des fonctionnalités mais en réalité je n'ai pas beaucoup suivi parce que je ne joue pas à des jeux en VR.

Mais ce n'est pas la fin de l'histoire encore ! L'approche de ALVR reste assez fragile, et non optimale : SteamVR pense qu'il envoie l'image à un vrai écran, il va donc attendre le dernier moment de telle sorte que l'image est produite juste avant l'évènement de VSync. On peut donc mieux faire, et probablement se passer de SteamVR qui reste propriétaire.
Avec Meumeu on s'est donc lancés dans un projet basé sur monado, dans le but de faire une implémentation native OpenXR. Malheureusement cher lecteur l'histoire a beaucoup moins de péripéties, Monado est entièrement libre, les mainteneurs ouverts à toute proposition, donc dès qu'un obstacle se présente on peut le résoudre proprement.
Le résultat est WiVRn, avec comme principal atout la possibilité de découper l'encodage en plusieurs sous-flux pour réduire la latence : en coupant en 2 morceaux on commence à transférer et décoder la première partie de l'image alors que le reste est encore en cours d'encodage, ce qui économise 2 à 3ms.
Le principal problème est qu'aucun jeu ne fonctionne avec OpenXR sous Linux, et il faut donc passer par Steam/Proton, même utiliser OpenComposite pour présenter l'API OpenVR, mais les résultats ne sont pas probants.
N'ayant pas gagné assez de traction, je me suis un peu désintéressé du projet, je compte sur vous pour y contribuer ou me donner envie de m'y remettre.

  • # une app ?

    Posté par  (site web personnel) . Évalué à 3.

    Il faudrait faire un compositeur x pour bosser avec le casque ?

    "La première sécurité est la liberté"

    • [^] # Re: une app ?

      Posté par  . Évalué à 5.

      Il y a https://github.com/galister/WlxOverlay (OpenVR) et https://github.com/galister/wlx-overlay-x (OpenXR) pour afficher le bureau en VR.

      https://stardustxr.org/ est un compositeur wayland spécifique pour la VR.

      Je n'ai jamais essayé, et je suppose qu'avec la résolution du Quest 1 le texte sera peu lisible. J'ai des doutes sur l'intérêt de la VR pour autre chose que du jeu/divertissement.

      • [^] # Re: une app ?

        Posté par  (site web personnel) . Évalué à 4.

        Et pour de la sculpture 3d type blender ?

        "La première sécurité est la liberté"

        • [^] # Re: une app ?

          Posté par  . Évalué à 2.

          Si une application veut vraiment interagir en VR, elle utilise OpenXR, ce qui permet d'avoir les informations de position du casque, des manettes et envoyer les commandes d'affichage. Blender a déjà un plugin pour afficher en VR, mais pas encore pour éditer je crois.

          Ensuite pour ce qui est en dehors de l'application il n'y a pas grand chose pour le moment : une seule application peut être active à la fois et le lancement d'une application se fait de manière externe. Je ne sais pas si Monado a des projet de gérer une bascule d'application active, ou un lanceur basique pour ne pas avoir juste un écran noir quand on met le casque. Chez SteamVR il y a un "SteamVR Home" qui sert de lanceur et un peu de démo.

  • # Wow (et questions)

    Posté par  (site web personnel) . Évalué à 4.

    Wow.
    J'ai rarement lu un texte à la fois si technique et si conversationnel, genre "journal de dév du bureau d'à côté", sur un thème aussi niche (car trop "ludique", pourtant une commu est supposée exister…)

    J'ai d'ailleurs découvert que les modèles de casque sont en dur dans le kernel pour éviter que le bureau s'affiche dessus.

    Ah incroyable. Donc y a un "support" ;-).

    Malheureusement cher lecteur l'histoire a beaucoup moins de péripéties, Monado est entièrement libre, les mainteneurs ouverts à toute proposition, donc dès qu'un obstacle se présente on peut le résoudre proprement.

    Un chemin de croix, quoi :).

    Le sujet m'intéresse car, bien que n'étant plus un hardcore gamer, je n'ai pas envie que mon casque devienne une brique quand le fabricant déménagera sa prison dorée (login online, etc).

    Mon cas d'usage est bien sûr le jeu SteamVR.
    Mais oublions ça deux secondes, un wrapper SteamVR->OpenXR est-il envisageable à ton avis? Et pourquoi OpenVR, qui a l'air d'être en concurrence directe, est-il si mal-aimé ?
    Dans tous les cas, envisages-tu l'intégration de WiVRn dans une solution "clés en main" qui gérerais aussi les inputs du gyroscope positionnel du casque (j'ignore même si ça existe) ?

    PS : Je promets d'essayer WiVRn une fois !

    • [^] # Re: Wow (et questions)

      Posté par  . Évalué à 2.

      Sur OpenVR/SteamVR/OpenXR c'est toujours un peu compliqué.

      OpenVR est un "vieux" standard poussé par Valve à l'origine, et entre autres le seul backend supporté par Unity pour Linux.
      SteamVR est à la fois l'environnement d'accueil de Steam en VR et une implémentation à la fois d'OpenVR et OpenXR.
      Monado est un peu le mesa d'OpenXR, avec des pilotes pour plusieurs casques.

      OpenVR est en très lié à SteamVR, et le projet OpenComposite qui tente de faire une implémentation de OpenVR sur OpenXR n'est pas capable de faire tourner tous les jeux. Comme OpenVR est considéré comme obsolète y compris par Valve, on aimerait bien qu'il disparaisse tout simplement.

      WiVRn est prévu pour être auto-suffisant dans le cas OpenXR, il gère la vidéo, les positions, les boutons, les vibrations. À part l'installation de l'application sur le casque, et le support OpenVR c'est déjà clé en mains.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.