blob: 4f5d90e566e272117b553ece9211d479ddd0487c [file] [log] [blame]
/*
* Copyright (C) 2014 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "HIDGamepad.h"
#if ENABLE(GAMEPAD) && PLATFORM(MAC)
#include <IOKit/hid/IOHIDElement.h>
#include <IOKit/hid/IOHIDUsageTables.h>
#include <IOKit/hid/IOHIDValue.h>
#include <wtf/HexNumber.h>
#include <wtf/cf/TypeCastsCF.h>
#include <wtf/text/CString.h>
WTF_DECLARE_CF_TYPE_TRAIT(IOHIDElement);
namespace WebCore {
HIDGamepad::HIDGamepad(IOHIDDeviceRef hidDevice, unsigned index)
: PlatformGamepad(index)
, m_hidDevice(hidDevice)
{
m_connectTime = m_lastUpdateTime = MonotonicTime::now();
CFNumberRef cfVendorID = (CFNumberRef)IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVendorIDKey));
CFNumberRef cfProductID = (CFNumberRef)IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductIDKey));
int vendorID, productID;
CFNumberGetValue(cfVendorID, kCFNumberIntType, &vendorID);
CFNumberGetValue(cfProductID, kCFNumberIntType, &productID);
CFStringRef cfProductName = (CFStringRef)IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductKey));
String productName(cfProductName);
// Currently the spec has no formatting for the id string.
// This string formatting matches Firefox.
m_id = makeString(hex(vendorID, Lowercase), '-', hex(productID, Lowercase), '-', productName);
initElements();
}
void HIDGamepad::getCurrentValueForElement(const HIDGamepadElement& gamepadElement)
{
IOHIDElementRef element = gamepadElement.iohidElement.get();
IOHIDValueRef value;
if (IOHIDDeviceGetValue(IOHIDElementGetDevice(element), element, &value) == kIOReturnSuccess)
valueChanged(value);
}
void HIDGamepad::initElements()
{
RetainPtr<CFArrayRef> elements = adoptCF(IOHIDDeviceCopyMatchingElements(m_hidDevice.get(), NULL, kIOHIDOptionsTypeNone));
initElementsFromArray(elements.get());
// Buttons are specified to appear highest priority first in the array.
std::sort(m_buttons.begin(), m_buttons.end(), [](auto& a, auto& b) {
return a->priority < b->priority;
});
m_axisValues.resize(m_axes.size());
m_buttonValues.resize(m_buttons.size() + (m_dPads.size() * 4));
for (auto& button : m_buttons)
getCurrentValueForElement(button.get());
for (auto& dPad : m_dPads)
getCurrentValueForElement(dPad.get());
for (auto& axis : m_axes)
getCurrentValueForElement(axis.get());
}
void HIDGamepad::initElementsFromArray(CFArrayRef elements)
{
for (CFIndex i = 0, count = CFArrayGetCount(elements); i < count; ++i) {
IOHIDElementRef element = checked_cf_cast<IOHIDElementRef>(CFArrayGetValueAtIndex(elements, i));
if (CFGetTypeID(element) != IOHIDElementGetTypeID())
continue;
// As a physical element can appear in the device twice (in different collections) and can be
// represented by different IOHIDElementRef objects, we look at the IOHIDElementCookie which
// is meant to be unique for each physical element.
IOHIDElementCookie cookie = IOHIDElementGetCookie(element);
if (m_elementMap.contains(cookie))
continue;
IOHIDElementType type = IOHIDElementGetType(element);
if ((type == kIOHIDElementTypeInput_Misc || type == kIOHIDElementTypeInput_Button)) {
if (maybeAddButton(element))
continue;
if (maybeAddDPad(element))
continue;
}
if ((type == kIOHIDElementTypeInput_Misc || type == kIOHIDElementTypeInput_Axis) && maybeAddAxis(element))
continue;
if (type == kIOHIDElementTypeCollection)
initElementsFromArray(IOHIDElementGetChildren(element));
}
}
bool HIDGamepad::maybeAddButton(IOHIDElementRef element)
{
uint32_t usagePage = IOHIDElementGetUsagePage(element);
if (usagePage != kHIDPage_Button && usagePage != kHIDPage_GenericDesktop)
return false;
uint32_t usage = IOHIDElementGetUsage(element);
if (usagePage == kHIDPage_GenericDesktop) {
if (usage < kHIDUsage_GD_DPadUp || usage > kHIDUsage_GD_DPadLeft)
return false;
usage = std::numeric_limits<uint32_t>::max();
} else if (!usage)
return false;
CFIndex min = IOHIDElementGetLogicalMin(element);
CFIndex max = IOHIDElementGetLogicalMax(element);
m_buttons.append(makeUniqueRef<HIDGamepadButton>(usage, min, max, element));
IOHIDElementCookie cookie = IOHIDElementGetCookie(element);
m_elementMap.set(cookie, &m_buttons.last().get());
return true;
}
bool HIDGamepad::maybeAddDPad(IOHIDElementRef element)
{
uint32_t usagePage = IOHIDElementGetUsagePage(element);
if (usagePage != kHIDPage_GenericDesktop)
return false;
uint32_t usage = IOHIDElementGetUsage(element);
if (!usage || usage != kHIDUsage_GD_Hatswitch)
return false;
CFIndex min = IOHIDElementGetLogicalMin(element);
CFIndex max = IOHIDElementGetLogicalMax(element);
m_dPads.append(makeUniqueRef<HIDGamepadDPad>(min, max, element));
IOHIDElementCookie cookie = IOHIDElementGetCookie(element);
m_elementMap.set(cookie, &m_dPads.last().get());
return true;
}
bool HIDGamepad::maybeAddAxis(IOHIDElementRef element)
{
uint32_t usagePage = IOHIDElementGetUsagePage(element);
if (usagePage != kHIDPage_GenericDesktop)
return false;
uint32_t usage = IOHIDElementGetUsage(element);
// This range covers the standard axis usages.
if (usage < kHIDUsage_GD_X || usage > kHIDUsage_GD_Rz)
return false;
CFIndex min = IOHIDElementGetPhysicalMin(element);
CFIndex max = IOHIDElementGetPhysicalMax(element);
m_axes.append(makeUniqueRef<HIDGamepadAxis>(min, max, element));
IOHIDElementCookie cookie = IOHIDElementGetCookie(element);
m_elementMap.set(cookie, &m_axes.last().get());
return true;
}
static void fillInButtonValues(int value, double& button0, double& button1, double& button2, double& button3)
{
// Standard DPads have 8 possible position values, moving around a circle:
// 0 - Up
// 1 - Up + right
// 2 - Right
// 3 - Down + right
// 4 - Down
// 5 - Down + left
// 6 - Left
// 7 - Up and Left
// These 8 positions are mapped on to 4 button positions:
// 0 - Up
// 1 - Down
// 2 - Left
// 3 - Right
if (value < 0 || value > 7) {
// Handle an invalid input value that we don't know how to interpret.
button0 = 0.0;
button1 = 0.0;
button2 = 0.0;
button3 = 0.0;
return;
}
button0 = value < 2 || value == 7 ? 1.0 : 0.0;
button1 = value > 2 && value < 6 ? 1.0 : 0.0;
button2 = value > 4 ? 1.0 : 0.0;
button3 = value > 0 && value < 4 ? 1.0 : 0.0;
}
HIDInputType HIDGamepad::valueChanged(IOHIDValueRef value)
{
IOHIDElementCookie cookie = IOHIDElementGetCookie(IOHIDValueGetElement(value));
HIDGamepadElement* element = m_elementMap.get(cookie);
// This might be an element we don't currently handle as input so we can skip it.
if (!element)
return HIDInputType::NotAButtonPress;
element->rawValue = IOHIDValueGetScaledValue(value, kIOHIDValueScaleTypePhysical);
if (element->isButton()) {
for (unsigned i = 0; i < m_buttons.size(); ++i) {
if (&m_buttons[i].get() == element) {
m_buttonValues[i] = element->normalizedValue();
break;
}
}
} else if (element->isAxis()) {
for (unsigned i = 0; i < m_axes.size(); ++i) {
if (&m_axes[i].get() == element) {
m_axisValues[i] = element->normalizedValue();
break;
}
}
} else if (element->isDPad()) {
int intValue = IOHIDValueGetIntegerValue(value) - element->min;
for (unsigned i = 0; i < m_dPads.size(); ++i) {
if (&m_dPads[i].get() != element)
continue;
// Each DPad represents 4 button values which are tacked on to the end of the values from non-DPad buttons.
unsigned firstButtonValue = m_buttons.size() + i * 4;
ASSERT(m_buttonValues.size() > firstButtonValue + 3);
fillInButtonValues(intValue, m_buttonValues[firstButtonValue], m_buttonValues[firstButtonValue + 1], m_buttonValues[firstButtonValue + 2], m_buttonValues[firstButtonValue + 3]);
}
} else
ASSERT_NOT_REACHED();
m_lastUpdateTime = MonotonicTime::now();
return element->isButton() ? HIDInputType::ButtonPress : HIDInputType::NotAButtonPress;
}
} // namespace WebCore
#endif // ENABLE(GAMEPAD) && PLATFORM(MAC)