Les coulisses du Techdays 2014 : Développer un driver pour l’Oculus Rift : Part I

23 Décembre 2013 : J-50 avant l’édition des Techdays 2014.

Il y a une dizaine de jours l’équipe de BabylonJS (David Catuhe, David Rousset, et Pierre Lagarde pour ne pas les nommer), se posait la question suivante :

Quelle serait la meilleure manière d’intégrer l’Oculus Rift à BabylonJS ?

Sachant que BabylonJS est un moteur 3D qui fonctionne sur WebGL, il est destiné essentiellement à tourner dans un navigateur. La 1ere réponse qui vient à l’esprit est donc de développer un plug-in à IE 11. Néanmoins, si on veut que cela fonctionne sur les autres navigateurs il faut alors développer un Plug-In pour chaque navigateur.

Néanmoins, IE11 et les autres navigateurs savent à priori utiliser les Sensors APIs pour s’attacher à des capteurs de type orientation par exemple. APIs qui permettent facilement de piloter tous types de capteurs et qui sont également disponibles désormais avec le Windows Runtime 8.1 pour le développement d’application Modern UI.

La 2ième solution qui s’offre à nous est de partir du document Integrating Motion and Orientation Sensors et de développer un driver Orientation pour l’Oculus Rift.

C’est ce que nous allons aborder dans ces différents Billets, et vous verrez que tout compte fait, à bien y regarder, que c’est plus simple que l’on croit.

Mais avant de démarrer, il y a un certain nombre de prérequis à suivre et surtout une préparation de l’environnement de développement et du pc.

Préparation de l’environnement de développement

Tout d’abord, il est préférable de ne pas utiliser comme moi je l’ai fait, une machine de production, qui est dans un domaine, assujettie à Bitlocker et des contraintes de stratégies venant de L’IT. Cela ne ferait que retarder le moment ou vous allez pouvoir coder. Une seconde machine est donc préférable, mais attention, je ne suis pas sur qu’une machine Virtuelle soit utile dans ce cas la.

  1. Il vous faut un outil de Développement, et Visual Studio 2013 fait très bien l’affaire, comme nous le verrons par la suite.
  2. Installer le SDK Windows 8.1, ça mange pas de pain.
  3. Installer le WDK Windows 8.1, c’est nécessaire (WDK=Windows Driver Kit)

L’installation du SDK et du WDK ce fait dans ce répertoire, C:\Program Files (x86)\Windows Kits\8.1 gardez le à l’esprit car nous irons y piocher les outils nécessaires pour tester, tracer, et installer notre driver.

Pour que Windows accepte l’installation du driver, il faut qu’il soit signé. L’étape de la signature du driver venant en toute fin du processus, nous allons préparer la machine pour qu’elle accepte l’installation du driver sans signature.

Pour ce faire dans une console en mode administrateur, il suffit de taper la commande suivante :

BCDEDIT /SET TESTSIGNING ON

et de rebooter la machine.

Néanmoins, cette commande ne peut aboutir que si vous avez désactivez le Secure Boot dans le BIOS\EFI.

Si tel n’est pas le cas, elle échouera, il faudra donc aller dans la BIOS et le désactiver. Mais attention désactiver cette option, peut engendrer une rupture des stratégies imposées par votre IT (par exemple BitLocker vous demandera de rentrer la clé de récupération) Alors même si on arrive en fin de compte à s’en sortir, il est préférable d’utiliser une machine de développement à part.

Rappel sur l’architecture de Windows

Avant de poursuivre, j’aimerai faire un rapide rappel sur l’architecture de Windows.

Windows Architecture

Sur cette image, on voit bien que Windows est composé de deux blocs principaux.

Le Mode User et le Mode Kernel, et on imagine qu’un driver est développé dans les couches basses de Windows, c’est à dire en mode Kernel. Bien évidement la majorité des drivers sont développés dans ce mode, pour éviter la transition entre mode Kernel et mode User qui peut être pénalisant pour les performances. Néanmoins, lorsque ce type de driver plante voici ce qui arrivait :

