Raw Input für Joysticks und Gamecontroller

Hier können Artikel, Tutorials, Bücherrezensionen, Dokumente aller Art, Texturen, Sprites, Sounds, Musik und Modelle zur Verfügung gestellt bzw. verlinkt werden.
Forumsregeln
Möglichst sinnvolle Präfixe oder die Themensymbole nutzen.
Antworten
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Raw Input für Joysticks und Gamecontroller

Beitrag von Krishty »

Wir schreiben das Jahr 2013. DirectInput ist seit 13 Jahren kaum weiterentwickelt worden und wird, wie XInput, von Microsoft zum Durchsetzen marktpolitischer Interessen missbraucht. Darüber hinaus unterstützt es zwar grundlegende Funktionen von Joysticks und Game Pads, ist aber z.B. durch die Limitierung auf acht Achsen für professionelle Simulationen arg limitiert. Was soll man statt DirectInput nehmen? Die Standardantwort lautet „Raw Input!“ – aber Tutorials, wie eine Anwendung damit andere Geräte als Tastaturen und Mäuse ansteuern kann, sind äußerst rar. Darum bemühe ich mich mal:

Einführung

Falls ihr das hier implementieren wollt, braucht ihr das Windows Driver Kit. Die aktuelle Version für Windows XP bis Windows 7 gibt es hier; startet schonmal den Download. Für eure Benutzer ergeben sich keine Umstände (anders als beim DirectX SDK, wo die Laufzeitbibliothek auf dem Zielsystem installiert sein muss); wir brauchen auch nicht mehr als ein paar Header; also habt keine Angst.

Es gibt bereits einen Artikel auf CodeProject, der die Grundlagen erklärt. Weil der Artikel recht kurz gehalten ist und einfach ein paar fest verdrahtete Achsen in ein globales Array schmeißt, möchte ich hier mal eine komplette Implementierung samt Datenstruktur und Unterstützung für Kalibrierung anbieten.

Wir müssen zu allererst ein paar Begriffe und Grundlagen klären. Also: Joysticks, Gamecontroller, Lenkräder, Grafiktabletts, und was man sonst noch so an den USB-Port klatscht, nennen sich HID – Human Interface Device.

Wie ihr merkt, ist das eine sehr heterogene Menge von Geräten, und um die alle irgendwie adressieren und ansprechen zu können, hat das USB-Forum die HID-Geräteklassifikation standardisiert: Universal Serial Bus (USB) HID Usage Tables Version 1.11 vom 27.06.2001. Klingt schrecklich; ist auch dick und fett; aber wir brauchen es, weil darin alle Codes aufgelistet sind die wir brauchen, um beliebige USB-Geräte ansteuern zu können. Öffnet es schonmal in einem neuen Tab.

Grob überrissen arbeitet der Standard dabei mit zwei Klassifikationen: Die Usage Page gibt die generelle Geräteklassifikation an – ob es sich um ein normales Desktop-USB-Gerät wie einen Joystick handelt oder um dedizierte Hardware wie etwa eine komplette Flugsimulatorrampe. Die Usage adressiert dann einen speziellen Schalter der Geräteklasse – z.B. die X-Achse des Joysticks oder Löschen-Knopf eines USB-Anrufbeantworters.

Mit einer Kombination aus Usage Page und Usage können wir also jeden Schalter jedes Geräts, das USB-kompatibel ist, ansprechen. Wenn ihr im verlinkten Dokument zu Seite 27 geht, seht ihr dort ab Wert 0x30 alle Schalter aufgelistet, die unter allgemeine Desktop-Geräte fallen: X- und Y-Achsen (0x30 und 0x31) von Joysticks oder Game Pads; die Zusatztasten von Tastaturen; usw. (Mit den Werten <0x30 werden Geräte identifiziert; das brauchen wir erstmal nicht.) Wenn ihr das Ganze überfliegt findet ihr sogar Fahrradsimulatoren und ähnliche Exoten; was wir hier machen ist also serious shit.

Und mit diesen Werten wird die API funktionieren, die wir hier implementieren: Ihr werdet sagen, dass ihr vom ersten angeschlossenen Gerät, als allgemeines Desktop-USB-Gerät, die X-Achse braucht. Und was ihr zurückbekommt, wird eine float zwischen 0 und 1 sein, die euch angibt, wie weit rechts der Joystick gerade steht. Oder, dass ihr vom selben Gerät den 4. Button braucht; und ihr bekommt 1.0f zurück, falls er gedrückt ist.

Der USB-HID-Standard unterstützt übrigens weitaus mehr Schalter als die Achsen und Knöpfe, die man vom Joystick und Gamepad kennt: Beispielsweise Wippschalter; Mehrfach-Radio-Buttons; Knöpfe mit Einrastpositionen; usw usf. Die ignoriere ich hier, damit es nicht zu kompliziert wird. Wer es will, kann die API ja später nachrüsten. Ebenso wird es hier nur Polling geben, weil das einfacher ist.

Jetzt installiert erstmal das Windows Driver Kit bevor es im nächsten Beitrag an die technischen Grundlagen geht.
Zuletzt geändert von Krishty am 14.04.2013, 22:16, insgesamt 1-mal geändert.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Raw Input für Joysticks und Gamecontroller

Beitrag von Krishty »

Technische Grundlagen

Wenn sich USB-Geräte beim Betriebssystem melden, geben sie ein komplettes Protokoll über ihren Zustand ab. Wenn also ein Knopf gedrückt wird, meldet sich das Gerät bei Windows nicht mit „Knopf 3 ist gedrückt!“, sondern schickt den Zustand aller Achsen und aller Knöpfe. Das nennt sich Input Report.

Weil die Geräte – wie gesagt – sehr vielseitig sind und auch vielseitige Funktionen anbieten, gibt es keine allgemeine Datenstruktur, wo ein paar Werte drinstehen. Viel mehr kommt da ein Matsch von Werten ohne feste Struktur an. Jedoch muss erstmal bekannt sein, wie der Input Report-Matsch interpretiert werden soll. Das teilt das Gerät in seinem Input Report Protocol mit – einer Bytecode-Sprache, die aussagt, wo sich welcher Wert des Geräts in dem Matsch des Input Reports finden lässt. Raw Input und die HID API kennen dies auch unter dem Namen Preparsed Data.

Keine Angst – technischer wird es nicht. Zum Glück erledigt Windows das Interpretieren für uns. Allerdings müssen wir Speicher für die Input Reports und das Input Report Protocol selber bereitstellen und, wannimmer wir den Gerätezustand abfragen, an die entsprechenden Funktionen übergeben. Ich erwähne das, damit ihr wisst, wozu dieser Speicher und diese Funktionsaufrufe da sind.

Weiterhin gibt es Klassen. Darin gruppieren HIDs Schalter, die gleiche Eigenschaften aufweisen. Habt ihr beispielsweise ein Game Pad mit acht normalen Knöpfen und vier berührungsemfpindlichen Knöpfen, ist es wahrscheinlich, dass das Game Pad zwei Klassen meldet:
  • Schalter mit an/aus-Zustand (acht Untereinträge)
  • Schalter mit Gleitzustand (vier Untereinträge)
Leider könnten das auch ohne ersichtlichen Grund noch mehr Gruppen sein – ich weiß nicht genau, wann der Treiber ähnliche Schalter zu einer Klasse gruppiert und wann nicht. Das ist aber auch keine große Sache – ihr solltet lediglich wissen, dass Schalter gruppiert werden und dass man manchmal Schalter nur für ihre ganze Klasse auf einmal abfragen kann.

Weiterhin hat Windows die Eigenart, zwischen Achsen (also Joystick-Achsen; Schiebereglern und allem, was Gleitzustand hat) und Knöpfen (entweder an oder aus) zu unterscheiden. Das ist für uns nicht weiter tragisch und wir werden es der Einfachheit halber wegabstrahieren, sorgt aber hier und da für ein paar Zeilen Quelltext mehr.

