diff -r 000000000000 -r 6474c204b198 hal/windows/WindowsGamepad.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hal/windows/WindowsGamepad.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,782 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include +#include + +#include +#ifndef UNICODE +#define UNICODE +#endif +#include +#define DIRECTINPUT_VERSION 0x0800 +#include + +#include "nsIComponentManager.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsITimer.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "mozilla/dom/GamepadService.h" +#include "mozilla/Mutex.h" +#include "mozilla/Services.h" + +namespace { + +using mozilla::dom::GamepadService; +using mozilla::Mutex; +using mozilla::MutexAutoLock; + +const unsigned kMaxAxes = 32; +const LONG kMaxAxisValue = 65535; +const DWORD BUTTON_DOWN_MASK = 0x80; +// Multiple devices-changed notifications can be sent when a device +// is connected, because USB devices consist of multiple logical devices. +// Therefore, we wait a bit after receiving one before looking for +// device changes. +const uint32_t kDevicesChangedStableDelay = 200; + +class WindowsGamepadService; +WindowsGamepadService* gService = nullptr; + +typedef struct { + float x,y; +} HatState; + +struct Gamepad { + // From DirectInput, unique to this device+computer combination. + GUID guidInstance; + // The ID assigned by the base GamepadService + int globalID; + // A somewhat unique string consisting of the USB vendor/product IDs, + // and the controller name. + char idstring[128]; + // USB vendor and product IDs + int vendorID; + int productID; + // Information about the physical device. + DWORD axes[kMaxAxes]; + int numAxes; + int numHats; + int numButtons; + // The human-readable device name. + char name[128]; + // The DirectInput device. + nsRefPtr device; + // A handle that DirectInput signals when there is new data from + // the device. + HANDLE event; + // The state of any POV hats on the device. + HatState hatState[4]; + // Used during rescan to find devices that were disconnected. + bool present; + // Passed back from the main thread to indicate a device can + // now be removed. + bool remove; +}; + +// Given DWORD |hatPos| representing the position of the POV hat per: +// http://msdn.microsoft.com/en-us/library/ee418260%28v=VS.85%29.aspx +// fill |axes| with the position of the x and y axes. +// +//XXX: ostensibly the values could be arbitrary degrees for a hat with +// full rotation, but we'll punt on that for now. This should handle +// 8-way D-pads exposed as POV hats. +static void +HatPosToAxes(DWORD hatPos, HatState& axes) { + // hatPos is in hundredths of a degree clockwise from north. + if (LOWORD(hatPos) == 0xFFFF) { + // centered + axes.x = axes.y = 0.0; + } + else if (hatPos == 0) { + // Up + axes.x = 0.0; + axes.y = -1.0; + } + else if (hatPos == 45 * DI_DEGREES) { + // Up-right + axes.x = 1.0; + axes.y = -1.0; + } + else if (hatPos == 90 * DI_DEGREES) { + // Right + axes.x = 1.0; + axes.y = 0.0; + } + else if (hatPos == 135 * DI_DEGREES) { + // Down-right + axes.x = 1.0; + axes.y = 1.0; + } + else if (hatPos == 180 * DI_DEGREES) { + // Down + axes.x = 0.0; + axes.y = 1.0; + } + else if (hatPos == 225 * DI_DEGREES) { + // Down-left + axes.x = -1.0; + axes.y = 1.0; + } + else if (hatPos == 270 * DI_DEGREES) { + // Left + axes.x = -1.0; + axes.y = 0.0; + } + else if (hatPos == 315 * DI_DEGREES) { + // Up-left + axes.x = -1.0; + axes.y = -1.0; + } +} + +class Observer : public nsIObserver { +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + Observer(WindowsGamepadService& svc) : mSvc(svc), + mObserving(true) { + nsresult rv; + mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + observerService->AddObserver(this, + NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, + false); + } + + void Stop() { + if (mTimer) { + mTimer->Cancel(); + } + if (mObserving) { + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + observerService->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID); + mObserving = false; + } + } + + virtual ~Observer() { + Stop(); + } + + void SetDeviceChangeTimer() { + // set stable timer, since we will get multiple devices-changed + // notifications at once + if (mTimer) { + mTimer->Cancel(); + mTimer->Init(this, kDevicesChangedStableDelay, nsITimer::TYPE_ONE_SHOT); + } + } + +private: + // Gamepad service owns us, we just hold a reference back to it. + WindowsGamepadService& mSvc; + nsCOMPtr mTimer; + bool mObserving; +}; + +NS_IMPL_ISUPPORTS(Observer, nsIObserver); + +class WindowsGamepadService { +public: + WindowsGamepadService(); + virtual ~WindowsGamepadService() { + Cleanup(); + CloseHandle(mThreadExitEvent); + CloseHandle(mThreadRescanEvent); + if (dinput) { + dinput->Release(); + dinput = nullptr; + } + } + + enum DeviceChangeType { + DeviceChangeNotification, + DeviceChangeStable + }; + void DevicesChanged(DeviceChangeType type); + void Startup(); + void Shutdown(); + void SetGamepadID(int localID, int globalID); + void RemoveGamepad(int localID); + +private: + void ScanForDevices(); + void Cleanup(); + void CleanupGamepad(Gamepad& gamepad); + // Callback for enumerating axes on a device + static BOOL CALLBACK EnumObjectsCallback(LPCDIDEVICEOBJECTINSTANCE lpddoi, + LPVOID pvRef); + // Callback for enumerating devices via DInput + static BOOL CALLBACK EnumCallback(LPCDIDEVICEINSTANCE lpddi, LPVOID pvRef); + // Thread function to wait on device events + static DWORD WINAPI DInputThread(LPVOID arg); + + // Used to signal the background thread to exit. + HANDLE mThreadExitEvent; + // Used to signal the background thread to rescan devices. + HANDLE mThreadRescanEvent; + HANDLE mThread; + + // List of connected devices. + nsTArray mGamepads; + // Used to lock mutation of mGamepads. + Mutex mMutex; + // List of event handles used for signaling. + nsTArray mEvents; + + LPDIRECTINPUT8 dinput; + + nsRefPtr mObserver; +}; + +// Used to post events from the background thread to the foreground thread. +class GamepadEvent : public nsRunnable { +public: + typedef enum { + Axis, + Button, + HatX, + HatY, + HatXY, + Unknown + } Type; + + GamepadEvent(const Gamepad& gamepad, + Type type, + int which, + DWORD data) : mGlobalID(gamepad.globalID), + mGamepadAxes(gamepad.numAxes), + mType(type), + mWhich(which), + mData(data) { + } + + NS_IMETHOD Run() { + nsRefPtr gamepadsvc(GamepadService::GetService()); + if (!gamepadsvc) { + return NS_OK; + } + + switch (mType) { + case Button: + gamepadsvc->NewButtonEvent(mGlobalID, mWhich, mData & BUTTON_DOWN_MASK); + break; + case Axis: { + float adjustedData = ((float)mData * 2.0f) / (float)kMaxAxisValue - 1.0f; + gamepadsvc->NewAxisMoveEvent(mGlobalID, mWhich, adjustedData); + } + case HatX: + case HatY: + case HatXY: { + // Synthesize 2 axes per POV hat for convenience. + HatState hatState; + HatPosToAxes(mData, hatState); + int xAxis = mGamepadAxes + 2 * mWhich; + int yAxis = mGamepadAxes + 2 * mWhich + 1; + //TODO: ostensibly we could not fire an event if one axis hasn't + // changed, but it's a pain to track that. + if (mType == HatX || mType == HatXY) { + gamepadsvc->NewAxisMoveEvent(mGlobalID, xAxis, hatState.x); + } + if (mType == HatY || mType == HatXY) { + gamepadsvc->NewAxisMoveEvent(mGlobalID, yAxis, hatState.y); + } + break; + } + case Unknown: + break; + } + return NS_OK; + } + + int mGlobalID; + int mGamepadAxes; + // Type of event + Type mType; + // Which button/axis is involved + int mWhich; + // Data specific to event + DWORD mData; +}; + +class GamepadChangeEvent : public nsRunnable { +public: + enum Type { + Added, + Removed + }; + GamepadChangeEvent(Gamepad& gamepad, + int localID, + Type type) : mLocalID(localID), + mName(gamepad.idstring), + mGlobalID(gamepad.globalID), + mGamepadButtons(gamepad.numButtons), + mGamepadAxes(gamepad.numAxes), + mGamepadHats(gamepad.numHats), + mType(type) { + } + + NS_IMETHOD Run() { + nsRefPtr gamepadsvc(GamepadService::GetService()); + if (!gamepadsvc) { + return NS_OK; + } + if (mType == Added) { + int globalID = gamepadsvc->AddGamepad(mName.get(), + mozilla::dom::NoMapping, + mGamepadButtons, + mGamepadAxes + + mGamepadHats*2); + if (gService) { + gService->SetGamepadID(mLocalID, globalID); + } + } else { + gamepadsvc->RemoveGamepad(mGlobalID); + if (gService) { + gService->RemoveGamepad(mLocalID); + } + } + return NS_OK; + } + +private: + // ID in WindowsGamepadService::mGamepads + int mLocalID; + nsCString mName; + int mGamepadButtons; + int mGamepadAxes; + int mGamepadHats; + // ID from GamepadService + uint32_t mGlobalID; + Type mType; +}; + +WindowsGamepadService::WindowsGamepadService() + : mThreadExitEvent(CreateEventW(nullptr, FALSE, FALSE, nullptr)), + mThreadRescanEvent(CreateEventW(nullptr, FALSE, FALSE, nullptr)), + mThread(nullptr), + mMutex("Windows Gamepad Service"), + dinput(nullptr) { + mObserver = new Observer(*this); + // Initialize DirectInput + CoInitialize(nullptr); + if (CoCreateInstance(CLSID_DirectInput8, + nullptr, + CLSCTX_INPROC_SERVER, + IID_IDirectInput8W, + (LPVOID*)&dinput) == S_OK) { + if (dinput->Initialize(GetModuleHandle(nullptr), + DIRECTINPUT_VERSION) != DI_OK) { + dinput->Release(); + dinput = nullptr; + } + } +} + +// static +BOOL CALLBACK +WindowsGamepadService::EnumObjectsCallback(LPCDIDEVICEOBJECTINSTANCE lpddoi, + LPVOID pvRef) { + // Ensure that all axes are using the same range. + Gamepad* gamepad = reinterpret_cast(pvRef); + DIPROPRANGE dp; + dp.diph.dwHeaderSize = sizeof(DIPROPHEADER); + dp.diph.dwSize = sizeof(DIPROPRANGE); + dp.diph.dwHow = DIPH_BYID; + dp.diph.dwObj = lpddoi->dwType; + dp.lMin = 0; + dp.lMax = kMaxAxisValue; + gamepad->device->SetProperty(DIPROP_RANGE, &dp.diph); + // Find what the dwOfs of this object in the c_dfDIJoystick data format is. + for (DWORD i = 0; i < c_dfDIJoystick.dwNumObjs; i++) { + if (c_dfDIJoystick.rgodf[i].pguid && + IsEqualGUID(*c_dfDIJoystick.rgodf[i].pguid, lpddoi->guidType) && + gamepad->numAxes < kMaxAxes) { + gamepad->axes[gamepad->numAxes] = c_dfDIJoystick.rgodf[i].dwOfs; + gamepad->numAxes++; + break; + } + } + return DIENUM_CONTINUE; +} + +// static +BOOL CALLBACK +WindowsGamepadService::EnumCallback(LPCDIDEVICEINSTANCE lpddi, + LPVOID pvRef) { + WindowsGamepadService* self = + reinterpret_cast(pvRef); + // See if this device is already present in our list. + { + MutexAutoLock lock(self->mMutex); + for (unsigned int i = 0; i < self->mGamepads.Length(); i++) { + if (memcmp(&lpddi->guidInstance, &self->mGamepads[i].guidInstance, + sizeof(GUID)) == 0) { + self->mGamepads[i].present = true; + return DIENUM_CONTINUE; + } + } + } + + Gamepad gamepad = {}; + if (self->dinput->CreateDevice(lpddi->guidInstance, + getter_AddRefs(gamepad.device), + nullptr) + == DI_OK) { + gamepad.present = true; + memcpy(&gamepad.guidInstance, &lpddi->guidInstance, sizeof(GUID)); + + DIDEVICEINSTANCE info; + info.dwSize = sizeof(DIDEVICEINSTANCE); + if (gamepad.device->GetDeviceInfo(&info) == DI_OK) { + WideCharToMultiByte(CP_UTF8, 0, info.tszProductName, -1, + gamepad.name, sizeof(gamepad.name), nullptr, nullptr); + } + // Get vendor id and product id + DIPROPDWORD dp; + dp.diph.dwSize = sizeof(DIPROPDWORD); + dp.diph.dwHeaderSize = sizeof(DIPROPHEADER); + dp.diph.dwObj = 0; + dp.diph.dwHow = DIPH_DEVICE; + if (gamepad.device->GetProperty(DIPROP_VIDPID, &dp.diph) == DI_OK) { + sprintf(gamepad.idstring, "%x-%x-%s", + LOWORD(dp.dwData), HIWORD(dp.dwData), gamepad.name); + } + DIDEVCAPS caps; + caps.dwSize = sizeof(DIDEVCAPS); + if (gamepad.device->GetCapabilities(&caps) == DI_OK) { + gamepad.numHats = caps.dwPOVs; + gamepad.numButtons = caps.dwButtons; + //XXX: handle polled devices? + // (caps.dwFlags & DIDC_POLLEDDATAFORMAT || caps.dwFlags & DIDC_POLLEDDEVICE) + } + // Set min/max range for all axes on the device. + // Axes will be gathered in EnumObjectsCallback. + gamepad.numAxes = 0; + gamepad.device->EnumObjects(EnumObjectsCallback, &gamepad, DIDFT_AXIS); + // Set up structure for setting buffer size for buffered data + dp.diph.dwHeaderSize = sizeof(DIPROPHEADER); + dp.diph.dwSize = sizeof(DIPROPDWORD); + dp.diph.dwObj = 0; + dp.diph.dwHow = DIPH_DEVICE; + dp.dwData = 64; // arbitrary + // Create event so DInput can signal us when there's new data. + gamepad.event = CreateEventW(nullptr, FALSE, FALSE, nullptr); + // Set data format, event notification, and acquire device + if (gamepad.device->SetDataFormat(&c_dfDIJoystick) == DI_OK && + gamepad.device->SetProperty(DIPROP_BUFFERSIZE, &dp.diph) == DI_OK && + gamepad.device->SetEventNotification(gamepad.event) == DI_OK && + gamepad.device->Acquire() == DI_OK) { + MutexAutoLock lock(self->mMutex); + self->mGamepads.AppendElement(gamepad); + // Inform the GamepadService + int localID = self->mGamepads.Length() - 1; + nsRefPtr event = + new GamepadChangeEvent(self->mGamepads[localID], + localID, + GamepadChangeEvent::Added); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + } + else { + if (gamepad.device) { + gamepad.device->SetEventNotification(nullptr); + } + CloseHandle(gamepad.event); + } + } + return DIENUM_CONTINUE; +} + +void +WindowsGamepadService::ScanForDevices() { + { + MutexAutoLock lock(mMutex); + for (int i = mGamepads.Length() - 1; i >= 0; i--) { + if (mGamepads[i].remove) { + + // Main thread has already handled this, safe to remove. + CleanupGamepad(mGamepads[i]); + mGamepads.RemoveElementAt(i); + } else { + mGamepads[i].present = false; + } + } + } + + dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, + (LPDIENUMDEVICESCALLBACK)EnumCallback, + this, + DIEDFL_ATTACHEDONLY); + + // Look for devices that are no longer present and inform the main thread. + { + MutexAutoLock lock(mMutex); + for (int i = mGamepads.Length() - 1; i >= 0; i--) { + if (!mGamepads[i].present) { + nsRefPtr event = + new GamepadChangeEvent(mGamepads[i], + i, + GamepadChangeEvent::Removed); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + } + } + + mEvents.Clear(); + for (unsigned int i = 0; i < mGamepads.Length(); i++) { + mEvents.AppendElement(mGamepads[i].event); + } + } + + + // These events must be the last elements in the array, so that + // the other elements match mGamepads in order. + mEvents.AppendElement(mThreadRescanEvent); + mEvents.AppendElement(mThreadExitEvent); +} + +// static +DWORD WINAPI +WindowsGamepadService::DInputThread(LPVOID arg) { + WindowsGamepadService* self = reinterpret_cast(arg); + self->ScanForDevices(); + + while (true) { + DWORD result = WaitForMultipleObjects(self->mEvents.Length(), + self->mEvents.Elements(), + FALSE, + INFINITE); + if (result == WAIT_FAILED || + result == WAIT_OBJECT_0 + self->mEvents.Length() - 1) { + // error, or the main thread signaled us to exit + break; + } + + unsigned int i = result - WAIT_OBJECT_0; + + if (i == self->mEvents.Length() - 2) { + // Main thread is signaling for a device rescan. + self->ScanForDevices(); + continue; + } + + { + MutexAutoLock lock(self->mMutex); + if (i >= self->mGamepads.Length()) { + // Something would be terribly wrong here, possibly we got + // a WAIT_ABANDONED_x result. + continue; + } + + // first query for the number of items in the buffer + DWORD items = INFINITE; + nsRefPtr device = self->mGamepads[i].device; + if (device->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), + nullptr, + &items, + DIGDD_PEEK)== DI_OK) { + while (items > 0) { + // now read each buffered event + //TODO: read more than one event at a time + DIDEVICEOBJECTDATA data; + DWORD readCount = sizeof(data) / sizeof(DIDEVICEOBJECTDATA); + if (device->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), + &data, &readCount, 0) == DI_OK) { + //TODO: data.dwTimeStamp + GamepadEvent::Type type = GamepadEvent::Unknown; + int which = -1; + if (data.dwOfs >= DIJOFS_BUTTON0 && data.dwOfs < DIJOFS_BUTTON(32)) { + type = GamepadEvent::Button; + which = data.dwOfs - DIJOFS_BUTTON0; + } + else if(data.dwOfs >= DIJOFS_X && data.dwOfs < DIJOFS_SLIDER(2)) { + // axis/slider + type = GamepadEvent::Axis; + for (int a = 0; a < self->mGamepads[i].numAxes; a++) { + if (self->mGamepads[i].axes[a] == data.dwOfs) { + which = a; + break; + } + } + } + else if (data.dwOfs >= DIJOFS_POV(0) && data.dwOfs < DIJOFS_POV(4)) { + HatState hatState; + HatPosToAxes(data.dwData, hatState); + which = (data.dwOfs - DIJOFS_POV(0)) / sizeof(DWORD); + // Only send out axis move events for the axes that moved + // in this hat move. + if (hatState.x != self->mGamepads[i].hatState[which].x) { + type = GamepadEvent::HatX; + } + if (hatState.y != self->mGamepads[i].hatState[which].y) { + if (type == GamepadEvent::HatX) { + type = GamepadEvent::HatXY; + } + else { + type = GamepadEvent::HatY; + } + } + self->mGamepads[i].hatState[which].x = hatState.x; + self->mGamepads[i].hatState[which].y = hatState.y; + } + + if (type != GamepadEvent::Unknown && which != -1) { + nsRefPtr event = + new GamepadEvent(self->mGamepads[i], type, which, data.dwData); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + } + } + items--; + } + } + } + } + return 0; +} + +void +WindowsGamepadService::Startup() { + mThread = CreateThread(nullptr, + 0, + DInputThread, + this, + 0, + nullptr); +} + +void +WindowsGamepadService::Shutdown() { + if (mThread) { + SetEvent(mThreadExitEvent); + WaitForSingleObject(mThread, INFINITE); + CloseHandle(mThread); + } + Cleanup(); +} + +// This method is called from the main thread. +void +WindowsGamepadService::SetGamepadID(int localID, int globalID) { + MutexAutoLock lock(mMutex); + mGamepads[localID].globalID = globalID; +} + +// This method is called from the main thread. +void WindowsGamepadService::RemoveGamepad(int localID) { + MutexAutoLock lock(mMutex); + mGamepads[localID].remove = true; + // Signal background thread to remove device. + DevicesChanged(DeviceChangeStable); +} + +void +WindowsGamepadService::Cleanup() { + for (unsigned int i = 0; i < mGamepads.Length(); i++) { + CleanupGamepad(mGamepads[i]); + } + mGamepads.Clear(); +} + +void +WindowsGamepadService::CleanupGamepad(Gamepad& gamepad) { + gamepad.device->Unacquire(); + gamepad.device->SetEventNotification(nullptr); + CloseHandle(gamepad.event); +} + +void +WindowsGamepadService::DevicesChanged(DeviceChangeType type) { + if (type == DeviceChangeNotification) { + mObserver->SetDeviceChangeTimer(); + } else if (type == DeviceChangeStable) { + SetEvent(mThreadRescanEvent); + } +} + +NS_IMETHODIMP +Observer::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + if (strcmp(aTopic, "timer-callback") == 0) { + mSvc.DevicesChanged(WindowsGamepadService::DeviceChangeStable); + } else if (strcmp(aTopic, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID) == 0) { + Stop(); + } + return NS_OK; +} + +HWND sHWnd = nullptr; + +static +LRESULT CALLBACK +GamepadWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + const unsigned int DBT_DEVICEARRIVAL = 0x8000; + const unsigned int DBT_DEVICEREMOVECOMPLETE = 0x8004; + const unsigned int DBT_DEVNODES_CHANGED = 0x7; + + if (msg == WM_DEVICECHANGE && + (wParam == DBT_DEVICEARRIVAL || + wParam == DBT_DEVICEREMOVECOMPLETE || + wParam == DBT_DEVNODES_CHANGED)) { + if (gService) { + gService->DevicesChanged(WindowsGamepadService::DeviceChangeNotification); + } + } + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +} // namespace + +namespace mozilla { +namespace hal_impl { + +void StartMonitoringGamepadStatus() +{ + if (gService) + return; + + gService = new WindowsGamepadService(); + gService->Startup(); + + if (sHWnd == nullptr) { + WNDCLASSW wc; + HMODULE hSelf = GetModuleHandle(nullptr); + + if (!GetClassInfoW(hSelf, L"MozillaGamepadClass", &wc)) { + ZeroMemory(&wc, sizeof(WNDCLASSW)); + wc.hInstance = hSelf; + wc.lpfnWndProc = GamepadWindowProc; + wc.lpszClassName = L"MozillaGamepadClass"; + RegisterClassW(&wc); + } + + sHWnd = CreateWindowW(L"MozillaGamepadClass", L"Gamepad Watcher", + 0, 0, 0, 0, 0, + nullptr, nullptr, hSelf, nullptr); + } +} + +void StopMonitoringGamepadStatus() +{ + if (!gService) + return; + + if (sHWnd) { + DestroyWindow(sHWnd); + sHWnd = nullptr; + } + + gService->Shutdown(); + delete gService; + gService = nullptr; +} + +} // namespace hal_impl +} // namespace mozilla +