image

Le fameux écran bleu qui a fait tant de torts à Windows, alors qu’il ne faisait que se protéger Clignement d'œil

Microsoft a donc décidé, pour renforcer la stabilité de son système et simplifier le développement de driver, de changer d’architecture et de passer du mode VxD vers l’architecture WDM (Windows Driver Model).

Néanmoins, développer un driver est un vrai chalenge, et les erreurs peuvent être toujours fatales. Microsoft fournit donc, depuis 2006 le Framework WDF (Windows Driver Frameworks), qui d’une part simplifie le développement, mais permet également de développer des drivers en mode User afin que le système soit encore stable même après un éventuel plantage du driver .

C’est tout naturellement vers ce type de driver que nous avons choisi de nous tourner pour notre Driver Oculus Rift, d’autant plus que des modèles sont disponibles avec Visual Studio 2013 comme nous allons le voir dans la prochaine section.

Création de la coquille de notre 1er driver.

Comme mon 1er job à Microsoft, c’était justement le développement de drivers, l’équipe a trouvé tout naturel de me demander de le faire. Néanmoins, comme cela a tellement évolué depuis 25 ans, je suis dans la peau d’un jeune débutant qui découvre un Framework (WDF) totalement nouveau. Je décide donc de partir d’exemples disponibles dans le Windows Kit Drivers Samples Pack

Sans ce pack, soit vous êtes un génie et muni de la doc vous y arrivez tout seul, soit vous vous inspirez des exemples tout comme moi. Néanmoins, pour bien comprendre les tenants et les aboutissants, je suis quand même parti from scratch, et me suis donné chaque jour un objectif bien précis pour arriver au but final, intégrer le capteur Oculus Rift. D’autant plus que nos 3 lascars, m’ont donnés fin décembre pour prouver que cela soit possible.

Comme je le disais en introduction, mon principal “client”, n’est pas une application utilisateur, en particulier, mais les Sensor APIS, qui se branche sur un driver de type OrientationSensor dans notre cas, et qui devra fusionner les données venant de l’Oculus Rift, qui possède quand à lui 3 Capteurs :

  • Un Gyroscope
  • Un magnétomètre
  • Un accéléromètre.

De mon point de vu, je sens que cela sera la partie la plus difficile à implémenter.

Néanmoins, il faut bien commencer et mon 1er objectif étant de comprendre la plateforme WDF, il faut laisser de coté l’Oculus Rift et mettre en place une coquille vide de notre driver.

Pour ce faire, nous allons partir des modèles de Drivers disponibles dans Visual Studio 2013 (Après avoir installé le WDK) comme illustré sur la figure suivante :

image

Comme on peut le voir, il existe pléthore de modèles, nous choisirons le modèle WDF (Windows Driver Frameworks), puis User Mode Driver (UMDF) qui nous permettra de développer un driver compatible Windows 7 et supérieur, pour de plus amples informations sur les versions de UMDF allez voir cette page UMDF Version History

image

Une fois le modèle choisi, un squelette de driver est créé comme illustré sur la figure suivante :

image

Cette solution contient deux projets.

  • Le projet UMDF Driver1 Package, qui comme sont nom l’indique servira à installer le driver
  • Le projet UMDF Driver1, qui est notre driver proprement dit.

A ce stade, vous pouvez d’ores et déjà compiler le driver en choisissant correctement la cible .

image

Pour ma part je cible un Windows 8.1 en 64 Bits.

Voyons rapidement et en très très gros, ce qu’il y a dans ce squelette, dans l’ordre :

Dllsup.cpp : Un driver ce n’est ni plus ni moins qu’une librairie. Nous avons donc besoin d’un point d’entrée DllMain, pour charger la librairie.

#include "internal.h"
#include "dllsup.tmh"

const CLSID CLSID_Driver =
{0x8982ab10,0x0bf3,0x4f8f,{0x92,0x62,0x6b,0x8e,0x04,0x7b,0x75,0x6e}};

HINSTANCE g_hInstance = NULL;

