chromium/device/vr/openxr/test/openxr_test_helper.cc

// Copyright 2019 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/vr/openxr/test/openxr_test_helper.h"

#include <cmath>
#include <limits>

#include "base/containers/contains.h"
#include "device/vr/openxr/openxr_interaction_profile_paths.h"
#include "device/vr/openxr/openxr_platform.h"
#include "device/vr/openxr/openxr_util.h"
#include "device/vr/openxr/openxr_view_configuration.h"
#include "third_party/openxr/src/src/common/hex_and_handles.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gfx/geometry/transform_util.h"

namespace {
bool PathContainsString(const std::string& path, const std::string& s) {
  return base::Contains(path, s);
}

device::XrEye GetEyeForIndex(uint32_t index, uint32_t num_views) {
  DCHECK_LE(num_views, 2u);

  if (num_views == 1) {
    // Per WebXR spec, the eye for the first person observer view is none.
    return device::XrEye::kNone;
  }

  // Per OpenXR spec, the left eye is at index 0 and the right eye at index 1
  // for the XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO view configuration.
  return (index == 0) ? device::XrEye::kLeft : device::XrEye::kRight;
}

int GetOffsetMultiplierForIndex(uint32_t index) {
  return ((index % 2 == 0) ? 1 : -1);
}

}  // namespace

OpenXrTestHelper::ActionProperties::ActionProperties()
    : type(XR_ACTION_TYPE_MAX_ENUM) {}

OpenXrTestHelper::ActionProperties::~ActionProperties() = default;

OpenXrTestHelper::ActionProperties::ActionProperties(
    const ActionProperties& other) {
  this->type = other.type;
  this->profile_binding_map = other.profile_binding_map;
}

OpenXrTestHelper::OpenXrTestHelper()
    // since openxr_statics is created first, so the first instance returned
    // should be a fake one since openxr_statics does not need to use
    // test_hook_;
    : system_id_(0),
      session_(XR_NULL_HANDLE),
      swapchain_(XR_NULL_HANDLE),
      frame_count_(0),
      session_state_(XR_SESSION_STATE_UNKNOWN),
      frame_begin_(false),
      acquired_swapchain_texture_(0),
      next_handle_(0),
      next_predicted_display_time_(0),
      interaction_profile_(device::kMicrosoftMotionInteractionProfilePath) {
  // We currently only support one primary and one secondary view configs, but
  // there will likely be more added in the future to support various devices.
  // Add new ones here for testing.

  // Per spec, the primary view configuration is always active.
  primary_configs_supported_.insert(
      {XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
       device::OpenXrViewConfiguration(
           XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, true /*active*/,
           2 /*num_views*/, kPrimaryViewDimension, kSwapCount)});

  // Mark all secondary views as inactive initially until a certain number of
  // frames has passed, so we can to test the process of switching states. It's
  // a common scenario for secondary view configurations to become active or
  // inactive during a session.
  secondary_configs_supported_.insert(
      {XR_VIEW_CONFIGURATION_TYPE_SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT,
       device::OpenXrViewConfiguration(
           XR_VIEW_CONFIGURATION_TYPE_SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT,
           false /*active*/, 1 /*num_views*/, kSecondaryViewDimension,
           kSwapCount)});
}

OpenXrTestHelper::~OpenXrTestHelper() = default;

void OpenXrTestHelper::Reset() {
  session_ = XR_NULL_HANDLE;
  swapchain_ = XR_NULL_HANDLE;
  session_state_ = XR_SESSION_STATE_UNKNOWN;

  system_id_ = 0;
  frame_begin_ = false;
  d3d_device_ = nullptr;
  acquired_swapchain_texture_ = 0;
  next_handle_ = 0;
  next_predicted_display_time_ = 0;

  // vectors
  textures_arr_.clear();
  paths_.clear();

  // unordered_maps
  actions_.clear();
  action_spaces_.clear();
  reference_spaces_.clear();
  action_sets_.clear();
  attached_action_sets_.clear();
  float_action_states_.clear();
  boolean_action_states_.clear();
  v2f_action_states_.clear();
  pose_action_state_.clear();

  // unordered_sets
  action_names_.clear();
  action_localized_names_.clear();
  action_set_names_.clear();
  action_set_localized_names_.clear();
}

void OpenXrTestHelper::TestFailure() {
  NOTREACHED_IN_MIGRATION();
}

void OpenXrTestHelper::SetTestHook(device::VRTestHook* hook) {
  base::AutoLock auto_lock(lock_);
  test_hook_ = hook;
}

void OpenXrTestHelper::OnPresentedFrame() {
  DCHECK_NE(textures_arr_.size(), 0ull);

  std::vector<device::ViewData> submitted_views;
  uint32_t current_x = 0;

  for (XrViewConfigurationType view_config_type : view_configs_enabled_) {
    const device::OpenXrViewConfiguration& view_config =
        GetViewConfigInfo(view_config_type);
    if (view_config.Active()) {
      const std::vector<device::OpenXrViewProperties>& view_properties =
          view_config.Properties();
      for (uint32_t i = 0; i < view_properties.size(); i++) {
        const device::OpenXrViewProperties& properties = view_properties[i];
        device::ViewData& data = submitted_views.emplace_back();
        data.viewport =
            gfx::Rect(current_x, 0, properties.Width(), properties.Height());
        data.eye = GetEyeForIndex(i, view_properties.size());

        CopyTextureDataIntoFrameData(current_x, data);
        current_x += properties.Width();
      }
    }
  }

  base::AutoLock auto_lock(lock_);
  if (!test_hook_)
    return;

  test_hook_->OnFrameSubmitted(submitted_views);
}

void OpenXrTestHelper::CopyTextureDataIntoFrameData(uint32_t x_start,
                                                    device::ViewData& data) {
  DCHECK(d3d_device_);
  DCHECK_NE(textures_arr_.size(), 0ull);
  Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;
  d3d_device_->GetImmediateContext(&context);

  constexpr uint32_t buffer_size = sizeof(device::ViewData::raw_buffer);
  constexpr uint32_t buffer_size_pixels = buffer_size / sizeof(device::Color);

  // We copy the submitted texture to a new texture, so we can map it, and
  // read back pixel data.
  auto desc = CD3D11_TEXTURE2D_DESC();
  desc.ArraySize = 1;
  desc.Width = buffer_size_pixels;
  desc.Height = 1;
  desc.MipLevels = 1;
  desc.SampleDesc.Count = 1;
  desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
  desc.Usage = D3D11_USAGE_STAGING;
  desc.BindFlags = 0;
  desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;

  Microsoft::WRL::ComPtr<ID3D11Texture2D> texture_destination;
  HRESULT hr =
      d3d_device_->CreateTexture2D(&desc, nullptr, &texture_destination);
  DCHECK_EQ(hr, S_OK);

  // A strip of pixels along the top of the texture, however many will fit into
  // our buffer.
  D3D11_BOX box{x_start, 0, 0, x_start + buffer_size_pixels, 1, 1};
  context->CopySubresourceRegion(
      texture_destination.Get(), 0, 0, 0, 0,
      textures_arr_[acquired_swapchain_texture_].Get(), 0, &box);

  D3D11_MAPPED_SUBRESOURCE map_data = {};
  hr = context->Map(texture_destination.Get(), 0, D3D11_MAP_READ, 0, &map_data);
  DCHECK_EQ(hr, S_OK);
  // We have a 1-pixel image, so store it in the provided ViewData
  // along with the raw data.
  device::Color* color = static_cast<device::Color*>(map_data.pData);
  data.color = color[0];
  memcpy(&data.raw_buffer, map_data.pData, buffer_size);

  context->Unmap(texture_destination.Get(), 0);
}