Wie ihr merkt, ist das Ganze ziemlich umfangreich. Wir kommen deshalb auch nicht mit einer API aus, sondern brauchen vier:
  • Die Raw Input API informiert unsere Anwendung darüber, wann der Benutzer Eingaben durch ein HID vorgenommen hat.
  • Die HID API aus dem Windows Driver Kit liefert uns die Funktionen, mit denen wir Gerätezustand interpretieren können. Dadurch wissen wir, welche Werte das Gerät für Achsen und Knöpfe meldet.
  • Die WinAPI liefert uns Geräteinformationen wie z.B. den Herstellernamen. Die werden unmittelbar für Kalibrierung gebraucht, und später für Force Feedback (was aber für diesen Artikel nicht mehr geplant ist).
  • DirectInput wird genutzt um Gerätekalibrierung abzufragen (unter Windows gibt es keinen anderen Weg, weil alle Treiber darauf ausgelegt sind). Das mag wie Luxus klingen – ich kenne aber ein paar Joysticks und Gamepads, die ohne Kalibrierung quasi unbrauchbar sind. Glücklicherweise werden wir DInput nicht direkt benutzen (aufrufen) sondern nur indirekt (Registry-Einstellungen abfragen).
Die nächste Herausforderung ist, eine Architektur bereitszustellen, die den Wulst für die Anwendung halbwegs abstrahiert.
Zuletzt geändert von Krishty am 20.06.2013, 17:39, insgesamt 2-mal geändert.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Raw Input für Joysticks und Gamecontroller

Beitrag von Krishty »

Architektur

Wir bekommen unsere Eingaben von der WinAPI (durch WM_INPUT-Nachrichten) und von der HID API (als konkrete Auslenkungswerte). Darauf programmieren wir eine Schicht, die diese Eingaben derart aufbereitet, dass darüber nur eine Funktion aufgerufen werden muss, die für einen Schalter (egal, ob Achse oder Knopf) sagt, was sein aktueller Wert ist.

Wie kommen unsere Eingaben denn nun genau an?
  • Die USB-geräte entscheiden, wann Eingaben generiert werden. Ich habe sowohl Geräte gefunden, die nur senden, wenn das Gerät tatsächlich bewegt wird; als auch Geräte, die situationsunabhängig 40 Mal pro Sekunde senden.
  • Dementsprechend bedeutet die Übermittlung eines Gerätezustands nicht, dass der Zustand sich seit der letzten Übermittlung geändert hat – wer eine ereignisbasierte Eingabeverarbeitung schreiben möchte, muss das bedenken!
  • Wir bekommen nicht mitgeteilt, was sich am Gerät geändert hat (müssen also jedes Mal den gesamten Zustand abfragen).
  • Übermittelt wird bei Eingaben jedem Fenster – nachdem sich die Anwendung dafür registriert hat – eine WM_INPUT-Nachricht. Die RAWINPUT-Struktur im lParam hat in ihrem header den dwType RIM_TYPEHID stehen, falls sich die Eingaben auf ein HID beziehen.
    Diese Nachricht ist synchron (wie alle Windows-Nachrichten!) – sie spiegelt also nicht den aktuellen Gerätezustand wieder, sondern den vom Zeitpunkt, an dem das Gerät gesendet hat. Je nachdem, wie schnell die Hauptschleife der Anwendung rotiert, kann sich da beträchtlich was getan haben.
  • Da einzelne Fenster ihre WM_INPUT-Nachrichten synchron erhalten, kann sich der Eingabezustand der Geräte von Fenster zu Fenster unterscheiden – je nachdem, wie weit jedes Fenster mit seiner Eingabeverarbeitung ist. Auch das ist normal für Win32-Programmierung.
  • Die WM_INPUT-Nachricht enthält eine RAWINPUT-Struktur hinter dem Handle im lParam. Davon ist header.hDevice das Raw Input-Handle des Geräts, das die Eingaben sendet. Das unterscheidet sich leider vom Kernel-Handle, das die HID API benutzt – die beiden dürfen nicht verwechselt werden!
  • Die Nachricht enthält weiterhin den Input Report – also den gesamten Gerätezustand in Form von Bytecode – der durch die HID API (für die ihr das Windows Driver Kit installiert habt) interpretiert werden muss.
  • Dummerweise führen nun mehrere Wege nach Rom, was uns die Wahl der Waffe deutlich erschwert. Das liegt darin begründet, dass HID-Achsauslenkungen drei Werte liefern:
    • Der interne Wert ist die Bitfolge, die im Input Report steht. Hier werden nur so viele Bits allokiert wie die Hardware tatsächlich braucht: Der Rundblickschalter eines Joysticks etwa, der in acht Richtungen schauen kann, würde hier drei Bits liefern. Ein Knopf würde ein einziges Bit (gedrückt oder nicht) liefern.
    • Der logische Wert ist die 32-Bit-Erweiterung des internen Werts – das ULONG, das die HID API bei uns abliefert.
    • Der physische Wert ist die Umrechnung des logischen Werts auf die wirkliche Welt. Während etwa bei einer USB-Maus zuerst logische Werte ankommen, die angeben, welche Verschiebung der Sensor gemessen hat, wären die physischen Werte der Maus die Anzahl der Inches, die dafür auf der Tischplatte zurückgelegt wurden. Weitere Beispiele: Ein USB-Thermometer könnte logische Werte im Bereich von 0 bis 100 liefern, die physischen Werten im Bereich von -10 bis +40° C entsprächen. Der Rundblickschalter eines Joysticks hätte den logischen Umfang von 1 bis 8, aber den physischen Umfang von 0 bis 315° (360° würde wieder 0° entsprechen).
    Die Funktionen, mit denen man an Daten kommt, sind:
    • Der wohl abstrakteste Ansatz ist Hidp_GetScaledUsageValue(): Hier gibt die HID API den physischen Wert einer Achse zurück (Knöpfe können damit nicht abgefragt werden!) – im Falle unseres Thermometers also eine Temperatur in Grad Celsius, inklusive Skalierung und Erweiterung des Vorzeichen-Bits auf 32 Bits.
      Das ist für uns nicht von Nutzen, weil die Funktion die Kalibrierung überspringt. Ohne Kalibrierung sind, wie gesagt, viele Game Controller schon ab Werk nutzlos.
    • Der CodeProject-Artikel nutzt HidP_GetUsageValue() für den logischen Wert einer einzelnen Achse, und HidP_GetUsages() um an ein Array zu kommen, in welchem die Usages aller Knöpfe einer Klasse, die derzeit gedrückt sind, aufgelistet werden.
      Diese Funktionen waren meine erste Wahl, aber mittlerweile habe ich erkannt, dass sie die Sache unnötig kompliziert machen und ziemlich „schwer“ sind: Damit muss jede Achse einzeln verarbeitet werden, und die Knöpfe klassenweise. Erkannt werden die Knöpfe anhand ihrer Usage – das bedeutet, dass wir bei jedem Knopf erstmal suchen müssen, welche Usage zu welchem unserer Knopf-Objekte gehört.
    • Die am niedrigsten ansetzende Funktion ist HidP_GetData(). Sie gibt in nur einem Aufruf eine Liste aller Achsauslenkungen sowie aller gedrückten Knöpfe zurück. Dabei ist jeder Achse und jedem Knopf ein Array-Index beigelegt, der während der gesamten Ausführungszeit gleich bleibt. Mit der richtigen Datenstruktur entfällt dadurch die Suche nach den passenden Achseigenschaften.
Diese Eingaben müssen wir gemäß Geräteeigenschaften (Wertumfang jeder Achse; Kalibrierung) aufwerten und abrufbar speichern. Daraus können wir unsere Architektur ableiten:
  • Die Geräte und deren Eigenschaften sind für das gesamte System identisch. Wir können sie also während der Enumeration in einem globalen Array speichern und von dort abrufen, wann immer wir ihre Eigenschaften brauchen (z.B. das Input Report Protocol, damit die HID API weiß, wie die Daten interpretiert werden müssen, und all sowas).
  • Die Gerätezustände – also, welcher Knopf gerade gedrückt ist und welche Achse wie weit ausgelenkt ist – können sich von Fenster zu Fenster unterscheiden. Darum bekommt jedes Fenster, das Eingaben verarbeitet, seine eigene Liste von Gerätetzuständen. Wir halten diese Liste deckungsgleich mit den globalen Geräten.
