blob: 26c2dfd142f174eb427293c6ce4c1c5ddd81655f [file] [log] [blame]
// Copyright (c) 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "device/gamepad/gamepad_device_mac.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_cftyperef.h"
#import <Foundation/Foundation.h>
namespace {
// http://www.usb.org/developers/hidpage
const uint32_t kGenericDesktopUsagePage = 0x01;
const uint32_t kGameControlsUsagePage = 0x05;
const uint32_t kButtonUsagePage = 0x09;
const uint32_t kJoystickUsageNumber = 0x04;
const uint32_t kGameUsageNumber = 0x05;
const uint32_t kMultiAxisUsageNumber = 0x08;
const uint32_t kAxisMinimumUsageNumber = 0x30;
const int kRumbleMagnitudeMax = 10000;
float NormalizeAxis(CFIndex value, CFIndex min, CFIndex max) {
return (2.f * (value - min) / static_cast<float>(max - min)) - 1.f;
}
float NormalizeUInt8Axis(uint8_t value, uint8_t min, uint8_t max) {
return (2.f * (value - min) / static_cast<float>(max - min)) - 1.f;
}
float NormalizeUInt16Axis(uint16_t value, uint16_t min, uint16_t max) {
return (2.f * (value - min) / static_cast<float>(max - min)) - 1.f;
}
float NormalizeUInt32Axis(uint32_t value, uint32_t min, uint32_t max) {
return (2.f * (value - min) / static_cast<float>(max - min)) - 1.f;
}
} // namespace
namespace device {
GamepadDeviceMac::GamepadDeviceMac(int location_id, IOHIDDeviceRef device_ref)
: location_id_(location_id),
device_ref_(device_ref),
ff_device_ref_(nullptr),
ff_effect_ref_(nullptr) {
std::fill(button_elements_, button_elements_ + Gamepad::kButtonsLengthCap,
nullptr);
std::fill(axis_elements_, axis_elements_ + Gamepad::kAxesLengthCap, nullptr);
std::fill(axis_minimums_, axis_minimums_ + Gamepad::kAxesLengthCap, 0);
std::fill(axis_maximums_, axis_maximums_ + Gamepad::kAxesLengthCap, 0);
std::fill(axis_report_sizes_, axis_report_sizes_ + Gamepad::kAxesLengthCap,
0);
if (device_ref != nullptr) {
ff_device_ref_ = CreateForceFeedbackDevice(device_ref);
if (ff_device_ref_ != nullptr) {
ff_effect_ref_ = CreateForceFeedbackEffect(ff_device_ref_, &ff_effect_,
&ff_custom_force_, force_data_,
axes_data_, direction_data_);
}
}
}
GamepadDeviceMac::~GamepadDeviceMac() = default;
void GamepadDeviceMac::DoShutdown() {
SetZeroVibration();
if (ff_device_ref_ != nullptr) {
if (ff_effect_ref_ != nullptr)
FFDeviceReleaseEffect(ff_device_ref_, ff_effect_ref_);
FFReleaseDevice(ff_device_ref_);
}
}
// static
bool GamepadDeviceMac::CheckCollection(IOHIDElementRef element) {
// Check that a parent collection of this element matches one of the usage
// numbers that we are looking for.
while ((element = IOHIDElementGetParent(element)) != NULL) {
uint32_t usage_page = IOHIDElementGetUsagePage(element);
uint32_t usage = IOHIDElementGetUsage(element);
if (usage_page == kGenericDesktopUsagePage) {
if (usage == kJoystickUsageNumber || usage == kGameUsageNumber ||
usage == kMultiAxisUsageNumber) {
return true;
}
}
}
return false;
}
bool GamepadDeviceMac::AddButtonsAndAxes(Gamepad* gamepad) {
base::ScopedCFTypeRef<CFArrayRef> elements_cf(IOHIDDeviceCopyMatchingElements(
device_ref_, nullptr, kIOHIDOptionsTypeNone));
NSArray* elements = base::mac::CFToNSCast(elements_cf);
DCHECK(elements);
DCHECK(gamepad);
gamepad->axes_length = 0;
gamepad->buttons_length = 0;
gamepad->timestamp = 0;
memset(gamepad->axes, 0, sizeof(gamepad->axes));
memset(gamepad->buttons, 0, sizeof(gamepad->buttons));
bool mapped_all_axes = true;
for (id elem in elements) {
IOHIDElementRef element = reinterpret_cast<IOHIDElementRef>(elem);
if (!CheckCollection(element))
continue;
uint32_t usage_page = IOHIDElementGetUsagePage(element);
uint32_t usage = IOHIDElementGetUsage(element);
if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Button &&
usage_page == kButtonUsagePage) {
uint32_t button_index = usage - 1;
if (button_index < Gamepad::kButtonsLengthCap) {
button_elements_[button_index] = element;
gamepad->buttons_length =
std::max(gamepad->buttons_length, button_index + 1);
}
} else if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Misc) {
uint32_t axis_index = usage - kAxisMinimumUsageNumber;
if (axis_index < Gamepad::kAxesLengthCap) {
axis_elements_[axis_index] = element;
gamepad->axes_length = std::max(gamepad->axes_length, axis_index + 1);
} else {
mapped_all_axes = false;
}
}
}
if (!mapped_all_axes) {
// For axes whose usage puts them outside the standard axesLengthCap range.
uint32_t next_index = 0;
for (id elem in elements) {
IOHIDElementRef element = reinterpret_cast<IOHIDElementRef>(elem);
if (!CheckCollection(element))
continue;
uint32_t usage_page = IOHIDElementGetUsagePage(element);
uint32_t usage = IOHIDElementGetUsage(element);
if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Misc &&
usage - kAxisMinimumUsageNumber >= Gamepad::kAxesLengthCap &&
usage_page <= kGameControlsUsagePage) {
for (; next_index < Gamepad::kAxesLengthCap; ++next_index) {
if (axis_elements_[next_index] == NULL)
break;
}
if (next_index < Gamepad::kAxesLengthCap) {
axis_elements_[next_index] = element;
gamepad->axes_length = std::max(gamepad->axes_length, next_index + 1);
}
}
if (next_index >= Gamepad::kAxesLengthCap)
break;
}
}
for (uint32_t axis_index = 0; axis_index < gamepad->axes_length;
++axis_index) {
IOHIDElementRef element = axis_elements_[axis_index];
if (element != NULL) {
CFIndex axis_min = IOHIDElementGetLogicalMin(element);
CFIndex axis_max = IOHIDElementGetLogicalMax(element);
// Some HID axes report a logical range of -1 to 0 signed, which must be
// interpreted as 0 to -1 unsigned for correct normalization behavior.
if (axis_min == -1 && axis_max == 0) {
axis_max = -1;
axis_min = 0;
}
axis_minimums_[axis_index] = axis_min;
axis_maximums_[axis_index] = axis_max;
axis_report_sizes_[axis_index] = IOHIDElementGetReportSize(element);
}
}
return (gamepad->axes_length > 0 || gamepad->buttons_length > 0);
}
void GamepadDeviceMac::UpdateGamepadForValue(IOHIDValueRef value,
Gamepad* gamepad) {
DCHECK(gamepad);
IOHIDElementRef element = IOHIDValueGetElement(value);
uint32_t value_length = IOHIDValueGetLength(value);
if (value_length > 4) {
// Workaround for bizarre issue with PS3 controllers that try to return
// massive (30+ byte) values and crash IOHIDValueGetIntegerValue
return;
}
// Find and fill in the associated button event, if any.
for (size_t i = 0; i < gamepad->buttons_length; ++i) {
if (button_elements_[i] == element) {
bool pressed = IOHIDValueGetIntegerValue(value);
gamepad->buttons[i].pressed = pressed;
gamepad->buttons[i].value = pressed ? 1.f : 0.f;
gamepad->timestamp =
std::max(gamepad->timestamp, IOHIDValueGetTimeStamp(value));
return;
}
}
// Find and fill in the associated axis event, if any.
for (size_t i = 0; i < gamepad->axes_length; ++i) {
if (axis_elements_[i] == element) {
CFIndex axis_min = axis_minimums_[i];
CFIndex axis_max = axis_maximums_[i];
CFIndex axis_value = IOHIDValueGetIntegerValue(value);
if (axis_min > axis_max) {
// We'll need to interpret this axis as unsigned during normalization.
switch (axis_report_sizes_[i]) {
case 8:
gamepad->axes[i] =
NormalizeUInt8Axis(axis_value, axis_min, axis_max);
break;
case 16:
gamepad->axes[i] =
NormalizeUInt16Axis(axis_value, axis_min, axis_max);
break;
case 32:
gamepad->axes[i] =
NormalizeUInt32Axis(axis_value, axis_min, axis_max);
break;
}
} else {
gamepad->axes[i] = NormalizeAxis(axis_value, axis_min, axis_max);
}
gamepad->timestamp =
std::max(gamepad->timestamp, IOHIDValueGetTimeStamp(value));
return;
}
}
}
void GamepadDeviceMac::SetVibration(double strong_magnitude,
double weak_magnitude) {
FFCUSTOMFORCE* ff_custom_force =
static_cast<FFCUSTOMFORCE*>(ff_effect_.lpvTypeSpecificParams);
DCHECK(ff_custom_force);
DCHECK(ff_custom_force->rglForceData);
ff_custom_force->rglForceData[0] =
static_cast<LONG>(strong_magnitude * kRumbleMagnitudeMax);
ff_custom_force->rglForceData[1] =
static_cast<LONG>(weak_magnitude * kRumbleMagnitudeMax);
// Download the effect to the device and start the effect.
HRESULT res = FFEffectSetParameters(
ff_effect_ref_, &ff_effect_,
FFEP_DURATION | FFEP_STARTDELAY | FFEP_TYPESPECIFICPARAMS);
if (res == FF_OK)
FFEffectStart(ff_effect_ref_, 1, FFES_SOLO);
}
void GamepadDeviceMac::SetZeroVibration() {
FFEffectStop(ff_effect_ref_);
}
// static
FFDeviceObjectReference GamepadDeviceMac::CreateForceFeedbackDevice(
IOHIDDeviceRef device_ref) {
io_service_t service = IOHIDDeviceGetService(device_ref);
if (service == MACH_PORT_NULL)
return nullptr;
HRESULT res = FFIsForceFeedback(service);
if (res != FF_OK)
return nullptr;
FFDeviceObjectReference ff_device_ref;
res = FFCreateDevice(service, &ff_device_ref);
if (res != FF_OK)
return nullptr;
return ff_device_ref;
}
// static
FFEffectObjectReference GamepadDeviceMac::CreateForceFeedbackEffect(
FFDeviceObjectReference ff_device_ref,
FFEFFECT* ff_effect,
FFCUSTOMFORCE* ff_custom_force,
LONG* force_data,
DWORD* axes_data,
LONG* direction_data) {
DCHECK(ff_effect);
DCHECK(ff_custom_force);
DCHECK(force_data);
DCHECK(axes_data);
DCHECK(direction_data);
FFCAPABILITIES caps;
HRESULT res = FFDeviceGetForceFeedbackCapabilities(ff_device_ref, &caps);
if (res != FF_OK)
return nullptr;
if ((caps.supportedEffects & FFCAP_ET_CUSTOMFORCE) == 0)
return nullptr;
force_data[0] = 0;
force_data[1] = 0;
axes_data[0] = caps.ffAxes[0];
axes_data[1] = caps.ffAxes[1];
direction_data[0] = 0;
direction_data[1] = 0;
ff_custom_force->cChannels = 2;
ff_custom_force->cSamples = 2;
ff_custom_force->rglForceData = force_data;
ff_custom_force->dwSamplePeriod = 100000; // 100 ms
ff_effect->dwSize = sizeof(FFEFFECT);
ff_effect->dwFlags = FFEFF_OBJECTOFFSETS | FFEFF_SPHERICAL;
ff_effect->dwDuration = 5000000; // 5 seconds
ff_effect->dwSamplePeriod = 100000; // 100 ms
ff_effect->dwGain = 10000;
ff_effect->dwTriggerButton = FFEB_NOTRIGGER;
ff_effect->dwTriggerRepeatInterval = 0;
ff_effect->cAxes = caps.numFfAxes;
ff_effect->rgdwAxes = axes_data;
ff_effect->rglDirection = direction_data;
ff_effect->lpEnvelope = nullptr;
ff_effect->cbTypeSpecificParams = sizeof(FFCUSTOMFORCE);
ff_effect->lpvTypeSpecificParams = ff_custom_force;
ff_effect->dwStartDelay = 0;
FFEffectObjectReference ff_effect_ref;
res = FFDeviceCreateEffect(ff_device_ref, kFFEffectType_CustomForce_ID,
ff_effect, &ff_effect_ref);
if (res != FF_OK)
return nullptr;
return ff_effect_ref;
}
} // namespace device