XrSystemId OpenXrTestHelper::GetSystemId() {
  system_id_ = 1;
  return system_id_;
}

XrSystemProperties OpenXrTestHelper::GetSystemProperties() {
  return kSystemProperties;
}

XrResult OpenXrTestHelper::CreateSession(XrSession* session) {
  RETURN_IF(session_state_ != XR_SESSION_STATE_UNKNOWN,
            XR_ERROR_VALIDATION_FAILURE,
            "SessionState is not unknown before xrCreateSession");
  session_ = TreatIntegerAsHandle<XrSession>(++next_handle_);
  *session = session_;
  SetSessionState(XR_SESSION_STATE_IDLE);
  if (GetCanCreateSession()) {
    SetSessionState(XR_SESSION_STATE_READY);
  } else {
    SetSessionState(XR_SESSION_STATE_EXITING);
  }
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::DestroySession(XrSession session) {
  RETURN_IF_XR_FAILED(ValidateSession(session));

  // Clear the test helper state so that tests can request multiple sessions.
  Reset();

  return XR_SUCCESS;
}

XrSwapchain OpenXrTestHelper::CreateSwapchain() {
  // Our OpenXR backend currently only creates one swapchain at a time, so any
  // previously created swapchain must have been destroyed.
  DCHECK_EQ(swapchain_, static_cast<XrSwapchain>(XR_NULL_HANDLE));
  swapchain_ = TreatIntegerAsHandle<XrSwapchain>(++next_handle_);
  return swapchain_;
}

XrResult OpenXrTestHelper::DestroySwapchain(XrSwapchain swapchain) {
  RETURN_IF_XR_FAILED(ValidateSwapchain(swapchain));
  swapchain_ = XR_NULL_HANDLE;
  return XR_SUCCESS;
}

XrInstance OpenXrTestHelper::CreateInstance() {
  return reinterpret_cast<XrInstance>(this);
}

XrResult OpenXrTestHelper::DestroyInstance(XrInstance instance) {
  RETURN_IF_XR_FAILED(ValidateInstance(instance));
  // Though Reset() primarily clears variables relating to being able to create
  // a new session, some tests may instead destroy the device (to simulate a
  // crash or simply removing the headset). It is impossible to keep an active
  // session with a destroyed instance, so this ensures that the test helper is
  // setup to allow a new session to be requested.
  Reset();
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::GetActionStateFloat(XrAction action,
                                               XrActionStateFloat* data) const {
  RETURN_IF_XR_FAILED(ValidateAction(action));
  const ActionProperties& cur_action_properties = actions_.at(action);
  RETURN_IF(cur_action_properties.type != XR_ACTION_TYPE_FLOAT_INPUT,
            XR_ERROR_ACTION_TYPE_MISMATCH, "XrActionStateFloat type mismatch");
  RETURN_IF(data == nullptr, XR_ERROR_VALIDATION_FAILURE,
            "XrActionStateFloat is nullptr");
  *data = float_action_states_.at(action);
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::GetActionStateBoolean(
    XrAction action,
    XrActionStateBoolean* data) const {
  RETURN_IF_XR_FAILED(ValidateAction(action));
  const ActionProperties& cur_action_properties = actions_.at(action);
  RETURN_IF(cur_action_properties.type != XR_ACTION_TYPE_BOOLEAN_INPUT,
            XR_ERROR_ACTION_TYPE_MISMATCH,
            "GetActionStateBoolean type mismatch");
  RETURN_IF(data == nullptr, XR_ERROR_VALIDATION_FAILURE,
            "XrActionStateBoolean is nullptr");
  *data = boolean_action_states_.at(action);
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::GetActionStateVector2f(
    XrAction action,
    XrActionStateVector2f* data) const {
  RETURN_IF_XR_FAILED(ValidateAction(action));
  const ActionProperties& cur_action_properties = actions_.at(action);
  RETURN_IF(cur_action_properties.type != XR_ACTION_TYPE_VECTOR2F_INPUT,
            XR_ERROR_ACTION_TYPE_MISMATCH,
            "GetActionStateVector2f type mismatch");
  RETURN_IF(data == nullptr, XR_ERROR_VALIDATION_FAILURE,
            "XrActionStateVector2f is nullptr");
  *data = v2f_action_states_.at(action);
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::GetActionStatePose(XrAction action,
                                              XrActionStatePose* data) const {
  RETURN_IF_XR_FAILED(ValidateAction(action));
  const ActionProperties& cur_action_properties = actions_.at(action);
  RETURN_IF(cur_action_properties.type != XR_ACTION_TYPE_POSE_INPUT,
            XR_ERROR_ACTION_TYPE_MISMATCH,
            "GetActionStateVector2f type mismatch");
  RETURN_IF(data == nullptr, XR_ERROR_VALIDATION_FAILURE,
            "XrActionStatePose is nullptr");
  *data = pose_action_state_.at(action);
  return XR_SUCCESS;
}

XrSpace OpenXrTestHelper::CreateReferenceSpace(XrReferenceSpaceType type) {
  XrSpace cur_space = TreatIntegerAsHandle<XrSpace>(++next_handle_);
  switch (type) {
    case XR_REFERENCE_SPACE_TYPE_VIEW:
      reference_spaces_[cur_space] = kViewReferenceSpacePath;
      break;
    case XR_REFERENCE_SPACE_TYPE_LOCAL:
      reference_spaces_[cur_space] = kLocalReferenceSpacePath;
      break;
    case XR_REFERENCE_SPACE_TYPE_STAGE:
      reference_spaces_[cur_space] = kStageReferenceSpacePath;
      break;
    case XR_REFERENCE_SPACE_TYPE_UNBOUNDED_MSFT:
      reference_spaces_[cur_space] = kUnboundedReferenceSpacePath;
      break;
    default:
      NOTREACHED_IN_MIGRATION() << "Unsupported XrReferenceSpaceType: " << type;
  }
  return cur_space;
}

XrResult OpenXrTestHelper::CreateActionSpace(
    const XrActionSpaceCreateInfo& action_space_create_info,
    XrSpace* space) {
  RETURN_IF_XR_FAILED(ValidateActionSpaceCreateInfo(action_space_create_info));
  *space = TreatIntegerAsHandle<XrSpace>(++next_handle_);
  action_spaces_[*space] = action_space_create_info.action;
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::DestroySpace(XrSpace space) {
  RETURN_IF_XR_FAILED(ValidateSpace(space));
  reference_spaces_.erase(space) || action_spaces_.erase(space);
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::CreateAction(XrActionSet action_set,
                                        const XrActionCreateInfo& create_info,
                                        XrAction* action) {
  RETURN_IF_XR_FAILED(ValidateActionSet(action_set));
  RETURN_IF_XR_FAILED(ValidateActionSetNotAttached(action_set));
  RETURN_IF_XR_FAILED(ValidateActionCreateInfo(create_info));
  action_names_.emplace(create_info.actionName);
  action_localized_names_.emplace(create_info.localizedActionName);
  // The OpenXR Loader will return an error if the action handle is 0.
  XrAction cur_action = TreatIntegerAsHandle<XrAction>(++next_handle_);
  ActionProperties cur_action_properties;
  cur_action_properties.type = create_info.actionType;
  switch (create_info.actionType) {
    case XR_ACTION_TYPE_FLOAT_INPUT: {
      float_action_states_[cur_action];
      break;
    }
    case XR_ACTION_TYPE_BOOLEAN_INPUT: {
      boolean_action_states_[cur_action];
      break;
    }
    case XR_ACTION_TYPE_VECTOR2F_INPUT: {
      v2f_action_states_[cur_action];
      break;
    }
    case XR_ACTION_TYPE_POSE_INPUT: {
      pose_action_state_[cur_action];
      break;
    }
    default: {
      LOG(ERROR)
          << __FUNCTION__
          << "This type of Action is not supported by test at the moment";
    }
  }

  action_sets_[action_set].push_back(cur_action);
  actions_[cur_action] = cur_action_properties;
  RETURN_IF(action == nullptr, XR_ERROR_VALIDATION_FAILURE,
            "XrAction is nullptr");
  *action = cur_action;
  return XR_SUCCESS;
}

XrActionSet OpenXrTestHelper::CreateActionSet(
    const XrActionSetCreateInfo& create_info) {
  action_set_names_.emplace(create_info.actionSetName);
  action_set_localized_names_.emplace(create_info.localizedActionSetName);
  // The OpenXR Loader will return an error if the action set handle is 0.
  XrActionSet cur_action_set =
      TreatIntegerAsHandle<XrActionSet>(++next_handle_);
  action_sets_[cur_action_set];
  return cur_action_set;
}

XrResult OpenXrTestHelper::DestroyActionSet(XrActionSet action_set) {
  RETURN_IF_XR_FAILED(ValidateActionSet(action_set));
  action_sets_.erase(action_set);
  return XR_SUCCESS;
}

XrPath OpenXrTestHelper::GetPath(std::string path_string) {
  for (auto it = paths_.begin(); it != paths_.end(); it++) {
    if (it->compare(path_string) == 0) {
      return it - paths_.begin() + 1;
    }
  }
  paths_.emplace_back(path_string);
  // path can't be 0 since 0 is reserved for XR_NULL_HANDLE
  return paths_.size();
}

XrPath OpenXrTestHelper::GetCurrentInteractionProfile() {
  return GetPath(interaction_profile_);
}

XrHandTrackerEXT OpenXrTestHelper::CreateHandTracker(XrHandEXT hand) {
  switch (hand) {
    case XR_HAND_LEFT_EXT:
      DCHECK_EQ(left_hand_, static_cast<XrHandTrackerEXT>(XR_NULL_HANDLE));
      left_hand_ = TreatIntegerAsHandle<XrHandTrackerEXT>(++next_handle_);
      return left_hand_;
    case XR_HAND_RIGHT_EXT:
      DCHECK_EQ(right_hand_, static_cast<XrHandTrackerEXT>(XR_NULL_HANDLE));
      right_hand_ = TreatIntegerAsHandle<XrHandTrackerEXT>(++next_handle_);
      return right_hand_;
    default:
      NOTREACHED();
  }
}

XrResult OpenXrTestHelper::DestroyHandTracker(XrHandTrackerEXT hand_tracker) {
  RETURN_IF_XR_FAILED(ValidateHandTracker(hand_tracker));
  if (left_hand_ == hand_tracker) {
    left_hand_ = XR_NULL_HANDLE;
  } else if (right_hand_ == hand_tracker) {
    right_hand_ = XR_NULL_HANDLE;
  }

  return XR_SUCCESS;
}

device::OpenXrViewConfiguration& OpenXrTestHelper::GetViewConfigInfo(
    XrViewConfigurationType view_config) {
  const auto& primary_config = primary_configs_supported_.find(view_config);
  if (primary_config != primary_configs_supported_.end()) {
    return primary_config->second;
  }

  const auto& secondary_config = secondary_configs_supported_.find(view_config);
  // The view configuration type should have been validated by the caller.
  CHECK(secondary_config != secondary_configs_supported_.end());

  return secondary_config->second;
}

std::vector<XrViewConfigurationType> OpenXrTestHelper::SupportedViewConfigs()
    const {
  std::vector<XrViewConfigurationType> view_configs;
  for (auto& view_config : primary_configs_supported_) {
    view_configs.push_back(view_config.first);
  }
  for (auto& view_config : secondary_configs_supported_) {
    view_configs.push_back(view_config.first);
  }

  return view_configs;
}

XrResult OpenXrTestHelper::GetSecondaryConfigStates(
    uint32_t count,
    XrSecondaryViewConfigurationStateMSFT* states) const {
  // The number of secondary view configurations is the number of total views
  // minus the primary view configuration (there is always exactly one primary
  // config)
  RETURN_IF(count != view_configs_enabled_.size() - 1,
            XR_ERROR_SIZE_INSUFFICIENT,
            "XrSecondaryViewConfigurationFrameStateMSFT "
            "viewConfigurationCount insufficient");

  // Start at 1, since the primary view is always added first in BeginSession.
  for (uint32_t i = 1; i < view_configs_enabled_.size(); i++) {
    const device::OpenXrViewConfiguration& view_config =
        secondary_configs_supported_.find(view_configs_enabled_[i])->second;
    states[i - 1] = {XR_TYPE_SECONDARY_VIEW_CONFIGURATION_STATE_MSFT, nullptr,
                     view_config.Type(), view_config.Active()};
  }

  return XR_SUCCESS;
}

XrViewConfigurationType OpenXrTestHelper::PrimaryViewConfig() const {
  return view_configs_enabled_[0];
}

XrResult OpenXrTestHelper::BeginSession(
    const std::vector<XrViewConfigurationType>& view_configs) {
  RETURN_IF(IsSessionRunning(), XR_ERROR_SESSION_RUNNING,
            "Session is already running");
  RETURN_IF(session_state_ != XR_SESSION_STATE_READY,
            XR_ERROR_SESSION_NOT_READY,
            "Session is not XR_ERROR_SESSION_NOT_READY");

  // xrBeginSession in the fake OpenXR runtime should have added the primary
  // view configuration first.
  if (!base::Contains(primary_configs_supported_, view_configs[0])) {
    return XR_ERROR_VIEW_CONFIGURATION_TYPE_UNSUPPORTED;
  }

  // Add the primary view configuration first - this assumption is made in other
  // areas of the code.
  view_configs_enabled_.push_back(view_configs[0]);

  // Process the rest of the view configurations, which should all be secondary.
  for (uint32_t i = 1; i < view_configs.size(); i++) {
    if (!base::Contains(secondary_configs_supported_, view_configs[i])) {
      return XR_ERROR_VIEW_CONFIGURATION_TYPE_UNSUPPORTED;
    }

    // Check for additional primary view configuration.
    if (base::Contains(primary_configs_supported_, view_configs[i])) {
      return XR_ERROR_VALIDATION_FAILURE;
    }

    // Check for duplicates.
    if (base::Contains(view_configs_enabled_, view_configs[i])) {
      return XR_ERROR_VALIDATION_FAILURE;
    }

    view_configs_enabled_.push_back(view_configs[i]);
  }

  SetSessionState(XR_SESSION_STATE_FOCUSED);
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::EndSession() {
  // Per OpenXR 1.0 spec: "An application can only call xrEndSession when the
  // session is in the XR_SESSION_STATE_STOPPING state"
  RETURN_IF(session_state_ != XR_SESSION_STATE_STOPPING,
            XR_ERROR_SESSION_NOT_STOPPING,
            "Session state is not XR_ERROR_SESSION_NOT_STOPPING");
  SetSessionState(XR_SESSION_STATE_IDLE);
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::BeginFrame() {
  if (!IsSessionRunning()) {
    return XR_ERROR_SESSION_NOT_RUNNING;
  }

  if (frame_begin_) {
    return XR_FRAME_DISCARDED;
  }
  frame_begin_ = true;
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::EndFrame() {
  if (!IsSessionRunning()) {
    return XR_ERROR_SESSION_NOT_RUNNING;
  }

  if (!frame_begin_) {
    return XR_ERROR_CALL_ORDER_INVALID;
  }

  frame_begin_ = false;

  ++frame_count_;
  if (frame_count_ % 10 == 0 && view_configs_enabled_.size() > 1) {
    // Flip the active state of all secondary views every 10 frames
    for (uint32_t i = 1; i < view_configs_enabled_.size(); i++) {
      device::OpenXrViewConfiguration& view_config =
          secondary_configs_supported_.find(view_configs_enabled_[i])->second;
      view_config.SetActive(!view_config.Active());
    }

    // Re-create the D3D textures, since adding or removing a view will change
    // the width and height of the overall texture.
    ReinitializeTextures();
  }

  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::BindActionAndPath(XrPath interaction_profile_path,
                                             XrActionSuggestedBinding binding) {
  ActionProperties& current_action = actions_[binding.action];
  current_action.profile_binding_map[interaction_profile_path] =
      binding.binding;
  return XR_SUCCESS;
}

void OpenXrTestHelper::AddDimensions(
    const device::OpenXrViewConfiguration& view_config,
    uint32_t& width,
    uint32_t& height) const {
  const std::vector<device::OpenXrViewProperties>& views =
      view_config.Properties();
  for (const device::OpenXrViewProperties& view : views) {
    width += view.Width();
    height = std::max(height, view.Height());
  }
}

void OpenXrTestHelper::ReinitializeTextures() {
  DCHECK(d3d_device_);

  uint32_t total_width = 0;
  uint32_t total_height = 0;

  // The first view config in the enabled list should always be the primary
  // view configuration.
  const auto primary =
      primary_configs_supported_.find(view_configs_enabled_[0]);
  CHECK(primary != primary_configs_supported_.end());
  AddDimensions(primary->second, total_width, total_height);

  // Add secondary views
  for (uint32_t i = 1; i < view_configs_enabled_.size(); i++) {
    // There shouldn't be any more primary views enabled.
    DCHECK(primary_configs_supported_.find(view_configs_enabled_[i]) ==
           primary_configs_supported_.end());
    const auto secondary =
        secondary_configs_supported_.find(view_configs_enabled_[i]);
    CHECK(secondary != secondary_configs_supported_.end());
    if (secondary->second.Active()) {
      AddDimensions(secondary->second, total_width, total_height);
    }
  }

  CreateTextures(total_width, total_height);
}

void OpenXrTestHelper::CreateTextures(uint32_t width, uint32_t height) {
  DCHECK(d3d_device_);
  textures_arr_.clear();

  D3D11_TEXTURE2D_DESC desc{};
  desc.Width = width;
  desc.Height = height;
  desc.MipLevels = 1;
  desc.ArraySize = 1;
  desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
  desc.SampleDesc.Count = 1;
  desc.Usage = D3D11_USAGE_DEFAULT;
  desc.BindFlags = D3D11_BIND_RENDER_TARGET;

  for (uint32_t i = 0; i < kMinSwapchainBuffering; i++) {
    Microsoft::WRL::ComPtr<ID3D11Texture2D> texture;
    HRESULT hr = d3d_device_->CreateTexture2D(&desc, nullptr, &texture);
    DCHECK_EQ(hr, S_OK);

    textures_arr_.push_back(texture);
  }
}

void OpenXrTestHelper::SetD3DDevice(ID3D11Device* d3d_device) {
  DCHECK_EQ(d3d_device_, nullptr);
  DCHECK_NE(d3d_device, nullptr);
  d3d_device_ = d3d_device;

  // The device is set when the session is created. However, the view
  // configurations to enable are not specified until a session begins, so we
  // should use the default primary dimensions to create the textures. The width
  // is multiplied by 2 because WebXR uses a single double wide texture.
  CreateTextures(kPrimaryViewDimension * 2, kPrimaryViewDimension);
}

XrResult OpenXrTestHelper::AttachActionSets(
    const XrSessionActionSetsAttachInfo& attach_info) {
  RETURN_IF(attach_info.type != XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO,
            XR_ERROR_VALIDATION_FAILURE,
            "XrSessionActionSetsAttachInfo type invalid");
  RETURN_IF(attach_info.next != nullptr, XR_ERROR_VALIDATION_FAILURE,
            "XrSessionActionSetsAttachInfo next is not nullptr");
  if (attached_action_sets_.size() != 0) {
    return XR_ERROR_ACTIONSETS_ALREADY_ATTACHED;
  }

  for (uint32_t i = 0; i < attach_info.countActionSets; i++) {
    XrActionSet action_set = attach_info.actionSets[i];
    RETURN_IF_XR_FAILED(ValidateActionSet(action_set));
    attached_action_sets_[action_set] = action_sets_[action_set];
  }

  return XR_SUCCESS;
}

uint32_t OpenXrTestHelper::AttachedActionSetsSize() const {
  return attached_action_sets_.size();
}

XrResult OpenXrTestHelper::SyncActionData(XrActionSet action_set) {
  RETURN_IF_XR_FAILED(ValidateActionSet(action_set));
  RETURN_IF(ValidateActionSetNotAttached(action_set) !=
                XR_ERROR_ACTIONSETS_ALREADY_ATTACHED,
            XR_ERROR_ACTIONSET_NOT_ATTACHED,
            "XrActionSet has to be attached to the session before sync");
  const std::vector<XrAction>& actions = action_sets_[action_set];
  for (uint32_t i = 0; i < actions.size(); i++) {
    RETURN_IF_XR_FAILED(UpdateAction(actions[i]));
  }
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::UpdateAction(XrAction action) {
  RETURN_IF_XR_FAILED(ValidateAction(action));
  ActionProperties& cur_action_properties = actions_[action];
  XrPath interaction_profile_path = GetPath(interaction_profile_);

  if (cur_action_properties.profile_binding_map.count(
          interaction_profile_path) == 0) {
    // Only update actions that have binding for current interaction_profile_
    return XR_SUCCESS;
  }

  XrPath action_path =
      cur_action_properties.profile_binding_map[interaction_profile_path];
  std::string path_string = PathToString(action_path);

  bool support_path =
      PathContainsString(path_string, "/user/hand/left/input") ||
      PathContainsString(path_string, "/user/hand/right/input");
  RETURN_IF_FALSE(
      support_path, XR_ERROR_VALIDATION_FAILURE,
      "UpdateAction this action has a path that is not supported by test now");

  device::ControllerFrameData data = GetControllerDataFromPath(path_string);

  switch (cur_action_properties.type) {
    case XR_ACTION_TYPE_FLOAT_INPUT: {
      if (!(PathContainsString(path_string, "/trigger") ||
            PathContainsString(path_string, "/squeeze") ||
            PathContainsString(path_string, "/force") ||
            PathContainsString(path_string, "/value"))) {
        NOTREACHED_IN_MIGRATION()
            << "Found path with unsupported float action: " << path_string;
      }
      float_action_states_[action].isActive = data.is_valid;
      break;
    }
    case XR_ACTION_TYPE_BOOLEAN_INPUT: {
      device::XrButtonId button_id = device::kMax;
      if (PathContainsString(path_string, "/trackpad/")) {
        button_id = device::kAxisTrackpad;
      } else if (PathContainsString(path_string, "/thumbstick/")) {
        button_id = device::kAxisThumbstick;
      } else if (PathContainsString(path_string, "/trigger/")) {
        button_id = device::kAxisTrigger;
      } else if (PathContainsString(path_string, "/squeeze/")) {
        button_id = device::kGrip;
      } else if (PathContainsString(path_string, "/menu/")) {
        button_id = device::kMenu;
      } else if (PathContainsString(path_string, "/select/")) {
        // for WMR simple controller select is mapped to test type trigger
        button_id = device::kAxisTrigger;
      } else if (PathContainsString(path_string, "/thumbrest/")) {
        button_id = device::kThumbRest;
      } else if (PathContainsString(path_string, "/a/")) {
        button_id = device::kA;
      } else if (PathContainsString(path_string, "/b/")) {
        button_id = device::kB;
      } else if (PathContainsString(path_string, "/x/")) {
        button_id = device::kX;
      } else if (PathContainsString(path_string, "/y/")) {
        button_id = device::kY;
      } else if (PathContainsString(path_string, "/shoulder/")) {
        button_id = device::kShoulder;
      } else if (PathContainsString(path_string, "/pinch_ext/")) {
        button_id = device::kAxisTrigger;
      } else if (PathContainsString(path_string, "/grasp_ext/")) {
        button_id = device::kGrip;
      } else {
        NOTREACHED_IN_MIGRATION()
            << "Unrecognized boolean button: " << path_string;
      }
      uint64_t button_mask = XrButtonMaskFromId(button_id);

      // This bool pressed is needed because XrActionStateBoolean.currentState
      // is XrBool32 which is uint32_t. And XrActionStateBoolean.currentState
      // won't behave correctly if we try to set it using an uint64_t value like
      // button_mask, like: boolean_action_states_[].currentState =
      // data.buttons_pressed & button_mask
      boolean_action_states_[action].isActive = data.is_valid;
      bool button_supported = data.supported_buttons & button_mask;

      if (PathContainsString(path_string, "/value") ||
          PathContainsString(path_string, "/click")) {
        bool pressed = data.buttons_pressed & button_mask;
        boolean_action_states_[action].currentState =
            button_supported && pressed;
      } else if (PathContainsString(path_string, "/touch")) {
        bool touched = data.buttons_touched & button_mask;
        boolean_action_states_[action].currentState =
            button_supported && touched;
      } else {
        NOTREACHED_IN_MIGRATION()
            << "Boolean actions only supports path string ends with "
               "value, click, or touch";
      }
      break;
    }
    case XR_ACTION_TYPE_VECTOR2F_INPUT: {
      device::XrButtonId button_id = device::kMax;
      if (PathContainsString(path_string, "/trackpad")) {
        button_id = device::kAxisTrackpad;
      } else if (PathContainsString(path_string, "/thumbstick")) {
        button_id = device::kAxisThumbstick;
      } else {
        NOTREACHED_IN_MIGRATION()
            << "Path is " << path_string
            << "But only Trackpad and thumbstick has 2d vector action";
      }
      uint64_t axis_mask = XrAxisOffsetFromId(button_id);
      v2f_action_states_[action].currentState.x = data.axis_data[axis_mask].x;
      // we have to negate y because webxr has different direction for y than
      // openxr
      v2f_action_states_[action].currentState.y = -data.axis_data[axis_mask].y;
      v2f_action_states_[action].isActive = data.is_valid;
      break;
    }
    case XR_ACTION_TYPE_POSE_INPUT: {
      pose_action_state_[action].isActive = data.is_valid;
      break;
    }
    default: {
      RETURN_IF_FALSE(false, XR_ERROR_VALIDATION_FAILURE,
                      "UpdateAction does not support this type of action");
      break;
    }
  }

  return XR_SUCCESS;
}

void OpenXrTestHelper::SetSessionState(XrSessionState state) {
  session_state_ = state;
  XrEventDataBuffer event_data;
  XrEventDataSessionStateChanged* event_data_ptr =
      reinterpret_cast<XrEventDataSessionStateChanged*>(&event_data);

  event_data_ptr->type = XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED;
  event_data_ptr->session = session_;
  event_data_ptr->state = session_state_;
  event_data_ptr->time = next_predicted_display_time_;

  event_queue_.push(event_data);
}

XrResult OpenXrTestHelper::PollEvent(XrEventDataBuffer* event_data) {
  RETURN_IF(event_data == nullptr, XR_ERROR_VALIDATION_FAILURE,
            "XrEventDataBuffer is nullptr");
  RETURN_IF_FALSE(event_data->type == XR_TYPE_EVENT_DATA_BUFFER,
                  XR_ERROR_VALIDATION_FAILURE,
                  "xrPollEvent event_data type invalid");
  UpdateEventQueue();
  if (!event_queue_.empty()) {
    *event_data = event_queue_.front();
    event_queue_.pop();
    return XR_SUCCESS;
  }

  return XR_EVENT_UNAVAILABLE;
}

const std::vector<Microsoft::WRL::ComPtr<ID3D11Texture2D>>&
OpenXrTestHelper::GetSwapchainTextures() const {
  return textures_arr_;
}

uint32_t OpenXrTestHelper::NextSwapchainImageIndex() {
  acquired_swapchain_texture_ =
      (acquired_swapchain_texture_ + 1) % textures_arr_.size();
  return acquired_swapchain_texture_;
}

XrTime OpenXrTestHelper::NextPredictedDisplayTime() {
  return ++next_predicted_display_time_;
}

void OpenXrTestHelper::UpdateEventQueue() {
  base::AutoLock auto_lock(lock_);
  if (test_hook_) {
    device_test::mojom::EventData data = {};
    do {
      data = test_hook_->WaitGetEventData();
      if (data.type == device_test::mojom::EventType::kSessionLost) {
        SetSessionState(XR_SESSION_STATE_STOPPING);
      } else if (data.type ==
                 device_test::mojom::EventType::kVisibilityVisibleBlurred) {
        // WebXR Visible-Blurred map to OpenXR Visible
        SetSessionState(XR_SESSION_STATE_VISIBLE);
      } else if (data.type == device_test::mojom::EventType::kInstanceLost) {
        XrEventDataBuffer event_data = {
            XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING};
        event_queue_.push(event_data);
      } else if (data.type ==
                 device_test::mojom::EventType::kInteractionProfileChanged) {
        UpdateInteractionProfile(data.interaction_profile);
        XrEventDataBuffer event_data;
        XrEventDataInteractionProfileChanged* interaction_profile_changed =
            reinterpret_cast<XrEventDataInteractionProfileChanged*>(
                &event_data);
        interaction_profile_changed->type =
            XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED;
        interaction_profile_changed->session = session_;
        event_queue_.push(event_data);
      } else if (data.type != device_test::mojom::EventType::kNoEvent) {
        NOTREACHED_IN_MIGRATION()
            << "Event changed event type not implemented for test";
      }
    } while (data.type != device_test::mojom::EventType::kNoEvent);
  }
}

std::optional<gfx::Transform> OpenXrTestHelper::GetPose() {
  base::AutoLock lock(lock_);
  if (test_hook_) {
    device::PoseFrameData pose_data = test_hook_->WaitGetPresentingPose();
    if (pose_data.is_valid) {
      return PoseFrameDataToTransform(pose_data);
    }
  }
  return std::nullopt;
}

std::optional<device::DeviceConfig> OpenXrTestHelper::GetDeviceConfig() {
  base::AutoLock lock(lock_);
  if (test_hook_) {
    return test_hook_->WaitGetDeviceConfig();
  }
  return std::nullopt;
}

bool OpenXrTestHelper::GetCanCreateSession() {
  base::AutoLock lock(lock_);
  if (test_hook_) {
    return test_hook_->WaitGetCanCreateSession();
  }

  // In the absence of a test hook telling us that we can't create a session;
  // assume that we can, as there's enough of a default implementation to do so.
  return true;
}

device::ControllerFrameData OpenXrTestHelper::GetControllerDataFromPath(
    std::string path_string) const {
  device::ControllerRole role;
  if (PathContainsString(path_string, "/user/hand/left/")) {
    role = device::kControllerRoleLeft;
  } else if (PathContainsString(path_string, "/user/hand/right/")) {
    role = device::kControllerRoleRight;
  } else {
    NOTREACHED_IN_MIGRATION()
        << "Currently Path should belong to either left or right";
  }
  device::ControllerFrameData data;
  for (uint32_t i = 0; i < data_arr_.size(); i++) {
    if (data_arr_[i].role == role) {
      data = data_arr_[i];
    }
  }
  return data;
}

bool OpenXrTestHelper::IsSessionRunning() const {
  return session_state_ == XR_SESSION_STATE_SYNCHRONIZED ||
         session_state_ == XR_SESSION_STATE_VISIBLE ||
         session_state_ == XR_SESSION_STATE_FOCUSED;
}

void OpenXrTestHelper::UpdateInteractionProfile(
    device::mojom::OpenXrInteractionProfileType type) {
  switch (type) {
    case device::mojom::OpenXrInteractionProfileType::kMicrosoftMotion:
      interaction_profile_ = device::kMicrosoftMotionInteractionProfilePath;
      break;
    case device::mojom::OpenXrInteractionProfileType::kKHRSimple:
      interaction_profile_ = device::kKHRSimpleInteractionProfilePath;
      break;
    case device::mojom::OpenXrInteractionProfileType::kOculusTouch:
      interaction_profile_ = device::kOculusTouchInteractionProfilePath;
      break;
    case device::mojom::OpenXrInteractionProfileType::kValveIndex:
      interaction_profile_ = device::kValveIndexInteractionProfilePath;
      break;
    case device::mojom::OpenXrInteractionProfileType::kHTCVive:
      interaction_profile_ = device::kHTCViveInteractionProfilePath;
      break;
    case device::mojom::OpenXrInteractionProfileType::kSamsungOdyssey:
      interaction_profile_ = device::kSamsungOdysseyInteractionProfilePath;
      break;
    case device::mojom::OpenXrInteractionProfileType::kHPReverbG2:
      interaction_profile_ = device::kHPReverbG2InteractionProfilePath;
      break;
    case device::mojom::OpenXrInteractionProfileType::kHandSelectGrasp:
      interaction_profile_ = device::kHandSelectGraspInteractionProfilePath;
      break;
    case device::mojom::OpenXrInteractionProfileType::kViveCosmos:
      interaction_profile_ = device::kHTCViveCosmosInteractionProfilePath;
      break;
    case device::mojom::OpenXrInteractionProfileType::kExtHand:
      interaction_profile_ = device::kExtHandInteractionProfilePath;
      break;
    case device::mojom::OpenXrInteractionProfileType::kInvalid:
    case device::mojom::OpenXrInteractionProfileType::kMetaHandAim:
      NOTREACHED_IN_MIGRATION() << "Invalid EventData interaction_profile type";
      break;
  }
}

void OpenXrTestHelper::LocateSpace(XrSpace space, XrPosef* pose) {
  DCHECK_NE(pose, nullptr);
  *pose = device::PoseIdentity();
  std::optional<gfx::Transform> transform = std::nullopt;

  if (reference_spaces_.count(space) == 1) {
    if (reference_spaces_.at(space).compare(kStageReferenceSpacePath) == 0) {
      // This locate space call wants the transform from local to stage which we
      // only need to give it identity matrix.
      transform = gfx::Transform();
    } else if (reference_spaces_.at(space).compare(kViewReferenceSpacePath) ==
               0) {
      // This locate space call wants the transform of the head pose.
      transform = GetPose();
    } else {
      NOTREACHED_IN_MIGRATION()
          << "Only locate reference space for local and view are implemented";
    }
  } else if (action_spaces_.count(space) == 1) {
    XrAction cur_action = action_spaces_.at(space);
    ActionProperties cur_action_properties = actions_[cur_action];
    std::string path_string =
        PathToString(cur_action_properties
                         .profile_binding_map[GetPath(interaction_profile_)]);
    device::ControllerFrameData data =
        GetControllerDataFromPath(std::move(path_string));
    if (data.pose_data.is_valid) {
      transform = PoseFrameDataToTransform(data.pose_data);
    }
  } else {
    NOTREACHED_IN_MIGRATION()
        << "Locate Space only supports reference space or action "
           "space for controller";
  }

  if (transform) {
    std::optional<gfx::DecomposedTransform> decomposed_transform =
        transform->Decompose();
    DCHECK(decomposed_transform);

    pose->orientation.x = decomposed_transform->quaternion.x();
    pose->orientation.y = decomposed_transform->quaternion.y();
    pose->orientation.z = decomposed_transform->quaternion.z();
    pose->orientation.w = decomposed_transform->quaternion.w();

    pose->position.x = decomposed_transform->translate[0];
    pose->position.y = decomposed_transform->translate[1];
    pose->position.z = decomposed_transform->translate[2];
  }
}

std::string OpenXrTestHelper::PathToString(XrPath path) const {
  return paths_[path - 1];
}

bool OpenXrTestHelper::UpdateData() {
  base::AutoLock auto_lock(lock_);
  if (test_hook_) {
    for (uint32_t i = 0; i < device::kMaxTrackedDevices; i++) {
      data_arr_[i] = test_hook_->WaitGetControllerData(i);
    }
    return true;
  }
  return false;
}

bool OpenXrTestHelper::UpdateViews(XrViewConfigurationType view_config_type,
                                   XrView views[],
                                   uint32_t size) {
  device::OpenXrViewConfiguration& view_config =
      GetViewConfigInfo(view_config_type);
  RETURN_IF(size != view_config.Views().size(), XR_ERROR_VALIDATION_FAILURE,
            "UpdateViews mismatched number of views");
  RETURN_IF(size != 1 && size != 2, XR_ERROR_VALIDATION_FAILURE,
            "UpdateViews only supports view configurations with 1 or 2 views");

  std::optional<gfx::Transform> pose = GetPose();
  std::optional<device::DeviceConfig> config = GetDeviceConfig();

  if (!pose.has_value() && !config.has_value()) {
    return true;
  }

  for (uint32_t i = 0; i < size; i++) {
    if (pose.has_value()) {
      views[i].pose = device::GfxTransformToXrPose(*pose);
    }
    if (config.has_value()) {
      // For view configurations with 2 views, assume they are the left and
      // right eye and set the X offset from zero to be half the IPD.
      // View configurations with 1 view are not necessarily always at zero, so
      // just also use half the IPD as an arbitrary offset to avoid adding
      // additional logic to set to zero.
      views[i].pose.position.x =
          config->interpupillary_distance / 2 * GetOffsetMultiplierForIndex(i);
    }
  }

  return true;
}

XrResult OpenXrTestHelper::ValidateAction(XrAction action) const {
  RETURN_IF(actions_.count(action) != 1, XR_ERROR_HANDLE_INVALID,
            "ValidateAction: Invalid Action");
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateActionCreateInfo(
    const XrActionCreateInfo& create_info) const {
  RETURN_IF(create_info.type != XR_TYPE_ACTION_CREATE_INFO,
            XR_ERROR_VALIDATION_FAILURE,
            "ValidateActionCreateInfo type invalid");
  RETURN_IF(create_info.next != nullptr, XR_ERROR_VALIDATION_FAILURE,
            "ValidateActionCreateInfo next is not nullptr");
  RETURN_IF(create_info.actionName[0] == '\0', XR_ERROR_NAME_INVALID,
            "ValidateActionCreateInfo actionName invalid");
  RETURN_IF(create_info.actionType == XR_ACTION_TYPE_MAX_ENUM,
            XR_ERROR_VALIDATION_FAILURE,
            "ValidateActionCreateInfo action type invalid");
  RETURN_IF(create_info.localizedActionName[0] == '\0',
            XR_ERROR_LOCALIZED_NAME_INVALID,
            "ValidateActionCreateInfo localizedActionName invalid");
  RETURN_IF(action_names_.count(create_info.actionName) != 0,
            XR_ERROR_NAME_DUPLICATED,
            "ValidateActionCreateInfo actionName duplicate");
  RETURN_IF(action_localized_names_.count(create_info.localizedActionName) != 0,
            XR_ERROR_LOCALIZED_NAME_DUPLICATED,
            "ValidateActionCreateInfo localizedActionName duplicate");
  RETURN_IF_FALSE(create_info.countSubactionPaths == 0 &&
                      create_info.subactionPaths == nullptr,
                  XR_ERROR_VALIDATION_FAILURE,
                  "ValidateActionCreateInfo has subactionPaths which is not "
                  "supported by current version of test.");
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateActionSet(XrActionSet action_set) const {
  RETURN_IF_FALSE(action_sets_.count(action_set), XR_ERROR_HANDLE_INVALID,
                  "ValidateActionSet: Invalid action_set");
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateActionSetCreateInfo(
    const XrActionSetCreateInfo& create_info) const {
  RETURN_IF(create_info.type != XR_TYPE_ACTION_SET_CREATE_INFO,
            XR_ERROR_VALIDATION_FAILURE,
            "ValidateActionSetCreateInfo type invalid");
  RETURN_IF(create_info.actionSetName[0] == '\0', XR_ERROR_NAME_INVALID,
            "ValidateActionSetCreateInfo actionSetName invalid");
  RETURN_IF(create_info.localizedActionSetName[0] == '\0',
            XR_ERROR_LOCALIZED_NAME_INVALID,
            "ValidateActionSetCreateInfo localizedActionSetName invalid");
  RETURN_IF(action_set_names_.count(create_info.actionSetName) != 0,
            XR_ERROR_NAME_DUPLICATED,
            "ValidateActionSetCreateInfo actionSetName duplicate");
  RETURN_IF(action_set_localized_names_.count(
                create_info.localizedActionSetName) != 0,
            XR_ERROR_LOCALIZED_NAME_DUPLICATED,
            "ValidateActionSetCreateInfo localizedActionSetName duplicate");
  RETURN_IF(create_info.priority != 0, XR_ERROR_VALIDATION_FAILURE,
            "ValidateActionSetCreateInfo has priority which is not supported "
            "by current version of test.");
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateActionSetNotAttached(
    XrActionSet action_set) const {
  if (attached_action_sets_.count(action_set) == 1)
    return XR_ERROR_ACTIONSETS_ALREADY_ATTACHED;
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateActionSpaceCreateInfo(
    const XrActionSpaceCreateInfo& create_info) const {
  RETURN_IF(create_info.type != XR_TYPE_ACTION_SPACE_CREATE_INFO,
            XR_ERROR_VALIDATION_FAILURE,
            "ValidateActionSpaceCreateInfo type invalid");
  RETURN_IF(create_info.next != nullptr, XR_ERROR_VALIDATION_FAILURE,
            "ValidateActionSpaceCreateInfo next is not nullptr");
  RETURN_IF_XR_FAILED(ValidateAction(create_info.action));
  ActionProperties cur_action_properties = actions_.at(create_info.action);
  if (cur_action_properties.type != XR_ACTION_TYPE_POSE_INPUT) {
    return XR_ERROR_ACTION_TYPE_MISMATCH;
  }
  RETURN_IF(create_info.subactionPath != XR_NULL_PATH,
            XR_ERROR_VALIDATION_FAILURE,
            "ValidateActionSpaceCreateInfo subactionPath != XR_NULL_PATH");
  RETURN_IF_XR_FAILED(ValidateXrPosefIsIdentity(create_info.poseInActionSpace));
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateHandTracker(
    XrHandTrackerEXT hand_tracker) const {
  RETURN_IF(left_hand_ == XR_NULL_HANDLE && right_hand_ == XR_NULL_HANDLE,
            XR_ERROR_HANDLE_INVALID, "No Hand Tracker has been created");
  RETURN_IF(left_hand_ != hand_tracker && right_hand_ != hand_tracker,
            XR_ERROR_HANDLE_INVALID, "Hand Tracker invalid");
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateInstance(XrInstance instance) const {
  // The Fake OpenXr Runtime returns this global OpenXrTestHelper object as the
  // instance value on xrCreateInstance.

  RETURN_IF(reinterpret_cast<OpenXrTestHelper*>(instance) != this,
            XR_ERROR_HANDLE_INVALID, "XrInstance invalid");

  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateSystemId(XrSystemId system_id) const {
  RETURN_IF(system_id_ == 0, XR_ERROR_SYSTEM_INVALID,
            "XrSystemId has not been queried");
  RETURN_IF(system_id != system_id_, XR_ERROR_SYSTEM_INVALID,
            "XrSystemId invalid");

  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateSession(XrSession session) const {
  RETURN_IF(session_ == XR_NULL_HANDLE, XR_ERROR_HANDLE_INVALID,
            "XrSession has not been queried");
  RETURN_IF(session != session_, XR_ERROR_HANDLE_INVALID, "XrSession invalid");

  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateSwapchain(XrSwapchain swapchain) const {
  RETURN_IF(swapchain_ == XR_NULL_HANDLE, XR_ERROR_HANDLE_INVALID,
            "XrSwapchain has not been queried");
  RETURN_IF(swapchain != swapchain_, XR_ERROR_HANDLE_INVALID,
            "XrSwapchain invalid");

  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateSpace(XrSpace space) const {
  RETURN_IF(space == XR_NULL_HANDLE, XR_ERROR_HANDLE_INVALID,
            "XrSpace has not been queried");
  RETURN_IF(
      reference_spaces_.count(space) != 1 && action_spaces_.count(space) != 1,
      XR_ERROR_HANDLE_INVALID, space);

  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidatePath(XrPath path) const {
  RETURN_IF(path > paths_.size(), XR_ERROR_PATH_INVALID, "XrPath invalid");
  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidatePredictedDisplayTime(XrTime time) const {
  RETURN_IF(time == 0, XR_ERROR_VALIDATION_FAILURE,
            "XrTime has not been queried");
  RETURN_IF(time > next_predicted_display_time_, XR_ERROR_VALIDATION_FAILURE,
            "XrTime predicted display time invalid");

  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateXrCompositionLayerProjection(
    XrViewConfigurationType view_config,
    const XrCompositionLayerProjection& projection_layer) {
  // The caller should have validated the view configuration.
  RETURN_IF(projection_layer.type != XR_TYPE_COMPOSITION_LAYER_PROJECTION,
            XR_ERROR_LAYER_INVALID,
            "XrCompositionLayerProjection type invalid");
  RETURN_IF(projection_layer.next != nullptr, XR_ERROR_VALIDATION_FAILURE,
            "XrCompositionLayerProjection next is not nullptr");
  RETURN_IF(projection_layer.layerFlags != 0, XR_ERROR_VALIDATION_FAILURE,
            "XrCompositionLayerProjection layerflag is not 0");
  RETURN_IF(reference_spaces_.count(projection_layer.space) != 1,
            XR_ERROR_VALIDATION_FAILURE,
            "XrCompositionLayerProjection space is not reference space");
  std::string space_path = reference_spaces_.at(projection_layer.space);
  RETURN_IF(space_path.compare(kLocalReferenceSpacePath) != 0,
            XR_ERROR_VALIDATION_FAILURE,
            "XrCompositionLayerProjection space is not local space");
  RETURN_IF(projection_layer.views == nullptr, XR_ERROR_VALIDATION_FAILURE,
            "XrCompositionLayerProjection view is nullptr");

  const device::OpenXrViewConfiguration& config =
      GetViewConfigInfo(view_config);

  RETURN_IF(projection_layer.viewCount != config.Views().size(),
            XR_ERROR_VALIDATION_FAILURE,
            "XrCompositionLayerProjection viewCount invalid");

  for (uint32_t j = 0; j < projection_layer.viewCount; j++) {
    const XrCompositionLayerProjectionView& projection_view =
        projection_layer.views[j];
    RETURN_IF_XR_FAILED(ValidateXrCompositionLayerProjectionView(
        projection_view, projection_layer.viewCount, j));
  }

  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateXrCompositionLayerProjectionView(
    const XrCompositionLayerProjectionView& view,
    uint32_t view_count,
    uint32_t index) {
  DCHECK_LE(view_count, 2u);
  DCHECK_LE(index, 2u);
  RETURN_IF(view.type != XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW,
            XR_ERROR_VALIDATION_FAILURE,
            "XrCompositionLayerProjectionView type invalid");
  RETURN_IF(view.next != nullptr, XR_ERROR_VALIDATION_FAILURE,
            "XrCompositionLayerProjectionView next is not nullptr");

  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateXrPosefIsIdentity(
    const XrPosef& pose) const {
  XrPosef identity = device::PoseIdentity();
  bool is_identity = true;
  is_identity &= pose.orientation.x == identity.orientation.x;
  is_identity &= pose.orientation.y == identity.orientation.y;
  is_identity &= pose.orientation.z == identity.orientation.z;
  is_identity &= pose.orientation.w == identity.orientation.w;
  is_identity &= pose.position.x == identity.position.x;
  is_identity &= pose.position.y == identity.position.y;
  is_identity &= pose.position.z == identity.position.z;
  RETURN_IF_FALSE(is_identity, XR_ERROR_VALIDATION_FAILURE,
                  "XrPosef is not an identity");

  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateViews(uint32_t view_capacity_input,
                                         XrView* views) const {
  RETURN_IF(views == nullptr, XR_ERROR_VALIDATION_FAILURE, "XrView is nullptr");
  for (uint32_t i = 0; i < view_capacity_input; i++) {
    XrView view = views[i];
    RETURN_IF_FALSE(view.type == XR_TYPE_VIEW, XR_ERROR_VALIDATION_FAILURE,
                    "XrView type invalid");
    RETURN_IF(view.next != nullptr, XR_ERROR_VALIDATION_FAILURE,
              "XrView next is not nullptr");
  }

  return XR_SUCCESS;
}

XrResult OpenXrTestHelper::ValidateViewConfigType(
    XrViewConfigurationType view_config) const {
  RETURN_IF(!base::Contains(primary_configs_supported_, view_config) &&
                !base::Contains(secondary_configs_supported_, view_config),
            XR_ERROR_VIEW_CONFIGURATION_TYPE_UNSUPPORTED,
            "XrViewConfigurationType unsupported");

  return XR_SUCCESS;
}