Die Geräteeigenschaften aus dem ersten Punkt (die sich während der Ausführung des Programms nicht ändern – Hot-Plugging behandle ich hier nicht weil es eh nicht XP-kompatibel ist), die wir zur Eingabeverarbeitung brauchen, sind:
  • Das Geräte-Handle der Raw Input-API, damit wir wissen, welches Gerät gemeint ist, wenn eine WM_INPUT-Nachricht eintrifft.
  • Das Geräte-Handle der Win32-API, damit wir die HID API auf das Gerät ansetzen können. Der Windows NT-Kernel ist komplett dateibasiert, darum sind auch Geräte als Datei-Handles realisiert – wer es eine Nummer härter mag, kann WM_INPUT auch überspringen und direkt via ReadFile() Input Reports auslesen und via WriteFile() Force Feedback zurückschreiben ;)
  • Den menschenlesbaren Namen des Geräts. Zum einen sehr wertvoll für’s Debugging, zum anderen wird euch der Anwender dafür danken, falls ihr einen Kalibrierungsbildschirm einbaut.
  • Eine Auflistung aller Achsen, die in dem Gerät vorkommen. Das Speichern der Klassen würde nicht reichen: Achsen derselben Klasse können sich von einander unterscheiden (etwa, wenn sie unterschiedlich kalibriert sind). Weiterhin weist DirectInput einzelnen Achsen und Knöpfen menschenlesbare Namen zu, die wir für Debug-Ausgabe und Kalibrierungsbildschirme ebenfalls speichern („combined pedals“ ist jedenfalls aussagekräftiger als „up0001u0032“ – insbesondere, wenn der Anwender absichtlich Geräteachsen vertauscht hat damit sie in bestimmten Spielen besser funktionieren!).
    Darum speichern wir nochmal alle Achsen mit erweiterten Eigenschaften wie Kalibrierung und Namen einzeln …
  • … und alle Knöpfe ebenfalls.
Pro Fenster speichern wir dann im Gerätezustand:
  • Den letzten Input Report des Geräts. (Dann müssen wir ihn nicht jedes Mal erneut allokieren.)
  • Die Zustandsliste, die Hidp_GetData() zurückgegeben hat. (Wieder: Um Laufzeitallokationen zu sparen.)
  • Ein Array mit der aktuellen Auslenkung aller Achsen und Knöpfe, sortiert nach den Indizes, mit denen die HID API arbeitet. Weiterhin ist zu jedem Wert vermerkt, ob es sich um eine Achse oder einen Knopf handelt; falls um die Achse, wird zusätzlich auf die Kalibrierungsinformationen der Geräteeigenschaften verwiesen. Weil dies das Array ist, in dem die Anwendung Werte nachschlagen wird, speichern wir außerdem Usage Page und Usage, zu denen der Wert gehört.
So. Beim nächsten Mal gibt es dann endlich Quelltext; versprochen!

Ihr dürft bis dahin alles, was es an Fragen und Kritik gibt, hier posten.
Zuletzt geändert von Krishty am 28.04.2013, 00:56, insgesamt 1-mal geändert.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Raw Input für Joysticks und Gamecontroller

Beitrag von Krishty »

Datenstrukturen

Wir verteilen die Datenstrukturen auf drei Header:
  • HID.hpp enthält Deklarationen der Geräteeigenschaften. Wenn das umgebende Programm einzelne Schalter abfragt können die uns egal sein, darum wird dieser Header nur intern eingebunden.
  • HIDState.hpp enthält Deklarationen der Gerätezustände. Auch die bekommt das umgebende Programm nie zu Gesicht.
  • RawInputHIDWrapper.hpp enthält die API, mit der das umgebende Programm arbeitet.