class CMyDriverModule :
    public CAtlDllModuleT< CMyDriverModule >
{
};

CMyDriverModule _AtlModule;

extern "C"
BOOL
WINAPI
DllMain(
    HINSTANCE hInstance,
    DWORD dwReason,
    LPVOID lpReserved
    )
{
    if (dwReason == DLL_PROCESS_ATTACH) {
        WPP_INIT_TRACING(MYDRIVER_TRACING_ID);
        
        g_hInstance = hInstance;
        DisableThreadLibraryCalls(hInstance);

    } else if (dwReason == DLL_PROCESS_DETACH) {
        WPP_CLEANUP();
    }

    return _AtlModule.DllMain(dwReason, lpReserved);
}

STDAPI
DllGetClassObject(
    __in REFCLSID rclsid,
    __in REFIID riid,
    __deref_out LPVOID FAR* ppv
    )
{
    return _AtlModule.DllGetClassObject(rclsid, riid, ppv);
}

La 1ere chose que l’on peut constater, c’est que la classe CMyDriverModule dérive de CAtlDllModuleT<CMyDriverModule> , ce qui nous indique que le Framework WDF utilise la Technologie ATL (Active Template Library, qui se base elle même sur la technologie COM)

Il est donc fortement conseillé de connaitre au minimum comment ATL et COM fonctionnent, pour comprendre le fonctionnement d’un driver UMDF.

Une fois que la librairie est chargée en mémoire, et qu’elle c’est déclarée en tant qu’objet COM, le Framework WDF, va instancier une classe qui implémente l’interface IDriverEntry et appeler ça méthode OnDeviceAdd lors de l’installation du driver.

L’interface IDriverEntry est implémentée dans le fichier Driver.cpp. dont voici un extrait :

class CMyDriver :
    public CComObjectRootEx<CComMultiThreadModel>,
    public CComCoClass<CMyDriver, &CLSID_Driver>,
    public IDriverEntry
{
public:

    CMyDriver()
    {
    }

    DECLARE_NO_REGISTRY()

    DECLARE_NOT_AGGREGATABLE(CMyDriver)

    BEGIN_COM_MAP(CMyDriver)
        COM_INTERFACE_ENTRY(IDriverEntry)
    END_COM_MAP()

//
// Public methods
//
public:

    //
    // IDriverEntry methods
    //

    virtual
    HRESULT
    STDMETHODCALLTYPE
    OnInitialize(
        __in IWDFDriver *FxWdfDriver
        )
    {
        UNREFERENCED_PARAMETER(FxWdfDriver);
        return S_OK;
    }

    virtual
    HRESULT
    STDMETHODCALLTYPE
    OnDeviceAdd(
        __in IWDFDriver *FxWdfDriver,
        __in IWDFDeviceInitialize *FxDeviceInit
        );

    virtual
    VOID
    STDMETHODCALLTYPE
    OnDeinitialize(
        __in IWDFDriver *FxWdfDriver
        )
    {
        UNREFERENCED_PARAMETER(FxWdfDriver);
        return;
    }

};

OBJECT_ENTRY_AUTO(CLSID_Driver, CMyDriver)

Note : Je ne reviens pas sur les détails d’implémentation d’un objet COM au travers des ATL, vous pourrez trouver toute l’information que vous souhaitez ici. Logiquement vous n’aurez sans doute pas à toucher à cette déclaration pour l’instant ni à connaitre son fonctionnement.

Le Framework appelle donc la méthode OnDeviceAdd() qui a pour rôle d’instancier la classe CMyDevice implémentée dans le fichier Device.cpp.

HRESULT
CMyDriver::OnDeviceAdd(
    __in IWDFDriver *FxWdfDriver,
    __in IWDFDeviceInitialize *FxDeviceInit
    )

{
    HRESULT hr = S_OK;
    CMyDevice *device = NULL;

    //
    // Create device callback object
    //
    hr = CMyDevice::CreateInstanceAndInitialize(FxWdfDriver,
                                                FxDeviceInit,
                                                &device);

    //
    // Call the device's construct method.  This
    // allows the device to create any queues or other structures that it
    // needs now that the corresponding fx device object has been created.
    //
    if (SUCCEEDED(hr))
    {
        hr = device->Configure();
    }

    return hr;
}

