bug 690937 - Add XInput support to the Windows gamepad backend. r=jimm

This commit is contained in:
Ted Mielczarek 2014-04-16 19:43:39 -04:00
Родитель 873a726a19
Коммит ef5ccd9de9
2 изменённых файлов: 257 добавлений и 3 удалений

Просмотреть файл

@ -22,6 +22,19 @@ enum GamepadMappingType
StandardMapping = 1
};
// Per spec:
// https://dvcs.w3.org/hg/gamepad/raw-file/default/gamepad.html#remapping
const int kStandardGamepadButtons = 17;
const int kStandardGamepadAxes = 4;
const int kButtonLeftTrigger = 6;
const int kButtonRightTrigger = 7;
const int kLeftStickXAxis = 0;
const int kLeftStickYAxis = 1;
const int kRightStickXAxis = 2;
const int kRightStickYAxis = 3;
class Gamepad : public nsISupports,
public nsWrapperCache
{

Просмотреть файл

@ -12,6 +12,7 @@
#include <windows.h>
#include <hidsdi.h>
#include <stdio.h>
#include <Xinput.h>
#include "nsIComponentManager.h"
#include "nsIObserver.h"
@ -24,7 +25,7 @@
namespace {
using mozilla::dom::GamepadService;
using namespace mozilla::dom;
using mozilla::ArrayLength;
// USB HID usage tables, page 1 (Hat switch)
@ -45,6 +46,13 @@ const unsigned kMaxAxes = 32;
// Therefore, we wait a bit after receiving one before looking for
// device changes.
const uint32_t kDevicesChangedStableDelay = 200;
// XInput is a purely polling-driven API, so we need to
// poll it periodically. 50ms is arbitrarily chosen.
const uint32_t kXInputPollInterval = 50;
#ifndef XUSER_MAX_COUNT
#define XUSER_MAX_COUNT 4
#endif
const struct {
int usagePage;
@ -55,9 +63,31 @@ const struct {
{ kDesktopUsagePage, 5 } // Gamepad
};
const struct {
WORD button;
int mapped;
} kXIButtonMap[] = {
{ XINPUT_GAMEPAD_DPAD_UP, 12 },
{ XINPUT_GAMEPAD_DPAD_DOWN, 13 },
{ XINPUT_GAMEPAD_DPAD_LEFT, 14 },
{ XINPUT_GAMEPAD_DPAD_RIGHT, 15 },
{ XINPUT_GAMEPAD_START, 9 },
{ XINPUT_GAMEPAD_BACK, 8 },
{ XINPUT_GAMEPAD_LEFT_THUMB, 10 },
{ XINPUT_GAMEPAD_RIGHT_THUMB, 11 },
{ XINPUT_GAMEPAD_LEFT_SHOULDER, 4 },
{ XINPUT_GAMEPAD_RIGHT_SHOULDER, 5 },
{ XINPUT_GAMEPAD_A, 0 },
{ XINPUT_GAMEPAD_B, 1 },
{ XINPUT_GAMEPAD_X, 2 },
{ XINPUT_GAMEPAD_Y, 3 }
};
const size_t kNumMappings = ArrayLength(kXIButtonMap);
enum GamepadType {
kNoGamepad = 0,
kRawInputGamepad
kRawInputGamepad,
kXInputGamepad
};
class WindowsGamepadService;
@ -69,6 +99,12 @@ struct Gamepad {
// Handle to raw input device
HANDLE handle;
// XInput Index of the user's controller. Passed to XInputGetState.
DWORD userIndex;
// Last-known state of the controller.
XINPUT_STATE state;
// ID from the GamepadService, also used as the index into
// WindowsGamepadService::mGamepads.
int id;
@ -89,6 +125,55 @@ struct Gamepad {
bool present;
};
// Drop this in favor of decltype when we require a new enough SDK.
typedef void (WINAPI *XInputEnable_func)(BOOL);
// RAII class to wrap loading the XInput DLL
class XInputLoader {
public:
XInputLoader() : module(nullptr),
mXInputEnable(nullptr),
mXInputGetState(nullptr) {
// xinput1_4.dll exists on Windows 8
// xinput9_1_0.dll exists on Windows 7 and Vista
// xinput1_3.dll shipped with the DirectX SDK
const wchar_t* dlls[] = {L"xinput1_4.dll",
L"xinput9_1_0.dll",
L"xinput1_3.dll"};
const size_t kNumDLLs = ArrayLength(dlls);
for (size_t i = 0; i < kNumDLLs; ++i) {
module = LoadLibraryW(dlls[i]);
if (module) {
mXInputEnable = reinterpret_cast<XInputEnable_func>(
GetProcAddress(module, "XInputEnable"));
mXInputGetState = reinterpret_cast<decltype(XInputGetState)*>(
GetProcAddress(module, "XInputGetState"));
if (mXInputEnable) {
mXInputEnable(TRUE);
}
break;
}
}
}
~XInputLoader() {
//mXInputEnable = nullptr;
mXInputGetState = nullptr;
if (module) {
FreeLibrary(module);
}
}
operator bool() {
return module && mXInputGetState;
}
HMODULE module;
decltype(XInputGetState) *mXInputGetState;
XInputEnable_func mXInputEnable;
};
bool
GetPreparsedData(HANDLE handle, nsTArray<uint8_t>& data)
{
@ -293,6 +378,15 @@ private:
void ScanForDevices();
// Look for connected raw input devices.
void ScanForRawInputDevices();
// Look for connected XInput devices.
bool ScanForXInputDevices();
bool HaveXInputGamepad(int userIndex);
// Timer callback for XInput polling
static void XInputPollTimerCallback(nsITimer* aTimer, void* aClosure);
void PollXInput();
void CheckXInputChanges(Gamepad& gamepad, XINPUT_STATE& state);
// Get information about a raw input gamepad.
bool GetRawGamepad(HANDLE handle);
void Cleanup();
@ -301,13 +395,17 @@ private:
nsTArray<Gamepad> mGamepads;
nsRefPtr<Observer> mObserver;
nsCOMPtr<nsITimer> mXInputPollTimer;
HIDLoader mHID;
XInputLoader mXInput;
};
WindowsGamepadService::WindowsGamepadService()
{
nsresult rv;
mXInputPollTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
mObserver = new Observer(*this);
}
@ -337,6 +435,59 @@ WindowsGamepadService::ScanForRawInputDevices()
}
}
bool
WindowsGamepadService::HaveXInputGamepad(int userIndex)
{
for (unsigned int i = 0; i < mGamepads.Length(); i++) {
if (mGamepads[i].type == kXInputGamepad
&& mGamepads[i].userIndex == userIndex) {
mGamepads[i].present = true;
return true;
}
}
return false;
}
bool
WindowsGamepadService::ScanForXInputDevices()
{
MOZ_ASSERT(mXInput, "XInput should be present!");
nsRefPtr<GamepadService> gamepadsvc(GamepadService::GetService());
if (!gamepadsvc) {
return false;
}
bool found = false;
for (int i = 0; i < XUSER_MAX_COUNT; i++) {
XINPUT_STATE state = {};
if (mXInput.mXInputGetState(i, &state) != ERROR_SUCCESS) {
continue;
}
found = true;
// See if this device is already present in our list.
if (HaveXInputGamepad(i)) {
continue;
}
// Not already present, add it.
Gamepad gamepad = {};
gamepad.type = kXInputGamepad;
gamepad.present = true;
gamepad.state = state;
gamepad.userIndex = i;
gamepad.numButtons = kStandardGamepadButtons;
gamepad.numAxes = kStandardGamepadAxes;
gamepad.id = gamepadsvc->AddGamepad("xinput",
StandardMapping,
kStandardGamepadButtons,
kStandardGamepadAxes);
mGamepads.AppendElement(gamepad);
}
return found;
}
void
WindowsGamepadService::ScanForDevices()
{
@ -347,6 +498,15 @@ WindowsGamepadService::ScanForDevices()
if (mHID) {
ScanForRawInputDevices();
}
if (mXInput) {
mXInputPollTimer->Cancel();
if (ScanForXInputDevices()) {
mXInputPollTimer->InitWithFuncCallback(XInputPollTimerCallback,
this,
kXInputPollInterval,
nsITimer::TYPE_REPEATING_SLACK);
}
}
nsRefPtr<GamepadService> gamepadsvc(GamepadService::GetService());
if (!gamepadsvc) {
@ -361,6 +521,84 @@ WindowsGamepadService::ScanForDevices()
}
}
// static
void
WindowsGamepadService::XInputPollTimerCallback(nsITimer* aTimer,
void* aClosure)
{
WindowsGamepadService* self =
reinterpret_cast<WindowsGamepadService*>(aClosure);
self->PollXInput();
}
void
WindowsGamepadService::PollXInput()
{
for (unsigned int i = 0; i < mGamepads.Length(); i++) {
if (mGamepads[i].type != kXInputGamepad) {
continue;
}
XINPUT_STATE state = {};
DWORD res = mXInput.mXInputGetState(mGamepads[i].userIndex, &state);
if (res == ERROR_SUCCESS
&& state.dwPacketNumber != mGamepads[i].state.dwPacketNumber) {
CheckXInputChanges(mGamepads[i], state);
}
}
}
void WindowsGamepadService::CheckXInputChanges(Gamepad& gamepad,
XINPUT_STATE& state) {
nsRefPtr<GamepadService> gamepadsvc(GamepadService::GetService());
// Handle digital buttons first
for (size_t b = 0; b < kNumMappings; b++) {
if (state.Gamepad.wButtons & kXIButtonMap[b].button &&
!(gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button)) {
// Button pressed
gamepadsvc->NewButtonEvent(gamepad.id, kXIButtonMap[b].mapped, true);
} else if (!(state.Gamepad.wButtons & kXIButtonMap[b].button) &&
gamepad.state.Gamepad.wButtons & kXIButtonMap[b].button) {
// Button released
gamepadsvc->NewButtonEvent(gamepad.id, kXIButtonMap[b].mapped, false);
}
}
// Then triggers
if (state.Gamepad.bLeftTrigger != gamepad.state.Gamepad.bLeftTrigger) {
bool pressed =
state.Gamepad.bLeftTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
gamepadsvc->NewButtonEvent(gamepad.id, kButtonLeftTrigger,
pressed, state.Gamepad.bLeftTrigger / 255.0);
}
if (state.Gamepad.bRightTrigger != gamepad.state.Gamepad.bRightTrigger) {
bool pressed =
state.Gamepad.bRightTrigger >= XINPUT_GAMEPAD_TRIGGER_THRESHOLD;
gamepadsvc->NewButtonEvent(gamepad.id, kButtonRightTrigger,
pressed, state.Gamepad.bRightTrigger / 255.0);
}
// Finally deal with analog sticks
// TODO: bug 1001955 - Support deadzones.
if (state.Gamepad.sThumbLX != gamepad.state.Gamepad.sThumbLX) {
gamepadsvc->NewAxisMoveEvent(gamepad.id, kLeftStickXAxis,
state.Gamepad.sThumbLX / 32767.0);
}
if (state.Gamepad.sThumbLY != gamepad.state.Gamepad.sThumbLY) {
gamepadsvc->NewAxisMoveEvent(gamepad.id, kLeftStickYAxis,
-1.0 * state.Gamepad.sThumbLY / 32767.0);
}
if (state.Gamepad.sThumbRX != gamepad.state.Gamepad.sThumbRX) {
gamepadsvc->NewAxisMoveEvent(gamepad.id, kRightStickXAxis,
state.Gamepad.sThumbRX / 32767.0);
}
if (state.Gamepad.sThumbRY != gamepad.state.Gamepad.sThumbRY) {
gamepadsvc->NewAxisMoveEvent(gamepad.id, kRightStickYAxis,
-1.0 * state.Gamepad.sThumbRY / 32767.0);
}
gamepad.state = state;
}
// Used to sort a list of axes by HID usage.
class HidValueComparator {
public:
@ -524,7 +762,7 @@ WindowsGamepadService::GetRawGamepad(HANDLE handle)
}
gamepad.id = gamepadsvc->AddGamepad(gamepad_id,
mozilla::dom::NoMapping,
NoMapping,
gamepad.numButtons,
gamepad.numAxes);
mGamepads.AppendElement(gamepad);
@ -656,6 +894,9 @@ WindowsGamepadService::Shutdown()
void
WindowsGamepadService::Cleanup()
{
if (mXInputPollTimer) {
mXInputPollTimer->Cancel();
}
mGamepads.Clear();
}