Los geht’s mit HID.hpp:

    #include <cstdint>
    #define WIN32_LEAN_AND_MEAN
    #include <Windows.h>
    #include <string>
    #include <vector>
    extern "C" {
    #    include <hidsdi.h>
    }

    struct HID {

        struct Axis {
            //
Usage Page und Usage der Achse, z. B. „generic (0001) / slider (0036)“.
            USAGE                  usagePage;
            USAGE                  usage;
            //
Index der Achse in dem Array, das Hidp_GetData() füllt.
            USHORT                 index;
            //
Umfang der logischen Werte dieser Achse. Liegt der tatsächliche Wert außerhalb dieses
            //  Bereichs, müssen wir annehmen, dass die Achse „nicht gesetzt“ ist. Das passiert z. B.
            //  bei Rundblickschaltern, wenn man sie nicht drückt.
            // Die HID API behandelt diese Werte durchgängig unsigned, aber die USB-HID-Spezifikation
            //  erinnert ausdrücklich, dass hier negative Werte auftreten können.
            int32_t                logicalMinimum;
            int32_t                logicalMaximum;
            //
Ob wir DirectInput-Kalibrierungsdaten für diese Achse gefunden haben.
            bool                   isCalibrated;
            //
Durch DirectInput-Kalibrierung bestimmter Bereich der Achse, falls sie kalibriert ist.
            int32_t                logicalCalibratedMinimum;
            int32_t                logicalCalibratedMaximum;
            //
Durch DirectInput-Kalibrierung bestimmte Mittelstellung, falls die Achse kalibriert ist.
            int32_t                logicalCalibratedCenter;
            //
Physischer Bereich der Achse. Das verarbeiten wir hier nicht, sondern leiten es an die
            //  Anwendung weiter, damit die damit arbeiten kann.
            float                  physicalMinimum;
            float                  physicalMaximum;
            //
Der Name, den der Treiber der Achse gegeben hat; z. B. „combined pedals“.
            std::wstring           name;
        };

        struct Button {
            //
Usage Page und Usage des Knopfes, z. B. „buttons (0009) / secondary (0002)“.
            USAGE                  usagePage;
            USAGE                  usage;
            //
Index des Knopfs in dem Array, das Hidp_GetData() füllt.
            USHORT                 index;
            //
Der Name, den der Treiber dem Knopf gegeben hat; bei PlayStation-Controllern z. B. X
            //  oder Square.
            std::wstring           name;
        };

        //
Konstruktor; lädt das HID hinter dem gegebenen Raw Input Handle.
        explicit HID(HANDLE);

        ~HID() throw();

        //
Das Handle, über das die Raw Input API das Gerät anspricht.
        HANDLE                 rawInputHandle;
        //
Das Handle, über das der NT-Kernel das Gerät anspricht.
        HANDLE                 ntHandle;
        //
Herstellername und Produktbezeichnung des Geräts. (Wer will, kann den statisch allokieren –
        //  die MSDN schreibt:
        //       „For USB devices, the maximum string length is 126 wide characters (not including the
        //        terminating NULL character).
        // Damit wäre der größtmögliche Platzverbrauch 255 Buchstaben.
        std::wstring           name;
        //
Input Report Protocol. Diese Datenstruktur ist nirgends definiert; sie hängt vom Gerät ab.
        PHIDP_PREPARSED_DATA   toInputReportProtocol;
        //
Die Größe eines einzelnen Input Reports. Hierher wissen die einzelnen HID-Zustandsobjekte,
        //  wie viel Speicher sie zur Verfügung stellen müssen.
        ULONG                  sizeOfInputReport;
        //
Eigenschaften aller Achsen des Geräts.
        std::vector<Axis>      axes;
        //
Eigenschaften aller Knöpfe des Geräts.
        std::vector<Button>    buttons;
    private:
        //
Verhindert, dass diese Klasse kopiert oder zugewiesen wird, weil sonst u. U. die Handles
        //  leaken würden. Kann nachbessern, wer will.
        HID(HID const &);
        HID & operator = (HID const &);
    };

    //
Alle HIDs, die an dieses System angeschlossen sind.
    extern std::vector<HID *> theHIDs;

Weiter mit HIDState.hpp:

    #include "HID.hpp"

    class HIDState {
    public:

        //
Diese Daten über einen Schalter geben wir der Anwendung:
        struct Value {
            float relativeValue; //
0 bis 1
            // Es gibt noch mehr Eigenschaften physischer Werte zu beachten, aber für die meisten Fälle
            //  reichen diese beiden aus:
            float physicalMinimum;
            float physicalMaximum;
        };

        //
Konstruktor. Lauscht dem gegebenen Gerät.
        explicit HIDState(HID &);

        //
Gibt das Raw Input Handle des Geräts zurück. Nötig, damit wir wissen, wann sich eine
        //  WM_INPUT-Nachricht auf dieses Gerät bezieht.
        HANDLE rawInputHandle() const;

        //
Prüft, ob das Gerät eine bestimmte Achse oder einen bestimmten Knopf anbietet.
        bool has(USAGE usagePage, USAGE usage) const;

        //
Gibt den Wert einer bestimmten Achse oder eines bestimmten Knopfes zurück. Falls die Achse in
        //  Benutzung ist, liegt der Wert zwischen 0 und 1. Falls der Knopf gedrückt ist, ist der Wert
        //  1. Sonst ist der Wert NaN (not a number).
        Value const value(USAGE usagePage, USAGE usage) const;

        //
Verarbeitet Raw Input einer WM_INPUT-Nachricht.
        void update(HRAWINPUT);

    private:

        //
Zustand einer Achse oder eines Knopfes.
        struct State {
            //
Wie immer etwas, womit wir den Schalter identifizieren können.
            USAGE                   usagePage;
            USAGE                   usage;
            //
Der neueste logische Wert, der uns für dieses Gerät mitgeteilt wurde.
            int32_t                 value;
            //
Falls sich der Wert auf eine Achse bezieht, ein Zeiger auf die Eigenschaften der Achse in
            //  den HID-Eigenschaften. Für Knöpfe immer 'nullptr'.
            HID::Axis *             toAxisPropertiesOrNullIfButton;
        };

        HID &                   myHID;
        //
Puffer für den Input Report, damit wir ihn nicht bei jeder Eingabe neu allokieren müssen.
        std::vector<BYTE>       myInputReportScratch;
        //
Puffer für Hidp_GetData().
        std::vector<HIDP_DATA>  myStateScratch;
        //
Zustand aller Achsen und Knöpfe des HIDs.
        std::vector<State>      myStates;
    };


Schließlich die öffentliche Schnittstelle des Wrappers in RawInputHIDWrapper.hpp:

    #define WIN32_LEAN_AND_MEAN
    #include <Windows.h>
    #include <hidusage.h>
    #include <vector>

    class HIDState;

    //
Diese Klasse sollte pro Fenster, das Joysticks oder Game Controller verarbeiten will, einmal
    //  instanziert werden.
    class RawInputHIDWrapper {
    public:

        //
Lauscht allen HIDs, die dem Prozess bekannt sind.
        RawInputHIDWrapper();

        //
Das bekommt die Anwendung, wenn sie nach einer Achse oder einem Schalter fragt:
        struct Value {
            //
Hilft, das HID zu identifizieren, von dem die Eingabe stammt, wenn mehr als eines
            //  angeschlossen ist.
            HIDState const *    toHID;
            float               relativeValue; // 0 bis 1
            //
Es gibt noch mehr Eigenschaften physischer Werte zu beachten, aber für die meisten Fälle
            //  reichen diese beiden aus:
            float               physicalMinimum;
            float               physicalMaximum;
        };

        //
Prüft, ob eines der Geräte eine bestimmte Achse oder einen bestimmten Knopf anbietet.
        bool has(USAGE usagePage, USAGE usage) const;

        //
Wie HIDState::value(), nur mit einem zusätzlichen Zeiger zur Identifikation des HIDs.
        Value const value(USAGE usagePage, USAGE usage) const;

        //
Verarbeitet Raw Input einer WM_INPUT-Nachricht.
        void update(
            HANDLE device, //
aus dem Header der WM_INPUT-Nachricht
            HRAWINPUT lParam
        );

    private:
        std::vector<HIDState> myHIDStates;
    };


    void enumerateHIDs();

Beim nächsten Mal wird dann hoffentlich endlich was Ausführbares geschrieben.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Raw Input für Joysticks und Gamecontroller

Beitrag von Krishty »

HID-Enumeration

Was jetzt kommt, hasse ich: Nämlich ein großer, großer Block Quelltext, den ich nicht durch viel mehr erklären kann als durch „weil es nunmal so ist“. Man muss eben alle diese Funktionen in dieser Reihenfolge auf diese Art aufrufen weil es keinen anderen Weg gibt, an die HIDs zu kommen. Füllen wir als HID.cpp:

    #include "HID.hpp"

    #include <dinput.h>
    #include <dinputd.h>

    #include <cassert>


Wo wir gerade dabei sind: Bitte fügt den Abhängigkeiten des Projekts hid.lib aus dem Windows Driver Kit hinzu!

Unser erster Anlaufpunkt ist GetRawInputDeviceList(). Diese Funktion spuckt uns eine Liste aller Mäuse, Tastaturen, und HIDs aus, die derzeit an das System angeschlossen sind. Ausgabe ist eine Liste von RAWINPUTDEVICELIST-Datensätzen, die in ihrem hDevice-Attribut das Raw Input-Handle des Geräts aufführen.

    std::vector<HID *> theHIDs;

    void enumerateHIDs() {
        assert("enumerateHIDs() must be called only once" && theHIDs.empty());

        //
Erster Schritt: abfragen, wie viele HIDs angeschlossen sind.
        UINT expectedNumberOfHIDs = 0;
        if(0 != GetRawInputDeviceList(nullptr, &expectedNumberOfHIDs, sizeof(RAWINPUTDEVICELIST))) {
            return; //
Keine HIDs angeschlossen oder kein Raw Input verfügbar!
        }

        //
Zweiter Schritt: HID-Beschreibungen abfragen.
        std::vector<RAWINPUTDEVICELIST> hidDescriptors(expectedNumberOfHIDs);
        if(expectedNumberOfHIDs != GetRawInputDeviceList(&*hidDescriptors.begin(), &expectedNumberOfHIDs, sizeof(RAWINPUTDEVICELIST))) {
            return; //
Fehler!
        }

        //
Die enumerierten HIDs initialisieren. Dabei kann von unbekannten Achsen und Knöpfen über kaputte Registry-Einträge bis
        //  hin zu Treiberproblemen alles mögliche schiefgehen, darum sollte jeder Durchlauf isoliert stattfinden.
        for(auto const & hidDescriptor : hidDescriptors) {
            try {
                if(RIM_TYPEHID == hidDescriptor.dwType) { //
uns interessieren nur Mäuse, Joysticks, usw.
                    theHIDs.push_back(new HID(hidDescriptor.hDevice));
                }
            } catch(char const * error) { //
So schmeißen wir unsere Fehlermeldungen
                OutputDebugStringA(error);
                //
Mit dem nächsten Gerät fortfahren
            } catch(...) { // Pokémon Programming: Gotta catch 'em all!
                // Mit dem nächsten Gerät fortfahren
            }
        }

    }


Ein wesentlich dickerer Brocken ist der HID-Konstruktor. Dort müssen wir folgendes erledigen:
  • Das Handle erhalten, über das der NT-Kernel mit dem Gerät kommuniziert.
  • Herstellername und Produktbezeichnung des Geräts abfragen.
  • Das Input Report Protocol des Geräts abfragen und speichern.
  • Alle Achsen enumerieren.
    • Namen und DirectInput-Kalibrierungsinformationen für diese Achse aus der Registry laden.
    • DirectInput spricht die HIDs nicht – wie wir es tun – über Usage Page und Usage an, sondern über Indizes: Achse 0; Achse 1; …. Es werden sieben Achsen unterstützt (das ist ja Microsofts Argument, auf XInput umzusteigen: Der XBox-Controller hat zu viele Achsen für DirectInput!); und wenn DirectInput in der Kalibrierung von der vierten Achse spricht, wissen wir nicht, ob das die Z-Achse, das Ruder, das Gaspedal, oder die X-Rotationsachse ist. Das ist knifflig, aber ich erkläre es genau, sobald wir am zuständigen Quelltext arbeiten.
  • Alle Knöpfe enumerieren. (Auch hier müssen wir wieder DirectInput-Zuordnungen berücksichtigen.)
  • Logging, damit Debugging nicht komplett zur Hölle wird.
Also beginnen wir mit dem größten Brocken dieses Tutorials: Dem Konstruktor der HID-Klasse in HID.cpp.

    HID::HID(
        HANDLE const itsRawInputHandle
    )
        : rawInputHandle(itsRawInputHandle)
    {

        //
Jeder folgende Quelltext kommt hierher …

    }

Unter Windows sind auch Geräte als Dateien realisiert, die man öffnen und schließen kann. Wenn wir an das Handle kommen wollen, mit dem der NT-Kernel das HID ansteuert, müssen wir das durch einen Dateipfad tun. Den liefert uns die Raw Input API durch GetRawInputDeviceInfo() mit RIDI_DEVICENAME.

Pfade können (inklusive abschließender Null) eine Maximallänge von 260 Buchstaben haben (MAX_PATH). Da wir die Unicode-Version benutzen, könnte dem Pfad ein \\?\ vorangestellt sein. Die Maximallänge des Gerätepfads inklusive Null ist also 264 Buchstaben:

        wchar_t pathBuffer[260 + 4];
        auto pathsLength = UINT(sizeof pathBuffer / sizeof pathBuffer[0]);
        pathsLength = GetRawInputDeviceInfoW(itsRawInputHandle, RIDI_DEVICENAME, pathBuffer, &pathsLength);
        if(sizeof pathBuffer / sizeof pathBuffer[0] < pathsLength) { //
Negative UINTs werden sehr groß positiv
            throw "invalid HID: could not retrieve its path";
        }


Jetzt bekommen wir es mit einem Windows XP-spezifischen Problem zu tun: Der zweite Buchstabe des Pfads ist manchmal ? statt \. Wer sich für die Gründe interessiert, kann sich die StackOverflow-Frage GetRawInputDeviceInfo returns wrong syntax of USB HID device name in Windows XP durchlesen.

        pathBuffer[1] = L'\\';

Dann kann das Kernel-Handle des HIDs geöffnet werden:

        ntHandle = CreateFileW(
            pathBuffer, 0u,
            FILE_SHARE_READ | FILE_SHARE_WRITE, //
wir können anderen Prozessen nicht verbieten, das HID zu benutzen
            nullptr,
            OPEN_EXISTING,
            0u, nullptr
        );
        if(INVALID_HANDLE_VALUE == ntHandle) {
            throw "invalid HID: could not open its handle";
        }


Jetzt fragen wir die HID API nach dem Namen des Geräts – falls irgendwas schief läuft (und dafür kommen noch genügend Möglichkeiten!), wissen Programmierer und Benutzer zumindest, welches Gerät schuld ist.

Fehler sehe ich dabei als nicht kritisch an: Man kann ein Gerät auch benutzen, wenn man nicht weiß, wie es heißt. Falls ihr das anders seht, werft eine Ausnahme (aber vergesst nicht, vorher ntHandle freizugeben!).

Wer wissen will, warum ich hier mit einem Array von 255 Buchstaben arbeite, muss die Deklaration von name im vorherigen Post nachlesen.

        {
            wchar_t nameBuffer[255];
            if(FALSE == HidD_GetManufacturerString(ntHandle, nameBuffer, 127)) {
                wcscpy(nameBuffer, L"(unknown)");
            } else {
                auto manufacturerLength = wcslen(nameBuffer);
                nameBuffer[manufacturerLength++] = ' ';
                HidD_GetProductString(ntHandle, nameBuffer + manufacturerLength, ULONG(255 - manufacturerLength));
            }
            name = nameBuffer;
            //
Hier solltet ihr jetzt den Namen loggen oder so …
        }

Okayokay; die leichtesten Sachen haben wir hinter uns. Nun wird es etwas schwieriger. Damit wir im Fehlerfall das bereits offene Kernel-Handle wieder freigeben, beginnen wir nun einen try-catch-Block:

        try {

            //
Jeder folgende Quelltext kommt hierher …

        } catch(...) {
            CloseHandle(ntHandle);
            throw;
        }


Als nächstes greifen wir uns das Input Report Protocol des HIDs durch HidD_GetPreparsedData():

            if(FALSE == HidD_GetPreparsedData(ntHandle, &toInputReportProtocol)) {
                throw "invalid HID: no input report protocol";
            }


Weil diese Daten von der HID API allokiert werden und nach ihrer Benutzung wieder freigegeben werden müssen, kommt der Rest in einen weiteren (aber in den letzten!) try-catch-Block:

            try {

                //
Jeder folgende Quelltext kommt hierher …

            } catch(...) {
                HidD_FreePreparsedData(toInputReportProtocol);
                throw;
            }


Nun können wir endlich HidP_GetCaps() aufrufen: Das füllt uns eine HIDP_CAPS-Instanz, die uns mitteilt, wie viele Klassen von Achsen und Knöpfen das HID anbietet; aber auch, wie groß der Datenblock ist, den wir pro Input Report zu erwarten haben:

                HIDP_CAPS capabilities;
                if(HIDP_STATUS_SUCCESS != HidP_GetCaps(toInputReportProtocol, &capabilities)) {
                    throw "invalid HID: no capabilities";
                }

                sizeOfInputReport = sizeof(RAWINPUTHEADER) + sizeof(RAWHID) + capabilities.InputReportByteLength;


Nun holen wir uns alle Klassen von Achsen und Knöpfen:

                std::vector<HIDP_BUTTON_CAPS> buttonClasses(capabilities.NumberInputButtonCaps);
                if(HIDP_STATUS_SUCCESS != HidP_GetButtonCaps(
                    HidP_Input,
                    &*buttonClasses.begin(), &capabilities.NumberInputButtonCaps,
                    toInputReportProtocol
                )) {
                    throw "invalid HID: could not retrieve its button classes";
                }

                std::vector<HIDP_VALUE_CAPS> axisClasses(capabilities.NumberInputValueCaps);
                if(HIDP_STATUS_SUCCESS != HidP_GetValueCaps(
                    HidP_Input,
                    &*axisClasses.begin(), &capabilities.NumberInputValueCaps,
                    toInputReportProtocol
                )) {
                    throw "invalid HID: could not retrieve its axis classes";
                }


Jede dieser Klassen kann eine oder mehrere Usages abdecken. Dafür stellt die HID API zwei getrennte Datenstrukturen bereit: HIDP_VALUE_CAPS stellt etwa ein Attribut BOOLEAN IsRange; und falls das TRUE ist, muss man alles weitere aus der Unterstruktur Range lesen, sonst aus NotRange. Wir werden nun über alle Klassen iterieren und alle zu Ranges (Ende inklusiv!) konvertieren. An jeder Stelle, wo wir die Klassen verarbeiten, wird dann aus

    if(IsRange) {
        for(auto i = Range.UsageMin; i <= Range.UsageMax; ++i) {
            doSomething(i);
        }
    } else {
        doSomething(NotRange.Usage);
    }


das viel einfachere

    for(auto i = Range.UsageMin; i <= Range.UsageMax; ++i) {
        doSomething(i);
    }


und spart uns eine riesige Menge Quelltext. Wir kombinieren das damit, die tatsächliche Anzahl von Knöpfen und Achsen zu zählen:

                size_t numberOfButtons = 0;
                for(auto & currentClass : buttonClasses) {
                    if(currentClass.IsRange) {
                        numberOfButtons += currentClass.Range.UsageMax - currentClass.Range.UsageMin + 1u;
                    } else {
                        currentClass.Range.UsageMin =
                        currentClass.Range.UsageMax = currentClass.NotRange.Usage;
                        currentClass.Range.DataIndexMin =
                        currentClass.Range.DataIndexMax = currentClass.NotRange.DataIndex;
                        currentClass.IsRange = 1;
                        ++numberOfButtons;
                    }
                }

                size_t numberOfAxes = 0;
                for(auto & currentClass : axisClasses) {
                    if(currentClass.IsRange) {
                        numberOfAxes += currentClass.Range.UsageMax - currentClass.Range.UsageMin + 1u;
                    } else {
                        currentClass.Range.UsageMin =
                        currentClass.Range.UsageMax = currentClass.NotRange.Usage;
                        currentClass.Range.DataIndexMin =
                        currentClass.Range.DataIndexMax = currentClass.NotRange.DataIndex;
                        currentClass.IsRange = 1;
                        ++numberOfAxes;
                    }
                }


Jetzt kommen wir zum härtesten Brocken der Initialisierung: Dem Laden von DirectInputs Kalibrierungsdaten.

DirectInput arbeitet mit einem Satz von acht Achsen: X, Y, Z, RX, RY, RZ, Slider, Slider 2. Davon wird die letzte, Slider 2, ignoriert:
Special Case Mappings (http://msdn.microsoft.com/en-us/library/windows/hardware/ff543445) hat geschrieben:As you can see, there is no differentiation between slider 0 and slider 1 in the DirectInput interfaces. This means that, when overriding axis 6 or 7, you always override the first available slider. If slider 0 is not yet defined as an axis and you map an axis to 7, slider 0 is overridden.
Es verbleiben also sieben Achsen.

Welche Usage Page und Usage an welche Achse gebunden ist, speichert DirectInput in der Registry unter HKEY_CURRENT_USER\System\CurrentControlSet\Control\MediaProperties\PrivateProperties\Joystick\OEM\VID_xxxx&PID_xxxx\Axes\x (wobei VID die vierstellige hexadezimale Herstellerkennung (Vendor ID) des HIDs angibt; PID seine Produktkennung; und das letzte x ist der dezimale Index der Achse). Das Format ist dabei eine DIOBJECTATTRIBUTES-Struktur.

Jetzt wird es leider noch kniffliger: Falls das HID keine Z-Achse anbietet, aber einen Schieberegler, dann landet letzterer auf dem Platz der Z-Achse. Beispiel:
  • Ein Joystick hat die Achsen X, Y, und Slider.
  • Der Anwender hat keine Achsen überschrieben.
  • Die Kalibrierung für X und Y findet sich unter Index 0 und 1 (ganz einfach).
  • DirectInput wird Slider automatisch auf Z verschieben, weil kein Z existiert.
  • Die Kalibrierung für Slider (und auch der zugehörige Name) befinden sich nicht am Index 6, sondern am Index 2.
Wenn das HID installiert wird, oder der Anwender den Kalibrierungsbildschirm ausführt, schreibt DirectInput die Kalibrierungsdaten in die Registry. Das Format ist dabei DIOBJECTCALIBRATION. Implementieren wir also erst einmal die Achszuordnung:

                struct MappingAndCalibration {
                    WORD                 usagePage; //
Null falls unbenutzt
                    WORD                 usage;
                    bool                 isCalibrated;
                    DIOBJECTCALIBRATION  calibration;
                    wchar_t              name[32];
                } dInputAxisMapping[7] = { }; //
Nullinitialisierung

                for(auto const & currentClass : axisClasses) {
                    if(HID_USAGE_PAGE_GENERIC != currentClass.UsagePage) {
                        continue;
                    }

                    auto const firstUsage = currentClass.Range.UsageMin;
                    auto const lastUsage = currentClass.Range.UsageMax;
                    for(WORD currentUsage = firstUsage; currentUsage <= lastUsage; ++currentUsage) {

                        auto const index = unsigned(currentUsage - HID_USAGE_GENERIC_X); // 0
für X, 1 für Y, …, 6 für Slider
                        if(index < 7) { // Nur ein Test nötig weil unsigned
                            dInputAxisMapping[index].usagePage = HID_USAGE_PAGE_GENERIC;
                            dInputAxisMapping[index].usagePage = currentUsage;
                        }

                    }
                }


                if(0 == dInputAxisMapping[2].usagePage) {
                    //
Hier Debug-Ausgabe, dass der Slider nun auf Z gemappt ist …
                    dInputAxisMapping[2] = dInputAxisMapping[6];
                    dInputAxisMapping[6].usagePage =
                    dInputAxisMapping[6].usage     = 0;
                }

               
Falls das HID eine bestimmte Achse anbietet, steht sie nun unter ihrer Standard-DirectInput-Index in dInputAxisMapping. Wir wiederholen das für Knöpfe. Glaubt man dem Wikipedia-Artikel über DirectInput, werden 128 Knöpfe unterstützt (die meisten Controller haben nicht mehr als acht).

                struct Mapping {
                    WORD    usagePage; //
Null falls unbenutzt
                    WORD    usage;
                    wchar_t name[32];
                } dInputButtonMapping[128] = { }; //
Nullinitialisierung

Diese Standardzuordnung wird nun von der in der Registry gespeicherten überschrieben. Weil die Registry als externe Informationsquelle unzuverlässig ist, und eine kaputte Kalibrierung das Spiel nicht abschießen sollte, behandle ich hier wieder alle Fehler als nicht-kritisch.

Um die Registry-Einträge lesen zu können, werden zuerst Herstellerkennung und Produktkennung des HIDs benötigt. Die liefert HidD_GetAttributes():

                HIDD_ATTRIBUTES vendorAndProductID;
                if(FALSE != HidD_GetAttributes(ntHandle, &vendorAndProductID)) {

                    wchar_t path[128] = L"System\\CurrentControlSet\\Control\\MediaProperties\\PrivateProperties\\Joystick\\OEM\\VID_????&PID_????\\Axes\\?";
                    path[84] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >> 12) & 0xF]);
                    path[85] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >>  8) & 0xF]);
                    path[86] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >>  4) & 0xF]);
                    path[87] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >>  0) & 0xF]);
                    path[93] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >> 12) & 0xF]);
                    path[94] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >>  8) & 0xF]);
                    path[95] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >>  4) & 0xF]);
                    path[96] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >>  0) & 0xF]);

                    for(size_t i = 0; i < sizeof dInputAxisMapping / sizeof dInputAxisMapping[0]; ++i) {
                        path[103] = wchar_t('0' + i);

                        HKEY key = nullptr;
                        if(0 != RegOpenKeyExW(HKEY_CURRENT_USER, path, 0, KEY_READ, &key)) {
                            continue; //
Die Achse wurde nicht überschrieben; nächste prüfen!
                        }

Viele Treiber weisen den HID-Achsen einen menschenlesbaren Namen zu; z.B. combined pedals oder throttle. Den nehmen wir direkt mit, wenn wir schonmal hier sind. Beachtet, dass Strings aus der Registry nicht zwingend nullterminiert sind!

                        {
                            DWORD valueType = REG_NONE;
                            DWORD valueSize = 0;
                            RegQueryValueExW(key, L"", nullptr, &valueType, nullptr, &valueSize);
                            if(REG_SZ == valueType && sizeof dInputAxisMapping.name > valueSize) {
                                RegQueryValueExW(key, L"", nullptr, &valueType, LPBYTE(dInputAxisMapping.name), &valueSize);
                                //
Der Name wurde bereits bei der Erzeugung genullt; kein Grund, die Null manuell anzufügen.
                            }
                        }

                        DIOBJECTATTRIBUTES mapping;
                        DWORD              valueType = REG_NONE;
                        DWORD              valueSize = 0;
                        RegQueryValueExW(key, L"Attributes", nullptr, &valueType, nullptr, &valueSize);
                        if(REG_BINARY == valueType && sizeof mapping == valueSize) {

                            RegQueryValueExW(key, L"Attributes", nullptr, &valueType, LPBYTE(&mapping), &valueSize);
                            if(0x15 > mapping.wUsagePage) { //
Gültige Usage Page?
                                dInputAxisMapping.usagePage = mapping.wUsagePage;
                                dInputAxisMapping.usage     = mapping.wUsage;
                            }

                            //
Hier solltet ihr Debug-Informationen ausgeben um das Ergebnis zu kontrollieren …
                        }

                        RegCloseKey(key);
                    } // for