La classe CMyDevice, possède une méthode statique CreateInstanceandInitialize qui a pour but comme son nom l’indique de créer, d’instancier et d’initialiser un objet du Framework WDF, IWDFDevice. Elle permet également de passer au framework, un pointeur sous la forme d’un IUnknown, afin de gérer des méthodes de rappels, comme nous le verrons dans un autre billet.

Pour l’instant, il n’y aucune raison de changer ou d’ajouter quoi que ce soit à ce que nous propose le modèle.

La classe CMyDevice, implémente également la méthode Configure() qui à pour rôle quand à elle

d’instancier et d’initialiser la classe CMyIoQueue, qui aura pour but essentiel de réagir aux entrées\sorties et de notifier le driver qu’une demande est en cours.

Pour ce faire elle doit implémenter l’interface IQueueCallbackDeviceIoControlet sa méthode associée OnDeviceIoControl comme illustré dans le code suivant :

class CMyIoQueue :
    public CComObjectRootEx<CComMultiThreadModel>,
    public IQueueCallbackDeviceIoControl
{

public:

    DECLARE_NOT_AGGREGATABLE(CMyIoQueue)

    BEGIN_COM_MAP(CMyIoQueue)
        COM_INTERFACE_ENTRY(IQueueCallbackDeviceIoControl)
    END_COM_MAP()

    CMyIoQueue() :
        m_FxQueue(NULL),
        m_Device(NULL)
    {
    }

    ~CMyIoQueue()
    {
        // empty
    }

    HRESULT
    Initialize(
        __in IWDFDevice *FxDevice,
        __in CMyDevice *MyDevice
        );

    static
    HRESULT
    CreateInstanceAndInitialize(
        __in IWDFDevice *FxDevice,
        __in CMyDevice *MyDevice,
        __out CMyIoQueue**    Queue
        );

    HRESULT
    Configure(
        VOID
        )
    {
        return S_OK;
    }

    //
    // Wdf Callbacks
    //

    //    
    // IQueueCallbackDeviceIoControl
    //
    virtual
    VOID
    STDMETHODCALLTYPE
    OnDeviceIoControl(
        __in IWDFIoQueue *pWdfQueue,
        __in IWDFIoRequest *pWdfRequest,
        __in ULONG ControlCode,
        __in SIZE_T InputBufferSizeInBytes,
        __in SIZE_T OutputBufferSizeInBytes
        );

VOID
STDMETHODCALLTYPE
CMyIoQueue::OnDeviceIoControl(
    __in IWDFIoQueue *FxQueue,
    __in IWDFIoRequest *FxRequest,
    __in ULONG ControlCode,
    __in SIZE_T InputBufferSizeInBytes,
    __in SIZE_T OutputBufferSizeInBytes
    )

{
    UNREFERENCED_PARAMETER(FxQueue);
    UNREFERENCED_PARAMETER(ControlCode);
    UNREFERENCED_PARAMETER(InputBufferSizeInBytes);
    UNREFERENCED_PARAMETER(OutputBufferSizeInBytes);

    HRESULT hr = S_OK;

    TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_QUEUE, "%!FUNC! Entry");

    if (m_Device == NULL) {
        // We don't have pointer to device object
        TraceEvents(TRACE_LEVEL_ERROR,
                   TRACE_QUEUE,
                   "%!FUNC!NULL pointer to device object.");
        hr = E_POINTER;
        goto Exit;
    }

    //
    // Process the IOCTLs
    //

Exit:

    FxRequest->Complete(hr);

    TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_QUEUE, "%!FUNC! Exit");

    return;

}

Pour bien visualiser ce qui se passe lors de l’installation puis de l’utilisation de notre driver voici un petit schéma qui récapitule le tout.

image

Maintenant que notre squelette est prêt et avant de commencer à coder spécifiquement pour notre driver sensor, il faut mettre en place les outils qui vont nous permettre de déboguer et tracer ce qui se passe dans le driver.

