chromium/device/gamepad/gamepad_device_mac.mm

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "device/gamepad/gamepad_device_mac.h"

#include <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/strings/sys_string_conversions.h"
#include "device/gamepad/dualshock4_controller.h"
#include "device/gamepad/gamepad_data_fetcher.h"
#include "device/gamepad/gamepad_id_list.h"
#include "device/gamepad/hid_haptic_gamepad.h"
#include "device/gamepad/hid_writer_mac.h"
#include "device/gamepad/xbox_hid_controller.h"

namespace device {

namespace {
// http://www.usb.org/developers/hidpage
const uint16_t kGenericDesktopUsagePage = 0x01;
const uint16_t kGameControlsUsagePage = 0x05;
const uint16_t kButtonUsagePage = 0x09;
const uint16_t kConsumerUsagePage = 0x0c;

const uint16_t kJoystickUsageNumber = 0x04;
const uint16_t kGameUsageNumber = 0x05;
const uint16_t kMultiAxisUsageNumber = 0x08;
const uint16_t kAxisMinimumUsageNumber = 0x30;
const uint16_t kSystemMainMenuUsageNumber = 0x85;
const uint16_t kPowerUsageNumber = 0x30;
const uint16_t kSearchUsageNumber = 0x0221;
const uint16_t kHomeUsageNumber = 0x0223;
const uint16_t kBackUsageNumber = 0x0224;
const uint16_t kRecordUsageNumber = 0xb2;

const int kRumbleMagnitudeMax = 10000;

struct SpecialUsages {
  const uint16_t usage_page;
  const uint16_t usage;
} kSpecialUsages[] = {
    // Xbox One S pre-FW update reports Xbox button as SystemMainMenu over BT.
    {kGenericDesktopUsagePage, kSystemMainMenuUsageNumber},
    // Power is used for the Guide button on the Nvidia Shield 2015 gamepad.
    {kConsumerUsagePage, kPowerUsageNumber},
    // Search is used for the Guide button on the Nvidia Shield 2017 gamepad.
    {kConsumerUsagePage, kSearchUsageNumber},
    // Start, Back, and Guide buttons are often reported as Consumer Home or
    // Back.
    {kConsumerUsagePage, kHomeUsageNumber},
    {kConsumerUsagePage, kBackUsageNumber},
    {kConsumerUsagePage, kRecordUsageNumber},
};
const size_t kSpecialUsagesLen = std::size(kSpecialUsages);

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;
}

GamepadBusType QueryBusType(IOHIDDeviceRef device) {
  CFStringRef transport_cf = base::apple::CFCast<CFStringRef>(
      IOHIDDeviceGetProperty(device, CFSTR(kIOHIDTransportKey)));
  if (transport_cf) {
    std::string transport = base::SysCFStringRefToUTF8(transport_cf);
    if (transport == kIOHIDTransportUSBValue)
      return GAMEPAD_BUS_USB;
    if (transport == kIOHIDTransportBluetoothValue ||
        transport == kIOHIDTransportBluetoothLowEnergyValue) {
      return GAMEPAD_BUS_BLUETOOTH;
    }
  }
  return GAMEPAD_BUS_UNKNOWN;
}

}  // namespace

GamepadDeviceMac::GamepadDeviceMac(int location_id,
                                   IOHIDDeviceRef device_ref,
                                   std::string_view product_name,
                                   int vendor_id,
                                   int product_id)
    : location_id_(location_id),
      device_ref_(device_ref),
      bus_type_(QueryBusType(device_ref_)),
      ff_device_ref_(nullptr),
      ff_effect_ref_(nullptr) {
  auto gamepad_id =
      GamepadIdList::Get().GetGamepadId(product_name, vendor_id, product_id);

  if (Dualshock4Controller::IsDualshock4(gamepad_id)) {
    dualshock4_ = std::make_unique<Dualshock4Controller>(
        gamepad_id, bus_type_, std::make_unique<HidWriterMac>(device_ref));
    return;
  }

  if (XboxHidController::IsXboxHid(gamepad_id)) {
    xbox_hid_ = std::make_unique<XboxHidController>(
        std::make_unique<HidWriterMac>(device_ref));
    return;
  }

  if (HidHapticGamepad::IsHidHaptic(vendor_id, product_id)) {
    hid_haptics_ = HidHapticGamepad::Create(
        vendor_id, product_id, std::make_unique<HidWriterMac>(device_ref));
    return;
  }

  if (device_ref) {
    ff_device_ref_ = CreateForceFeedbackDevice(device_ref);
    if (ff_device_ref_) {
      ff_effect_ref_ = CreateForceFeedbackEffect(ff_device_ref_, &ff_effect_,
                                                 &ff_custom_force_, force_data_,
                                                 axes_data_, direction_data_);
    }
  }
}

GamepadDeviceMac::~GamepadDeviceMac() = default;