jede Achse

Und weil das so einfach war, wiederholen wir es direkt mit Knöpfen!

                    wcscpy(path + 98, L"Buttons\\???");
                    for(size_t i = 0u; i < sizeof dInputButtonMapping / sizeof dInputButtonMapping[0]; ++i) {
                        if(i >= 100) { //
Dreistelliger Name?
                            path[106] = wchar_t('0' + i / 100u);
                            path[107] = wchar_t('0' + i % 100u / 10u);
                            path[108] = wchar_t('0' + i % 10u);
                            path[109] = wchar_t('\0');
                        } else if(i >= 10u) { //
Zweistelliger Name?
                            path[106] = wchar_t('0' + i / 10u);
                            path[107] = wchar_t('0' + i % 10u);
                            path[108] = wchar_t('\0');
                        } else { //
Einstelliger Name?
                            path[106] = wchar_t('0' + i);
                            path[107] = wchar_t('\0');
                        }

                        HKEY key = nullptr;
                        if(0 != RegOpenKeyExW(HKEY_CURRENT_USER, path, 0u, KEY_READ, &key)) {
                            continue;
                        }

                        {
                            DWORD valueType = REG_NONE;
                            DWORD valueSize = 0;
                            RegQueryValueExW(key, L"", nullptr, &valueType, nullptr, &valueSize);
                            if(REG_SZ == valueType && sizeof dInputButtonMapping.name > valueSize) {
                                RegQueryValueExW(key, L"", nullptr, &valueType, LPBYTE(dInputButtonMapping.name), &valueSize);
                            }
                        }

                        DIOBJECTATTRIBUTES mapping;
                        DWORD              valueType = REG_NONE;
                        DWORD              valueSize = 0;
                        RegQueryValueExW(key, L"Attributes", nullptr, &valueType, nullptr, &valueSize);
                        if(REG_BINARY == valueType && sizeof mapping == valueSize) {

                            RegQueryValueExW(key, L"Attributes", nullptr, &valueType, LPBYTE(&mapping), &valueSize);
                            if(0x15 > mapping.wUsagePage) { //
Gültige Usage Page?
                                dInputButtonMapping.usagePage = mapping.wUsagePage;
                                dInputButtonMapping.usage     = mapping.wUsage;
                            }

                            // Nochmal Debug-Informationen!
                        }

                        RegCloseKey(key);
                    } // for