Installation, Trace et débogue du driver.

Trace

Vous aurez remarqué, dans le code de notre squelette, qu’il fait souvent appelle à une méthode nommée TraceEvents, cette méthode n’existe pas en tant que telle, mais est créée à la volée par le système de tracing des drivers nommé WPP (Windows software trace preprocessor ).

Pour activer le tracing (qui est par défaut activé avec le modèle Visual Studio que nous venons d’utiliser) il suffit d’aller dans les propriétés de votre projet à la section WPP Tracing comme illustré sur la figure suivante :

image

D’activer Run Wpp Tracing, de scanner le fichier de configuration internal.h ou se trouve un certain nombre de paramètres propres aux tracing comme illustré dans le code suivant :

//
// Define the tracing flags.
//
// Tracing GUID - dc523bd1-c312-42da-97f9-6c7af6aa20e7
//

#define WPP_CONTROL_GUIDS                                              \\par     WPP_DEFINE_CONTROL_GUID(                                           \\par         MyDriver1TraceGuid, (dc523bd1,c312,42da,97f9,6c7af6aa20e7), \\par                                                                             \\par         WPP_DEFINE_BIT(MYDRIVER_ALL_INFO)                              \\par         WPP_DEFINE_BIT(TRACE_DRIVER)                                   \\par         WPP_DEFINE_BIT(TRACE_DEVICE)                                   \\par         WPP_DEFINE_BIT(TRACE_QUEUE)                                    \\par         WPP_DEFINE_BIT(TRACE_DDI) \\par         WPP_DEFINE_BIT(TRACE_OCULUS)                                  \\par         )                             

#define WPP_FLAG_LEVEL_LOGGER(flag, level)                                  \\par     WPP_LEVEL_LOGGER(flag)

#define WPP_FLAG_LEVEL_ENABLED(flag, level)                                 \\par     (WPP_LEVEL_ENABLED(flag) &&                                             \\par      WPP_CONTROL(WPP_BIT_ ## flag).Level >= level)

#define WPP_LEVEL_FLAGS_LOGGER(lvl,flags) \\par            WPP_LEVEL_LOGGER(flags)
               
#define WPP_LEVEL_FLAGS_ENABLED(lvl, flags) \\par            (WPP_LEVEL_ENABLED(flags) && WPP_CONTROL(WPP_BIT_ ## flags).Level >= lvl)

//
// This comment block is scanned by the trace preprocessor to define our
// Trace function.
//
// begin_wpp config
// FUNC Trace{FLAG=MYDRIVER_ALL_INFO}(LEVEL, MSG, ...);
// FUNC TraceEvents(LEVEL, FLAGS, MSG, ...);
// end_wpp
//

Ensuite il faut forcer la création d’un import en précisant dans les includes, un fichier avec l’extension tmh.

Par exemple dans notre fichier driver.cpp, vous trouverez un include “driver.tmh”,

#include "driver.tmh"

Si vous créez une nouvelle classe nommée OculusRift par exemple, dans le fichier OculusRift.cpp, il vous faudra rajouter la ligne #include “oculurift.tmh” pour avoir accès au tracing et ainsi de suite.

En fin pour visualiser les traces, DebugView n’est pas le bon outil. Il vous faut l’outil TraceView que vous trouverez dans le répertoire C:\Program Files (x86)\Windows Kits\8.1\Tools\

Vous lancez l’outil, vous avez accès à un interface minimale.

Dans le menu File, créer une nouvelle session de log

image

Activez le menu bouton Add Provider, et ajoutez le fournisseur de type PDB comme illustré sur la figure suivante :

image

Une fois fait, nous allons activer les messages que nous souhaitons tracer.

Sélectionnez le bouton Set Flags and Level, puis sur Level (et choisir le niveau de trace que vous souhaitez visualiser)

image

Voici ce que nous pouvons visualiser lors de l’installation du driver.

image

Installation.

