blob: 13f2ded894ec50780e1281f2ea461b719558b225 [file] [log] [blame]
// Copyright 2019 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/vr/windows_mixed_reality/mixed_reality_input_helper.h"
#include <windows.perception.h>
#include <windows.perception.spatial.h>
#include <windows.ui.input.spatial.h>
#include <wrl.h>
#include <wrl/event.h>
#include <unordered_map>
#include <vector>
#include "device/gamepad/public/cpp/gamepads.h"
#include "device/vr/public/mojom/isolated_xr_service.mojom.h"
#include "device/vr/windows_mixed_reality/wrappers/wmr_input_location.h"
#include "device/vr/windows_mixed_reality/wrappers/wmr_input_manager.h"
#include "device/vr/windows_mixed_reality/wrappers/wmr_input_source.h"
#include "device/vr/windows_mixed_reality/wrappers/wmr_input_source_state.h"
#include "device/vr/windows_mixed_reality/wrappers/wmr_pointer_pose.h"
#include "device/vr/windows_mixed_reality/wrappers/wmr_pointer_source_pose.h"
#include "ui/gfx/transform.h"
#include "ui/gfx/transform_util.h"
namespace device {
// We want to differentiate from gfx::Members, so we're not going to explicitly
// use anything from Windows::Foundation::Numerics
namespace WFN = ABI::Windows::Foundation::Numerics;
using Handedness =
ABI::Windows::UI::Input::Spatial::SpatialInteractionSourceHandedness;
using PressKind = ABI::Windows::UI::Input::Spatial::SpatialInteractionPressKind;
using SourceKind =
ABI::Windows::UI::Input::Spatial::SpatialInteractionSourceKind;
using ABI::Windows::Foundation::ITypedEventHandler;
using ABI::Windows::Perception::IPerceptionTimestamp;
using ABI::Windows::Perception::Spatial::ISpatialCoordinateSystem;
using ABI::Windows::UI::Input::Spatial::ISpatialInteractionManager;
using ABI::Windows::UI::Input::Spatial::ISpatialInteractionSourceEventArgs;
using ABI::Windows::UI::Input::Spatial::ISpatialInteractionSourceEventArgs2;
using ABI::Windows::UI::Input::Spatial::ISpatialInteractionSourceState;
using ABI::Windows::UI::Input::Spatial::SpatialInteractionManager;
using ABI::Windows::UI::Input::Spatial::SpatialInteractionSourceEventArgs;
using Microsoft::WRL::Callback;
using Microsoft::WRL::ComPtr;
typedef ITypedEventHandler<SpatialInteractionManager*,
SpatialInteractionSourceEventArgs*>
SpatialInteractionSourceEventHandler;
ParsedInputState::ParsedInputState() {}
ParsedInputState::~ParsedInputState() {}
ParsedInputState::ParsedInputState(ParsedInputState&& other) = default;
namespace {
constexpr double kDeadzoneMinimum = 0.1;
void AddButton(mojom::XRGamepadPtr& gamepad, ButtonData* data) {
if (data) {
auto button = mojom::XRGamepadButton::New();
button->pressed = data->pressed;
button->touched = data->touched;
button->value = data->value;
gamepad->buttons.push_back(std::move(button));
} else {
gamepad->buttons.push_back(mojom::XRGamepadButton::New());
}
}
// These methods are only called for the thumbstick and touchpad, which both
// have an X and Y.
void AddAxes(mojom::XRGamepadPtr& gamepad, ButtonData data) {
gamepad->axes.push_back(
std::fabs(data.x_axis) < kDeadzoneMinimum ? 0 : data.x_axis);
gamepad->axes.push_back(
std::fabs(data.y_axis) < kDeadzoneMinimum ? 0 : data.y_axis);
}
void AddButtonAndAxes(mojom::XRGamepadPtr& gamepad, ButtonData data) {
AddButton(gamepad, &data);
AddAxes(gamepad, data);
}
std::vector<float> ConvertToVector(GamepadVector vector) {
return {vector.x, vector.y, vector.z};
}
std::vector<float> ConvertToVector(GamepadQuaternion quat) {
return {quat.x, quat.y, quat.z, quat.w};
}
mojom::VRPosePtr ConvertToVRPose(GamepadPose gamepad_pose) {
if (!gamepad_pose.not_null)
return nullptr;
auto pose = mojom::VRPose::New();
if (gamepad_pose.orientation.not_null)
pose->orientation = ConvertToVector(gamepad_pose.orientation);
if (gamepad_pose.position.not_null)
pose->position = ConvertToVector(gamepad_pose.position);
if (gamepad_pose.angular_velocity.not_null)
pose->angularVelocity = ConvertToVector(gamepad_pose.angular_velocity);
if (gamepad_pose.linear_velocity.not_null)
pose->linearVelocity = ConvertToVector(gamepad_pose.linear_velocity);
if (gamepad_pose.angular_acceleration.not_null)
pose->angularAcceleration =
ConvertToVector(gamepad_pose.angular_acceleration);
if (gamepad_pose.linear_acceleration.not_null)
pose->linearAcceleration =
ConvertToVector(gamepad_pose.linear_acceleration);
return pose;
}
GamepadQuaternion ConvertToGamepadQuaternion(WFN::Quaternion quat) {
GamepadQuaternion gamepad_quaternion;
gamepad_quaternion.not_null = true;
gamepad_quaternion.x = quat.X;
gamepad_quaternion.y = quat.Y;
gamepad_quaternion.z = quat.Z;
gamepad_quaternion.w = quat.W;
return gamepad_quaternion;
}
GamepadVector ConvertToGamepadVector(WFN::Vector3 vec3) {
GamepadVector gamepad_vector;
gamepad_vector.not_null = true;
gamepad_vector.x = vec3.X;
gamepad_vector.y = vec3.Y;
gamepad_vector.z = vec3.Z;
return gamepad_vector;
}
mojom::XRGamepadPtr GetWebVRGamepad(ParsedInputState input_state) {
auto gamepad = mojom::XRGamepad::New();
// This matches the order of button trigger events from Edge. Note that we
// use the polled button state for select here. Voice (which we cannot get
// via polling), lacks enough data to be considered a "Gamepad", and if we
// used eventing the pressed state may be inconsistent.
AddButtonAndAxes(gamepad, input_state.button_data[ButtonName::kThumbstick]);
AddButton(gamepad, &input_state.button_data[ButtonName::kSelect]);
AddButton(gamepad, &input_state.button_data[ButtonName::kGrip]);
AddButton(gamepad, nullptr); // Nothing seems to trigger this button in Edge.
AddButtonAndAxes(gamepad, input_state.button_data[ButtonName::kTouchpad]);
gamepad->pose = ConvertToVRPose(input_state.gamepad_pose);
gamepad->hand = input_state.source_state->description->handedness;
gamepad->controller_id = input_state.source_state->source_id;
gamepad->can_provide_position = true;
gamepad->can_provide_orientation = true;
gamepad->timestamp = base::TimeTicks::Now();
return gamepad;
}
// Note that since this is built by polling, and so eventing changes are not
// accounted for here.
std::unordered_map<ButtonName, ButtonData> ParseButtonState(
const WMRInputSourceState& source_state) {
std::unordered_map<ButtonName, ButtonData> button_map;
ButtonData data = button_map[ButtonName::kSelect];
data.pressed = source_state.IsSelectPressed();
data.touched = data.pressed;
data.value = source_state.SelectPressedValue();
button_map[ButtonName::kSelect] = data;
data = button_map[ButtonName::kGrip];
data.pressed = source_state.IsGrasped();
data.touched = data.pressed;
data.value = data.pressed ? 1.0 : 0.0;
button_map[ButtonName::kGrip] = data;
if (!source_state.SupportsControllerProperties())
return button_map;
data = button_map[ButtonName::kThumbstick];
data.pressed = source_state.IsThumbstickPressed();
data.touched = data.pressed;
data.value = data.pressed ? 1.0 : 0.0;
data.x_axis = source_state.ThumbstickX();
data.y_axis = source_state.ThumbstickY();
button_map[ButtonName::kThumbstick] = data;
data = button_map[ButtonName::kTouchpad];
data.pressed = source_state.IsTouchpadPressed();
data.touched = source_state.IsTouchpadTouched();
data.value = data.pressed ? 1.0 : 0.0;
// Touchpad must be touched if it is pressed
if (data.pressed && !data.touched)
data.touched = true;
if (data.touched) {
data.x_axis = source_state.TouchpadX();
data.y_axis = source_state.TouchpadY();
}
button_map[ButtonName::kTouchpad] = data;
return button_map;
}
GamepadPose GetGamepadPose(const WMRInputLocation& location) {
GamepadPose gamepad_pose;
WFN::Quaternion quat;
if (location.TryGetOrientation(&quat)) {
gamepad_pose.not_null = true;
gamepad_pose.orientation = ConvertToGamepadQuaternion(quat);
}
WFN::Vector3 vec3;
if (location.TryGetPosition(&vec3)) {
gamepad_pose.not_null = true;
gamepad_pose.position = ConvertToGamepadVector(vec3);
}
if (location.TryGetVelocity(&vec3)) {
gamepad_pose.not_null = true;
gamepad_pose.linear_velocity = ConvertToGamepadVector(vec3);
}
if (location.TryGetAngularVelocity(&vec3)) {
gamepad_pose.not_null = true;
gamepad_pose.angular_velocity = ConvertToGamepadVector(vec3);
}
return gamepad_pose;
}
gfx::Transform CreateTransform(GamepadVector position,
GamepadQuaternion rotation) {
gfx::DecomposedTransform decomposed_transform;
decomposed_transform.translate[0] = position.x;
decomposed_transform.translate[1] = position.y;
decomposed_transform.translate[2] = position.z;
decomposed_transform.quaternion =
gfx::Quaternion(rotation.x, rotation.y, rotation.z, rotation.w);
return gfx::ComposeTransform(decomposed_transform);
}
bool TryGetPointerOffset(const WMRInputSourceState& state,
const WMRInputSource& source,
ComPtr<ISpatialCoordinateSystem> origin,
gfx::Transform origin_from_grip,
gfx::Transform* grip_from_pointer) {
DCHECK(grip_from_pointer);
*grip_from_pointer = gfx::Transform();
if (!origin)
return false;
// We can get the pointer position, but we'll need to transform it to an
// offset from the grip position. If we can't get an inverse of that,
// then go ahead and bail early.
gfx::Transform grip_from_origin;
if (!origin_from_grip.GetInverse(&grip_from_origin))
return false;
bool pointing_supported = source.IsPointingSupported();
WMRPointerPose pointer_pose(nullptr);
if (!state.TryGetPointerPose(origin, &pointer_pose))
return false;
WFN::Vector3 pos;
WFN::Quaternion rot;
if (pointing_supported) {
WMRPointerSourcePose pointer_source_pose(nullptr);
if (!pointer_pose.TryGetInteractionSourcePose(source, &pointer_source_pose))
return false;
pos = pointer_source_pose.Position();
rot = pointer_source_pose.Orientation();
} else {
pos = pointer_pose.HeadForward();
}
gfx::Transform origin_from_pointer = CreateTransform(
ConvertToGamepadVector(pos), ConvertToGamepadQuaternion(rot));
*grip_from_pointer = (grip_from_origin * origin_from_pointer);
return true;
}
device::mojom::XRHandedness WindowsToMojoHandedness(Handedness handedness) {
switch (handedness) {
case Handedness::SpatialInteractionSourceHandedness_Left:
return device::mojom::XRHandedness::LEFT;
case Handedness::SpatialInteractionSourceHandedness_Right:
return device::mojom::XRHandedness::RIGHT;
default:
return device::mojom::XRHandedness::NONE;
}
}
uint32_t GetSourceId(const WMRInputSource& source) {
uint32_t id = source.Id();
// Voice's ID seems to be coming through as 0, which will cause a DCHECK in
// the hash table used on the blink side. To ensure that we don't have any
// collisions with other ids, increment all of the ids by one.
id++;
DCHECK(id != 0);
return id;
}
} // namespace
MixedRealityInputHelper::MixedRealityInputHelper(HWND hwnd) : hwnd_(hwnd) {
pressed_token_.value = 0;
released_token_.value = 0;
}
MixedRealityInputHelper::~MixedRealityInputHelper() {
// Dispose must be called before destruction, which ensures that we're
// unsubscribed from events.
DCHECK(pressed_token_.value == 0);
DCHECK(released_token_.value == 0);
}
void MixedRealityInputHelper::Dispose() {
UnsubscribeEvents();
}
bool MixedRealityInputHelper::EnsureSpatialInteractionManager() {
if (input_manager_)
return true;
if (!hwnd_)
return false;
input_manager_ = WMRInputManager::GetForWindow(hwnd_);
if (!input_manager_)
return false;
SubscribeEvents();
return true;
}
std::vector<mojom::XRInputSourceStatePtr>
MixedRealityInputHelper::GetInputState(ComPtr<ISpatialCoordinateSystem> origin,
ComPtr<IPerceptionTimestamp> timestamp) {
std::vector<mojom::XRInputSourceStatePtr> input_states;
if (!timestamp || !origin || !EnsureSpatialInteractionManager())
return input_states;
base::AutoLock scoped_lock(lock_);
auto source_states = input_manager_->GetDetectedSourcesAtTimestamp(timestamp);
for (auto state : source_states) {
auto parsed_source_state = LockedParseWindowsSourceState(state, origin);
if (parsed_source_state.source_state)
input_states.push_back(std::move(parsed_source_state.source_state));
}
for (unsigned int i = 0; i < pending_voice_states_.size(); i++) {
auto parsed_source_state =
LockedParseWindowsSourceState(pending_voice_states_[i], origin);
if (parsed_source_state.source_state)
input_states.push_back(std::move(parsed_source_state.source_state));
}
pending_voice_states_.clear();
return input_states;
}
mojom::XRGamepadDataPtr MixedRealityInputHelper::GetWebVRGamepadData(
ComPtr<ISpatialCoordinateSystem> origin,
ComPtr<IPerceptionTimestamp> timestamp) {
auto ret = mojom::XRGamepadData::New();
if (!timestamp || !origin || !EnsureSpatialInteractionManager())
return ret;
base::AutoLock scoped_lock(lock_);
auto source_states = input_manager_->GetDetectedSourcesAtTimestamp(timestamp);
for (auto state : source_states) {
auto parsed_source_state = LockedParseWindowsSourceState(state, origin);
// If we have a grip, then we should have enough data.
if (parsed_source_state.source_state &&
parsed_source_state.source_state->grip)
ret->gamepads.push_back(GetWebVRGamepad(std::move(parsed_source_state)));
}
return ret;
}
ParsedInputState MixedRealityInputHelper::LockedParseWindowsSourceState(
const WMRInputSourceState& state,
ComPtr<ISpatialCoordinateSystem> origin) {
ParsedInputState input_state;
if (!origin)
return input_state;
WMRInputSource source = state.GetSource();
SourceKind source_kind = source.Kind();
bool is_controller =
(source_kind == SourceKind::SpatialInteractionSourceKind_Controller);
bool is_voice =
(source_kind == SourceKind::SpatialInteractionSourceKind_Voice);
if (!(is_controller || is_voice))
return input_state;
// Note that if this is from voice input, we're not supposed to send up the
// "grip" position, so this will be left as identity and let us still use
// the same code paths, as any transformations by it will leave the original
// item unaffected.
gfx::Transform origin_from_grip;
if (is_controller) {
input_state.button_data = ParseButtonState(state);
WMRInputLocation location_in_origin(nullptr);
if (!state.TryGetLocation(origin, &location_in_origin))
return input_state;
auto gamepad_pose = GetGamepadPose(location_in_origin);
if (!(gamepad_pose.not_null && gamepad_pose.position.not_null &&
gamepad_pose.orientation.not_null))
return input_state;
origin_from_grip =
CreateTransform(gamepad_pose.position, gamepad_pose.orientation);
input_state.gamepad_pose = gamepad_pose;
}
gfx::Transform grip_from_pointer;
if (!TryGetPointerOffset(state, source, origin, origin_from_grip,
&grip_from_pointer))
return input_state;
// Now that we know we have tracking for the object, we'll start building.
device::mojom::XRInputSourceStatePtr source_state =
device::mojom::XRInputSourceState::New();
// Hands may not have the same id especially if they are lost but since we
// are only tracking controllers/voice, this id should be consistent.
uint32_t id = GetSourceId(source);
source_state->source_id = id;
source_state->primary_input_pressed = controller_states_[id].pressed;
source_state->primary_input_clicked = controller_states_[id].clicked;
controller_states_[id].clicked = false;
// Grip position should *only* be specified for a controller.
if (is_controller) {
source_state->grip = origin_from_grip;
}
device::mojom::XRInputSourceDescriptionPtr description =
device::mojom::XRInputSourceDescription::New();
// If we've gotten this far we've gotten the real position.
description->emulated_position = false;
description->pointer_offset = grip_from_pointer;
if (is_voice) {
description->target_ray_mode = device::mojom::XRTargetRayMode::GAZING;
description->handedness = device::mojom::XRHandedness::NONE;
} else if (is_controller) {
description->target_ray_mode = device::mojom::XRTargetRayMode::POINTING;
description->handedness = WindowsToMojoHandedness(source.Handedness());
} else {
NOTREACHED();
}
source_state->description = std::move(description);
input_state.source_state = std::move(source_state);
return input_state;
}
HRESULT MixedRealityInputHelper::OnSourcePressed(
ISpatialInteractionManager* sender,
ISpatialInteractionSourceEventArgs* args) {
return ProcessSourceEvent(args, true /* is_pressed */);
}
HRESULT MixedRealityInputHelper::OnSourceReleased(
ISpatialInteractionManager* sender,
ISpatialInteractionSourceEventArgs* args) {
return ProcessSourceEvent(args, false /* is_pressed */);
}
HRESULT MixedRealityInputHelper::ProcessSourceEvent(
ISpatialInteractionSourceEventArgs* raw_args,
bool is_pressed) {
base::AutoLock scoped_lock(lock_);
ComPtr<ISpatialInteractionSourceEventArgs> args(raw_args);
ComPtr<ISpatialInteractionSourceEventArgs2> args2;
HRESULT hr = args.As(&args2);
if (FAILED(hr))
return S_OK;
PressKind press_kind;
hr = args2->get_PressKind(&press_kind);
DCHECK(SUCCEEDED(hr));
if (press_kind != PressKind::SpatialInteractionPressKind_Select)
return S_OK;
ComPtr<ISpatialInteractionSourceState> source_state_wmr;
hr = args->get_State(&source_state_wmr);
DCHECK(SUCCEEDED(hr));
WMRInputSourceState state(source_state_wmr);
WMRInputSource source = state.GetSource();
SourceKind source_kind = source.Kind();
if (source_kind != SourceKind::SpatialInteractionSourceKind_Controller &&
source_kind != SourceKind::SpatialInteractionSourceKind_Voice)
return S_OK;
uint32_t id = GetSourceId(source);
bool wasPressed = controller_states_[id].pressed;
bool wasClicked = controller_states_[id].clicked;
controller_states_[id].pressed = is_pressed;
controller_states_[id].clicked = wasClicked || (wasPressed && !is_pressed);
// Tracked controllers show up when we poll for DetectedSources, but voice
// does not.
if (source_kind == SourceKind::SpatialInteractionSourceKind_Voice &&
!is_pressed)
pending_voice_states_.push_back(std::move(state));
return S_OK;
}
void MixedRealityInputHelper::SubscribeEvents() {
DCHECK(input_manager_);
DCHECK(pressed_token_.value == 0);
DCHECK(released_token_.value == 0);
// The destructor ensures that we're unsubscribed so raw this is fine.
auto pressed_callback = Callback<SpatialInteractionSourceEventHandler>(
this, &MixedRealityInputHelper::OnSourcePressed);
HRESULT hr = input_manager_->GetComPtr()->add_SourcePressed(
pressed_callback.Get(), &pressed_token_);
DCHECK(SUCCEEDED(hr));
// The destructor ensures that we're unsubscribed so raw this is fine.
auto released_callback = Callback<SpatialInteractionSourceEventHandler>(
this, &MixedRealityInputHelper::OnSourceReleased);
hr = input_manager_->GetComPtr()->add_SourceReleased(released_callback.Get(),
&released_token_);
DCHECK(SUCCEEDED(hr));
}
void MixedRealityInputHelper::UnsubscribeEvents() {
base::AutoLock scoped_lock(lock_);
if (!input_manager_)
return;
HRESULT hr = S_OK;
if (pressed_token_.value != 0) {
hr = input_manager_->GetComPtr()->remove_SourcePressed(pressed_token_);
pressed_token_.value = 0;
DCHECK(SUCCEEDED(hr));
}
if (released_token_.value != 0) {
hr = input_manager_->GetComPtr()->remove_SourceReleased(released_token_);
released_token_.value = 0;
DCHECK(SUCCEEDED(hr));
}
}
} // namespace device