void GamepadDeviceMac::DoShutdown() {
  if (ff_device_ref_) {
    if (ff_effect_ref_) {
      FFDeviceReleaseEffect(ff_device_ref_, ff_effect_ref_);
      ff_effect_ref_ = nullptr;
    }
    FFReleaseDevice(ff_device_ref_);
    ff_device_ref_ = nullptr;
  }
  if (dualshock4_)
    dualshock4_->Shutdown();
  dualshock4_.reset();
  if (xbox_hid_)
    xbox_hid_->Shutdown();
  xbox_hid_.reset();
  if (hid_haptics_)
    hid_haptics_->Shutdown();
  hid_haptics_.reset();
}

// 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)) != nullptr) {
    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) {
  bool has_buttons = AddButtons(gamepad);
  bool has_axes = AddAxes(gamepad);
  gamepad->timestamp = GamepadDataFetcher::CurrentTimeInMicroseconds();
  return (has_buttons || has_axes);
}

bool GamepadDeviceMac::AddButtons(Gamepad* gamepad) {
  DCHECK(gamepad);
  memset(gamepad->buttons, 0, sizeof(gamepad->buttons));
  std::fill(button_elements_, button_elements_ + Gamepad::kButtonsLengthCap,
            nullptr);

  base::apple::ScopedCFTypeRef<CFArrayRef> elements(
      IOHIDDeviceCopyMatchingElements(device_ref_, /*matching=*/nullptr,
                                      kIOHIDOptionsTypeNone));
  if (!elements) {
    // IOHIDDeviceCopyMatchingElements returns nullptr if we don't have
    // permission to access the referenced IOHIDDevice.
    return false;
  }

  std::vector<IOHIDElementRef> special_element(kSpecialUsagesLen, nullptr);
  size_t button_count = 0;
  size_t unmapped_button_count = 0;

  for (CFIndex i = 0; i < CFArrayGetCount(elements.get()); ++i) {
    IOHIDElementRef element =
        (IOHIDElementRef)CFArrayGetValueAtIndex(elements.get(), i);
    if (!CheckCollection(element))
      continue;

    uint32_t usage_page = IOHIDElementGetUsagePage(element);
    uint32_t usage = IOHIDElementGetUsage(element);
    if (IOHIDElementGetType(element) == kIOHIDElementTypeInput_Button) {
      if (usage_page == kButtonUsagePage && usage > 0) {
        size_t button_index = size_t{usage - 1};

        // Ignore buttons with large usage values.
        if (button_index >= Gamepad::kButtonsLengthCap)
          continue;

        // Button index already assigned, ignore.
        if (button_elements_[button_index])
          continue;

        button_elements_[button_index] = element;
        gamepad->buttons[button_index].used = true;
        button_count = std::max(button_count, button_index + 1);
      } else {
        // Check for common gamepad buttons that are not on the Button usage
        // page. Button indices are assigned in a second pass.
        for (size_t special_index = 0; special_index < kSpecialUsagesLen;
             ++special_index) {
          const auto& special = kSpecialUsages[special_index];
          if (usage_page == special.usage_page && usage == special.usage) {
            special_element[special_index] = element;
            ++unmapped_button_count;
          }
        }
      }
    }
  }

  if (unmapped_button_count > 0) {
    // Insert unmapped buttons at unused button indices.
    size_t button_index = 0;
    for (size_t special_index = 0; special_index < kSpecialUsagesLen;
         ++special_index) {
      if (!special_element[special_index])
        continue;

      // Advance to the next unused button index.
      while (button_index < Gamepad::kButtonsLengthCap &&
             button_elements_[button_index]) {
        ++button_index;
      }
      if (button_index >= Gamepad::kButtonsLengthCap)
        break;

      button_elements_[button_index] = special_element[special_index];
      gamepad->buttons[button_index].used = true;
      button_count = std::max(button_count, button_index + 1);

      if (--unmapped_button_count == 0)
        break;
    }
  }

  gamepad->buttons_length = button_count;
  return gamepad->buttons_length > 0;
}