Lorsque nous compilons notre driver, il crée en même temps un répertoire comprenant tous les éléments nécessaires pour notre package d’installation. Par exemple

C:\Articledriver\UMDF Driver1\x64\Win8.1Debug\UMDF Driver1 Package

ou nous retrouvons les éléments suivants :

image

Je ne reviens pas dessus, vous trouverez toutes les informations nécessaires dans la documentation du Kit de développement.

Néanmoins, vous aurez remarquez, que dans notre solution nous avons un fichier nommé UMDFDriver1.inf. Ce fichier contient par défaut tous les éléments nécessaire pour que le déploiement se passe correctement, pour notre squelette nous ne le toucherons donc pas.

Néanmoins, ouvrez le car il y a, à la section [Standard.NT$ARCH$] une ligne qui nous intéresse le Hardware ID Root\UMDFDriver1 que nous utiliserons pour l’installation.

[Standard.NT$ARCH$]
%DeviceName%=MyDevice_Install, Root\UMDFDriver1

Pour installer le driver, allez chercher l’outil Devcon.exe dans le répertoire C:\Program Files (x86)\Windows Kits\8.1\Tools\

Ouvrez une console en mode administrateur et tapez la ligne suivante :

Devcon /install UMDFDriver1.inf Root\UMDFDriver1

La boite de dialogue de sécurité Windows apparaît vous signalant que le driver n’est pas signé.

Installer ce pilote quand même

image

Pour supprimer le pilote

Devcon /install Root\UMDFDriver1

Déboguer

Bien arrive le moment ou il faut déboguer le driver, car les traces ne suffisent plus.

En effet, par exemple, j’ai un plantage lors de l’installation du driver, comment puis-je déboguer si l’installation et l’initialisation de mon driver et de mon périphérique n’aboutissent pas ?

Tout d’abord, il faut savoir que comme nous sommes en train de développer un driver en mode User, notre driver est chargé par un exécutable qui se nomme WUDFHost.exe. ( Architecture of UMDF)

Pour déboguer dans Visual Studio il suffit alors d’attacher le débogueur à notre exécutable, comme illustré sur la figure suivante :

image

Néanmoins, deux problèmes surviennent :

Comme vous pouvez le constater WUDFHost.exe est chargé plusieurs fois, car il existe dans mon cas de figure plusieurs drivers UMDF qui sont déjà chargés en mémoire, alors lequel choisir ?

Pour ce faire il suffit d’afficher la fenêtre Debug->Windows->Modules et vérifier que votre driver est bien en mémoire, comme illustré sur la figure suivante :

image

Le second problème est plus épineux. En effet, si le driver est déjà en mémoire, voir n’est pas en mémoire car un plantage est survenu lors de son initialisation, comment arriver à attacher le débogueur avant que cela plante ?

Pour ce faire, il existe des clés dans le registre, qui permettent d’indiquer au Framework WDF un délai avant de charger la dll (HostProcessDbgBreakOnDriverLoad) et un délai avant d’initialiser le driver (HostProcessDbgBreakOnStart), disponibles ici : 

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WUDF\Services\{193a1820-d9ac-4997-8c55-be817523f6aa}]

Une valeur de 15 en Hexa suffit largement pour avoir le temps d’attacher le débogueur.

image

Une fois que tout fonctionne correctement, c’est à dire que votre driver s’initialise correctement, n’oubliez pas de revenir à une valeur de 0, sinon tous les drivers UMDF du système mettrons 21 secondes à se charger.

Bon, voici en quelques mots comment créer un squelette de driver, comment le déployer et comment le déboguer.

Dans un second billet, nous aborderons la manière de faire pour que notre driver soit reconnu comme étant un driver pour capteurs (Sensors) et attaquable directement par les APIs Sensor de Windows.

Puis nous finirons par un troisième billet, sur la manière d’intégrer cette fois-ci le Hardware, c’est à dire l’Oculus Rift et par une petite application DirectX en moderne UI qui utilise les APis Sensors et donc implicitement notre driver. Enfin nous verrons comment cela s’intègre également à BabylonJS.

A bientôt

Eric