jeden Knopf

Und nun der finale Registry-Akt: Die eigentlichen Kalibrierungsdaten. Wir finden sie unter HKEY_CURRENT_USER\System\CurrentControlSet\Control\MediaProperties\PrivateProperties\DirectInput\VID_xxxx&PIDxxxx\Calibration\0\Type\Axes\. Bedenkt: Den ganzen Wirrwarr mit den Achszuordnungen haben wir auf uns genommen, damit wir die Kalibrierungsschlüssel den HID-Achsen zuordnen können. Existieren keine Kalibrierungsdaten für eine Achse, ist die entsprechende Achszuordnung wertlos für uns. In diesem Fall markieren wir sie als ungültig, indem usagePage und usage genullt werden.

                    wcscpy(path, L"System\\CurrentControlSet\\Control\\MediaProperties\\PrivateProperties\\DirectInput\\VID_????&PID_????\\Calibration\\0\\Type\\Axes\\?");
                    path[83] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >> 12) & 0xF]);
                    path[84] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >>  8) & 0xF]);
                    path[85] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >>  4) & 0xF]);
                    path[86] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >>  0) & 0xF]);
                    path[92] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >> 12) & 0xF]);
                    path[93] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >>  8) & 0xF]);
                    path[94] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >>  4) & 0xF]);
                    path[95] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >>  0) & 0xF]);

                    for(size_t i = 0; i < sizeof dInputAxisMapping / sizeof dInputAxisMapping[0]; ++i) {
                        path[121] = wchar_t('0' + i);

                        bool success = false;

                        HKEY key = nullptr;
                        if(0 == RegOpenKeyExW(HKEY_CURRENT_USER, path, 0u, KEY_READ, &key)) {

                            auto & calibration = dInputAxisMapping.calibration;
                            DWORD  valueType   = REG_NONE;
                            DWORD  valueSize   = 0;
                            RegQueryValueExW(key, L"Calibration", nullptr, &valueType, nullptr, &valueSize);
                            if(REG_BINARY == valueType && sizeof calibration == valueSize) {

                                if(0 == RegQueryValueExW(key, L"Calibration", nullptr, &valueType, LPBYTE(&calibration), &valueSize)) {
                                    dInputAxisMapping.isCalibrated = true;
                                }

                            }

                            RegCloseKey(key);
                        }

                    } // for
