gecko-dev/hal/windows/WindowsGamepad.cpp

766 lines
22 KiB
C++

/* 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 <algorithm>
#include <cstddef>
#include <stdio.h>
#ifndef UNICODE
#define UNICODE
#endif
#include <windows.h>
#define DIRECTINPUT_VERSION 0x0800
#include <dinput.h>
#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 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.
int numAxes;
int numHats;
int numButtons;
// The human-readable device name.
char name[128];
// The DirectInput device.
nsRefPtr<IDirectInputDevice8> 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<nsIObserverService> observerService =
mozilla::services::GetObserverService();
observerService->AddObserver(this,
NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID,
false);
}
void Stop() {
if (mTimer) {
mTimer->Cancel();
}
if (mObserving) {
nsCOMPtr<nsIObserverService> 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<nsITimer> mTimer;
bool mObserving;
};
NS_IMPL_ISUPPORTS1(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<Gamepad> mGamepads;
// Used to lock mutation of mGamepads.
Mutex mMutex;
// List of event handles used for signaling.
nsTArray<HANDLE> mEvents;
LPDIRECTINPUT8 dinput;
nsRefPtr<Observer> 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<GamepadService> 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<GamepadService> 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<Gamepad*>(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);
return DIENUM_CONTINUE;
}
// static
BOOL CALLBACK
WindowsGamepadService::EnumCallback(LPCDIDEVICEINSTANCE lpddi,
LPVOID pvRef) {
WindowsGamepadService* self =
reinterpret_cast<WindowsGamepadService*>(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;
memset(&gamepad, 0, sizeof(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.numAxes = caps.dwAxes;
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.
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<GamepadChangeEvent> 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<GamepadChangeEvent> 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<WindowsGamepadService*>(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<IDirectInputDevice8> 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;
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;
which = (data.dwOfs - DIJOFS_X) / sizeof(LONG);
}
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) {
nsRefPtr<GamepadEvent> 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