bool GamepadDeviceMac::AddAxes(Gamepad* gamepad) {
  DCHECK(gamepad);
  memset(gamepad->axes, 0, sizeof(gamepad->axes));
  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);

  base::apple::ScopedCFTypeRef<CFArrayRef> elements(
      IOHIDDeviceCopyMatchingElements(device_ref_, /*matching=*/nullptr,
                                      kIOHIDOptionsTypeNone));
  if (!elements) {
    // IOHIDDeviceCopyMatchingElements returns nullptr if we don't have
    // permission to access the referenced IOHIDDevice.
    return false;
  }

  // Most axes are mapped so that their index in the Gamepad axes array
  // corresponds to the usage ID. However, this is not possible when the usage
  // ID would cause the axis index to exceed the bounds of the axes array.
  // Axes with large usage IDs are mapped in a second pass.
  size_t axis_count = 0;
  size_t unmapped_axis_count = 0;

  for (CFIndex i = 0; i < CFArrayGetCount(elements.get()); ++i) {
    IOHIDElementRef element =
        (IOHIDElementRef)CFArrayGetValueAtIndex(elements.get(), i);
    if (!CheckCollection(element))
      continue;

    uint32_t usage_page = IOHIDElementGetUsagePage(element);
    uint32_t usage = IOHIDElementGetUsage(element);
    if (IOHIDElementGetType(element) != kIOHIDElementTypeInput_Misc ||
        usage < kAxisMinimumUsageNumber) {
      continue;
    }

    size_t axis_index = size_t{usage - kAxisMinimumUsageNumber};
    if (axis_index < Gamepad::kAxesLengthCap) {
      // Axis index already assigned, ignore.
      if (axis_elements_[axis_index])
        continue;
      axis_elements_[axis_index] = element;
      axis_count = std::max(axis_count, axis_index + 1);
    } else if (usage_page <= kGameControlsUsagePage) {
      // Assign an index for this axis in the second pass.
      ++unmapped_axis_count;
    }
  }

  if (unmapped_axis_count > 0) {
    // Insert unmapped axes at unused axis indices.
    size_t axis_index = 0;
    for (CFIndex i = 0; i < CFArrayGetCount(elements.get()); ++i) {
      IOHIDElementRef element =
          (IOHIDElementRef)CFArrayGetValueAtIndex(elements.get(), i);
      if (!CheckCollection(element))
        continue;

      uint32_t usage_page = IOHIDElementGetUsagePage(element);
      uint32_t usage = IOHIDElementGetUsage(element);
      if (IOHIDElementGetType(element) != kIOHIDElementTypeInput_Misc ||
          usage < kAxisMinimumUsageNumber ||
          usage_page > kGameControlsUsagePage) {
        continue;
      }

      // Ignore axes with small usage IDs that should have been mapped in the
      // initial pass.
      if (size_t{usage - kAxisMinimumUsageNumber} < Gamepad::kAxesLengthCap)
        continue;

      // Advance to the next unused axis index.
      while (axis_index < Gamepad::kAxesLengthCap &&
             axis_elements_[axis_index]) {
        ++axis_index;
      }
      if (axis_index >= Gamepad::kAxesLengthCap)
        break;

      axis_elements_[axis_index] = element;
      axis_count = std::max(axis_count, axis_index + 1);

      if (--unmapped_axis_count == 0)
        break;
    }
  }

  // Fetch the logical range and report size for each axis.
  for (size_t axis_index = 0; axis_index < axis_count; ++axis_index) {
    IOHIDElementRef element = axis_elements_[axis_index];
    if (element != nullptr) {
      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);

      gamepad->axes_used |= 1 << axis_index;
    }
  }

  gamepad->axes_length = axis_count;
  return gamepad->axes_length > 0;
}

void GamepadDeviceMac::UpdateGamepadForValue(IOHIDValueRef value,
                                             Gamepad* gamepad) {
  DCHECK(gamepad);
  IOHIDElementRef element = IOHIDValueGetElement(value);
  uint32_t value_length = IOHIDValueGetLength(value);

  if (dualshock4_) {
    // Handle Dualshock4 input reports that do not specify HID gamepad usages
    // in the report descriptor.
    uint32_t report_id = IOHIDElementGetReportID(element);
    auto report = base::make_span(IOHIDValueGetBytePtr(value), value_length);
    if (dualshock4_->ProcessInputReport(report_id, report, gamepad))
      return;
  }

  // Values larger than 4 bytes cannot be handled by IOHIDValueGetIntegerValue.
  if (value_length > 4)
    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 = GamepadDataFetcher::CurrentTimeInMicroseconds();
      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 = GamepadDataFetcher::CurrentTimeInMicroseconds();
      return;
    }
  }
}

bool GamepadDeviceMac::SupportsVibration() {
  return dualshock4_ || xbox_hid_ || hid_haptics_ || ff_device_ref_;
}

void GamepadDeviceMac::SetVibration(mojom::GamepadEffectParametersPtr params) {
  if (dualshock4_) {
    dualshock4_->SetVibration(std::move(params));
    return;
  }

  if (xbox_hid_) {
    xbox_hid_->SetVibration(std::move(params));
    return;
  }

  if (hid_haptics_) {
    hid_haptics_->SetVibration(std::move(params));
    return;
  }

  if (ff_device_ref_) {
    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>(params->strong_magnitude * kRumbleMagnitudeMax);
    ff_custom_force->rglForceData[1] =
        static_cast<LONG>(params->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() {
  if (dualshock4_) {
    dualshock4_->SetZeroVibration();
    return;
  }

  if (xbox_hid_) {
    xbox_hid_->SetZeroVibration();
    return;
  }

  if (hid_haptics_) {
    hid_haptics_->SetZeroVibration();
    return;
  }

  if (ff_effect_ref_)
    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;
}

base::WeakPtr<AbstractHapticGamepad> GamepadDeviceMac::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

}  // namespace device