jede Achse

                } // if Herstellerkennung und Produktkennung gefunden

Jetzt haben wir alle Daten über das Gerät, die wir brauchen. Der Konstruktor wird damit abgeschlossen, dass wir die Informationen über Achsen und Knöpfe in den Attributen axes und buttons des HIDs speichern, damit sie beim Verarbeiten von Eingaben abgerufen werden können:

                for(auto const & currentClass : axisClasses) {

                    auto const firstUsage = currentClass.Range.UsageMin;
                    auto const lastUsage = currentClass.Range.UsageMax;
                    for(WORD currentUsage = firstUsage, currentIndex = currentClass.Range.DataIndexMin;
                        currentUsage <= lastUsage;
                        ++currentUsage, ++currentIndex
                    ) {

                        bool            isCalibrated      = false;
                        int32_t         calibratedMinimum;
                        int32_t         calibratedMaximum;
                        int32_t         calibratedCenter;
                        wchar_t const * toName            = L"";

                        //
Wurden Kalibrierungsdaten oder Name überschrieben?
                        for(auto & mapping : dInputAxisMapping) {
                            if(currentClass.UsagePage == mapping.usagePage && currentUsage == mapping.usage) {
                                toName            = mapping.name;
                                isCalibrated      = mapping.isCalibrated;
                                if(mapping.isCalibrated) {
                                    calibratedMinimum = mapping.calibration.lMin;
                                    calibratedCenter  = mapping.calibration.lCenter;
                                    calibratedMaximum = mapping.calibration.lMax;
                                }

                                mapping.usage = 0; //
Optimierung: bei zukünfigen Durchläufen überspringen
                                break;
                            }
                        }

                        Axis axis;
                        axis.usagePage                = currentClass.UsagePage;
                        axis.usage                    = currentUsage;
                        axis.index                    = currentIndex;
                        axis.logicalMinimum           = currentClass.LogicalMin;
                        axis.logicalMaximum           = currentClass.LogicalMax;
                        axis.isCalibrated             = isCalibrated;
                        if(isCalibrated) {
                            axis.logicalCalibratedMinimum = calibratedMinimum;
                            axis.logicalCalibratedMaximum = calibratedMaximum;
                            axis.logicalCalibratedCenter  = calibratedCenter;
                        }
                        axis.physicalMinimum          = float(currentClass.PhysicalMin);
                        axis.physicalMaximum = float(currentClass.PhysicalMax);
                        axis.name                     = toName;

                        axes.push_back(axis);
                    }
                }


Ähnlich für die Knöpfe; nur, dass es dafür keine Kalibrierungsinformationen gibt und die Namen das einzige sind, was wir überschreiben:

                for(auto const & currentClass : buttonClasses) {

                    auto const firstUsage = currentClass.Range.UsageMin;
                    auto const lastUsage = currentClass.Range.UsageMax;
                    for(WORD currentUsage = firstUsage, currentIndex = currentClass.Range.DataIndexMin;
                        currentUsage <= lastUsage;
                        ++currentUsage, ++currentIndex
                    ) {

                        //
Wurde der Name von DirectInput oder dem Treiber überschrieben?
                        wchar_t const * toName = L"";
                        for(auto & mapping : dInputButtonMapping) {
                            if(currentClass.UsagePage == mapping.usagePage && currentUsage == mapping.usage) {
                                toName = mapping.name;

                                mapping.usage = 0; //
Optimierung: bei zukünfigen Durchläufen überspringen
                                break;
                            }
                        }

                        Button button;
                        button.usagePage = currentClass.UsagePage;
                        button.usage     = currentUsage;
                        button.index     = currentIndex;
                        button.name      = toName;

                        buttons.push_back(button);
                    }

                }


Fertig! Ich empfehle aber dringend, dem Konstruktor am Ende noch Logging-Funktionalität hinzuzufügen, damit ihr die Ergebnisse einfach kontrollieren könnt. Bei mir sieht die beispielsweise so aus:

        std::cout << name << '\n';

        for(auto const & axis : axes) {

            std::wcout << "Achse " << std::hex << axis.usagePage << "/" << axis.usage;
            if(false == axis.name.empty()) {
                std::wcout << " (" << axis.name << ')';
            }
            if(axis.isCalibrated) {
                std::wcout << " kalibriert von " << std::dec << axis.logicalCalibratedMinimum << " ueber "
                           << axis.logicalCalibratedCenter << " zu " << axis.logicalCalibratedMaximum << '\n';
            } else {
                std::wcout << " nicht kalibriert von " << std::dec << axis.logicalMinimum << " bis " << axis.logicalMaximum << '\n';
            }

        }

        for(auto const & button : buttons) {

            std::wcout << "Knopf " << std::hex << button.usagePage << "/" << button.usage;
            if(button.name.empty()) {
                std::wcout << '\n';
            } else {
                std::wcout << " (" << button.name << ")\n";
            }

        }


und ich erhalte für meinen No-Name-Joystick folgende Ausgabe:

    Mega World USB Game Controllers
    Achse 1/32 kalibriert von 0 ueber 127 zu 255
    Achse 1/31 kalibriert von 0 ueber 128 zu 255
    Achse 1/30 kalibriert von 0 ueber 128 zu 255
    Achse 1/39 nicht kalibriert von 1 bis 8
    Knopf 9/1
    Knopf 9/2
    Knopf 9/3
    Knopf 9/4


Für meinen PlayStation-zu-USB-Adapter:

    GreenAsia Inc. USB Joystick
    Achse 1/0 nicht kalibriert von 0 bis 255
    Achse 1/31 (LY axis) kalibriert von 0 ueber 128 zu 255
    Achse 1/30 (LX axis) kalibriert von 0 ueber 128 zu 255
    Achse 1/35 (RY axis) kalibriert von 0 ueber 128 zu 255
    Achse 1/32 (RX axis) kalibriert von 0 ueber 128 zu 255
    Achse 1/39 nicht kalibriert von 0 bis 7
    Knopf 9/1 (Triangle)
    Knopf 9/2 (Circle)
    Knopf 9/3 (Cross)
    Knopf 9/4 (Square)
    Knopf 9/5 (L2)
    Knopf 9/6 (R2)
    Knopf 9/7 (L1)
    Knopf 9/8 (R1)
    Knopf 9/9 (Select)
    Knopf 9/a (Start)
    Knopf 9/b (L3)
    Knopf 9/c (R3)
    Knopf ff00/1


Noch schnell den Destruktor hinklatschen:

    HID::~HID() {

        HidD_FreePreparsedData(toInputReportProtocol);

        CloseHandle(ntHandle);

    }


Ich versichere damit hoch und heilig, dass das Schwierigste erledigt ist! Beim nächsten Mal geht es darum, endlich die Eingaben der angeschlossenen HIDs zu verarbeiten.
Zuletzt geändert von Krishty am 14.12.2014, 11:56, insgesamt 1-mal geändert.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
mrblacko
Beiträge: 1
Registriert: 20.09.2013, 16:08

Re: Raw Input für Joysticks und Gamecontroller

Beitrag von mrblacko »

hallo,

etrstmal ein großes Dankeschön für dieses Tutorial. Klasse Arbeit!
Eine Sache ist mir allerdings nicht ganz klar:
Du schreibst es sollen die Abhängigkeiten von dem Projekt hid.lib hinzugefügt werden.
Aber ich finde in meinem src/ Ordner kein Projekt mit solchem Namen und selbst wenn wäre mir nicht ganz klar wie ich seine Abhängigkeiten finden und hinzufügen würde.
Ich arbeite im Übrigen mit MSVC10 habe allerdings gelesen, dass es nicht sehr gut für die Zusammenarbeit mit dem WinDDk eignet.
Allerdings bezog sich das zum größten teil auf die Programmierung von kernel treibern. Also dieses Tutorial sollte doch trotzdem mit dem MSVC funktionieren oder?

Grüße
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Raw Input für Joysticks und Gamecontroller

Beitrag von Schrompf »

Das hat wenig mit RawInput zu tun, sondern nur mit den Abhängigkeiten bei C++. Die Einbindung einer externe Bibliothek besteht üblicherweise aus drei Schritten:

a) Mache den Compiler mit dem Include-Verzeichnis bekannt, in dem die Header liegen. In MSVC: Projekteinstellungen, VC++-Verzeichnisse, Include-Verzeichnis hinzufügen.
b) Mache den Compiler mit dem Libs-Verzeichnis bekannt, in dem die Library liegt. In MSVC: Projekteinstellungen, VC++-Verzeichnisse Bibliotheksverzeichnis hinzufügen.
c) Sage dem Linker, dass er die Lib mit einbinden soll. In MSVC: Projekteinstellungen, Linker, Eingabe, zusätzliche Abhängigkeiten

Such Dir die Verzeichnisse aus dem DDK raus und trage sie wie beschrieben in Deinem Projekt ein, dann sollte es funktionieren.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Raw Input für Joysticks und Gamecontroller

Beitrag von Krishty »

Genau wie Schrompf es sagt :)
mrblacko hat geschrieben:Ich arbeite im Übrigen mit MSVC10 habe allerdings gelesen, dass es nicht sehr gut für die Zusammenarbeit mit dem WinDDk eignet.
Allerdings bezog sich das zum größten teil auf die Programmierung von kernel treibern. Also dieses Tutorial sollte doch trotzdem mit dem MSVC funktionieren oder?
Ja; ich verfasse den Quelltext für dieses Tutorial mit VC10 – keine Schwierigkeiten. Ich finde es selber sehr umständlich, dass man das DDK herunterladen muss, um HIDs auswerten zu können. Die Gründe dafür kenne ich nicht. Das ist aber definitiv KEINE Treiberentwicklung, sondern wir brauchen bloß ein paar ganz normale Anwendungsfunktionen des Windows-Kernels, die dummerweise im DDK deklariert sind :)

Außerdem noch Entschuldigung, dass das Tutorial nicht fertig ist: Der Rest davon liegt seit vorgestern in meiner To-Do-Liste und ich hoffe, es an diesem Wochenende vervollständigen zu können!
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Raw Input für Joysticks und Gamecontroller

Beitrag von Krishty »

Der Rest ist immernoch auf der To-do-Liste. Tut mir leid, dass es ewig dauert. Ich habe aber gerade nach einem Tag Debugging den Enumerations-Quelltext korrigiert (Slider muss auf Z gemappt werden bevor die Registry-Kalibrierung gelesen wird) – wer schon abgeschrieben hat, muss das leider korrigieren.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Antworten