Files

1454 lines
52 KiB
C++

// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/VR/OpenXRUtilitySession.h"
#ifdef ENABLE_VR
// VulkanLoader.h must come first -- it defines the platform Vulkan types and VK_NO_PROTOTYPES.
#include "VideoBackends/Vulkan/VulkanLoader.h"
#define XR_USE_GRAPHICS_API_VULKAN
#if defined(ANDROID)
#include <jni.h>
#define XR_USE_PLATFORM_ANDROID
#endif
#include <algorithm>
#include <array>
#include <cfloat>
#include <chrono>
#include <cmath>
#include <cstring>
#include <iterator>
#include <optional>
#include <ranges>
#include <sstream>
#include <thread>
#include <utility>
#include <fmt/format.h>
#include <imgui.h>
#include <imgui_impl_vulkan.h>
#include <openxr/openxr_platform.h>
// On Linux, VulkanLoader.h defines VK_USE_PLATFORM_XLIB_KHR, so <vulkan/vulkan.h> pulls in
// <X11/Xlib.h>, which #defines None/Bool/Status/etc. as macros. Undo them before including the
// Dolphin headers below that use those names as identifiers (e.g. WiimoteSource::None,
// SettingType::Bool), matching the #undef pattern already used in BPMemory.h/XFMemory.h.
#ifdef None
#undef None
#endif
#ifdef Bool
#undef Bool
#endif
#ifdef Status
#undef Status
#endif
#ifdef Success
#undef Success
#endif
#ifdef Always
#undef Always
#endif
#include "Common/CommonPaths.h"
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Common/MsgHandler.h"
#include "Common/VR/OpenXRInputState.h"
#include "Core/Core.h"
#include "Core/HotkeyManager.h"
#include "Core/HW/GCPad.h"
#include "Core/HW/Wiimote.h"
#include "Core/System.h"
#include "InputCommon/ControlReference/ControlReference.h"
#include "InputCommon/ControllerEmu/ControlGroup/Attachments.h"
#include "InputCommon/ControllerEmu/ControllerEmu.h"
#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "InputCommon/ControllerInterface/MappingCommon.h"
#include "InputCommon/InputConfig.h"
#include "VideoBackends/Vulkan/VulkanContext.h"
#include "VideoCommon/VR/OpenXRBindingWizardModel.h"
#include "VideoCommon/VR/OpenXRManager.h"
#include "VideoCommon/VideoConfig.h"
namespace VR
{
namespace
{
using namespace std::chrono_literals;
constexpr char OPENXR_DEVICE[] = "OpenXR/0/OpenXR Controller";
constexpr uint32_t UI_WIDTH = 1600;
constexpr uint32_t UI_HEIGHT = 1100;
constexpr float UI_HEIGHT_METERS = 1.2f;
constexpr float UI_DISTANCE_METERS = 1.35f;
constexpr float UI_VERTICAL_OFFSET_METERS = 0.12f;
constexpr float INPUT_NEUTRAL_THRESHOLD = 0.20f;
std::atomic<bool> s_utility_session_active{false};
thread_local VkResult s_imgui_vulkan_error = VK_SUCCESS;
void RecordImGuiVulkanResult(VkResult result)
{
if (result != VK_SUCCESS)
{
s_imgui_vulkan_error = result;
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: ImGui Vulkan operation failed ({})",
static_cast<int>(result));
}
}
struct VulkanRequirements
{
std::vector<std::string> instance_extensions;
std::vector<std::string> device_extensions;
uint32_t min_api_version = 0;
uint32_t max_api_version = 0;
};
std::vector<std::string> ParseExtensionList(const std::string& list)
{
std::vector<std::string> extensions;
std::istringstream stream(list);
std::string extension;
while (stream >> extension)
extensions.emplace_back(std::move(extension));
return extensions;
}
template <typename T>
T GetXRFunction(XrInstance instance, const char* name)
{
PFN_xrVoidFunction function = nullptr;
if (XR_FAILED(xrGetInstanceProcAddr(instance, name, &function)))
return nullptr;
return reinterpret_cast<T>(function);
}
bool QueryVulkanRequirements(OpenXRManager& manager, VulkanRequirements* requirements)
{
const XrInstance instance = manager.GetInstance();
const XrSystemId system = manager.GetSystemId();
const auto get_requirements = GetXRFunction<PFN_xrGetVulkanGraphicsRequirementsKHR>(
instance, "xrGetVulkanGraphicsRequirementsKHR");
const auto get_instance_extensions = GetXRFunction<PFN_xrGetVulkanInstanceExtensionsKHR>(
instance, "xrGetVulkanInstanceExtensionsKHR");
const auto get_device_extensions =
GetXRFunction<PFN_xrGetVulkanDeviceExtensionsKHR>(instance, "xrGetVulkanDeviceExtensionsKHR");
if (!get_requirements || !get_instance_extensions || !get_device_extensions)
return false;
XrGraphicsRequirementsVulkanKHR graphics_requirements{XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR};
if (XR_FAILED(get_requirements(instance, system, &graphics_requirements)))
return false;
requirements->min_api_version =
VK_MAKE_API_VERSION(0, XR_VERSION_MAJOR(graphics_requirements.minApiVersionSupported),
XR_VERSION_MINOR(graphics_requirements.minApiVersionSupported),
XR_VERSION_PATCH(graphics_requirements.minApiVersionSupported));
const uint32_t reported_max =
VK_MAKE_API_VERSION(0, XR_VERSION_MAJOR(graphics_requirements.maxApiVersionSupported),
XR_VERSION_MINOR(graphics_requirements.maxApiVersionSupported),
XR_VERSION_PATCH(graphics_requirements.maxApiVersionSupported));
// XR_KHR_vulkan_enable revision 1 runtimes commonly report 1.0.0 as an unspecified ceiling.
requirements->max_api_version = reported_max > VK_API_VERSION_1_0 ? reported_max : 0;
const auto query_list = [instance, system](auto function, std::vector<std::string>* output) {
uint32_t length = 0;
if (XR_FAILED(function(instance, system, 0, &length, nullptr)))
return false;
if (length == 0)
return true;
std::string text(length, '\0');
if (XR_FAILED(function(instance, system, length, &length, text.data())))
return false;
*output = ParseExtensionList(text);
return true;
};
if (!query_list(get_instance_extensions, &requirements->instance_extensions) ||
!query_list(get_device_extensions, &requirements->device_extensions))
{
return false;
}
// The bundled ImGui Vulkan renderer loads its optional ImGui_ImplVulkanH desktop swapchain
// helpers together with the renderer entry points. The mapper does not call those helpers, but
// some Vulkan loaders return null for them unless these generic extensions are enabled.
// Neither extension creates a window surface, so the utility remains headless.
if (std::ranges::find(requirements->instance_extensions, VK_KHR_SURFACE_EXTENSION_NAME) ==
requirements->instance_extensions.end())
{
requirements->instance_extensions.emplace_back(VK_KHR_SURFACE_EXTENSION_NAME);
}
if (std::ranges::find(requirements->device_extensions, VK_KHR_SWAPCHAIN_EXTENSION_NAME) ==
requirements->device_extensions.end())
{
requirements->device_extensions.emplace_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
}
return true;
}
bool InputsAreNeutral(const Common::VR::OpenXRInputSnapshot& snapshot)
{
for (const auto& controller : snapshot.controllers)
{
if (controller.primary_button || controller.secondary_button || controller.menu_button ||
controller.trigger_button || controller.squeeze_button || controller.thumbstick_button ||
controller.trigger_value > INPUT_NEUTRAL_THRESHOLD ||
controller.squeeze_value > INPUT_NEUTRAL_THRESHOLD ||
std::abs(controller.thumbstick_x) > INPUT_NEUTRAL_THRESHOLD ||
std::abs(controller.thumbstick_y) > INPUT_NEUTRAL_THRESHOLD)
{
return false;
}
}
return true;
}
struct MappableControl
{
std::string group_name;
std::string control_name;
ControlReference* reference = nullptr;
};
struct MappableGroup
{
std::string name;
size_t first_control = 0;
size_t control_count = 0;
};
void AddMappableControls(ControllerEmu::ControlGroupContainer& container,
std::vector<MappableControl>* controls, std::string_view prefix = {},
const ControllerEmu::ControlGroup* excluded_group = nullptr,
const ControllerEmu::ControlGroup* renamed_group = nullptr,
const char* renamed_group_name = nullptr)
{
for (const auto& group : container.groups)
{
if (group.get() == excluded_group)
continue;
if (!IsOpenXRMapperMappableGroup(group->type))
continue;
std::string group_name = Common::GetStringT(group->ui_name.c_str());
if (group.get() == renamed_group && renamed_group_name)
group_name = renamed_group_name;
if (!prefix.empty())
group_name = fmt::format("{} - {}", prefix, group_name);
for (const auto& control : group->controls)
{
if (!control->control_ref->IsInput())
continue;
controls->push_back(
{group_name, Common::GetStringT(control->ui_name.c_str()), control->control_ref.get()});
}
}
}
class BindingWizard
{
public:
using Result = OpenXRBindingWizardModel::Result;
explicit BindingWizard(OpenXRUtilitySessionTarget target) : m_target(target)
{
InputConfig* config = nullptr;
switch (target.type)
{
case OpenXRUtilitySessionTargetType::WiiRemote:
config = Wiimote::GetConfig();
break;
case OpenXRUtilitySessionTargetType::GameCubeController:
config = Pad::GetConfig();
break;
case OpenXRUtilitySessionTargetType::Hotkeys:
config = HotkeyManagerEmu::GetConfig();
break;
}
m_controller = config && target.port >= 0 && target.port < config->GetControllerCount() ?
config->GetController(target.port) :
nullptr;
if (!m_controller)
return;
const ControllerEmu::ControlGroup* android_hotkey_group =
target.type == OpenXRUtilitySessionTargetType::Hotkeys ?
HotkeyManagerEmu::GetHotkeyGroup(HKGP_ANDROID) :
nullptr;
#if defined(ANDROID)
AddMappableControls(*m_controller, &m_controls, {}, nullptr, android_hotkey_group,
"Android / Quest");
#else
AddMappableControls(*m_controller, &m_controls, {}, android_hotkey_group);
#endif
if (target.type == OpenXRUtilitySessionTargetType::WiiRemote)
{
for (const auto& group : m_controller->groups)
{
if (group->type != ControllerEmu::GroupType::Attachments)
continue;
auto* attachments = static_cast<ControllerEmu::Attachments*>(group.get());
const auto selected = attachments->GetSelectedAttachment();
const auto& list = attachments->GetAttachmentList();
if (selected < list.size())
{
AddMappableControls(*list[selected], &m_controls,
Common::GetStringT(list[selected]->GetDisplayName().c_str()));
}
break;
}
}
BuildGroupColumns();
std::vector<std::string> expressions;
expressions.reserve(m_controls.size());
for (const auto& control : m_controls)
expressions.emplace_back(control.reference->GetExpression());
m_model = std::make_unique<OpenXRBindingWizardModel>(std::move(expressions));
}
bool IsValid() const { return m_controller != nullptr && m_model && m_model->IsValid(); }
Result GetResult() const { return m_model->GetResult(); }
Result DrawAndUpdate()
{
g_controller_interface.UpdateInput();
UpdateDetection();
ImGui::SetNextWindowPos({0.0f, 0.0f});
ImGui::SetNextWindowSize({static_cast<float>(UI_WIDTH), static_cast<float>(UI_HEIGHT)});
constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings;
ImGui::Begin("OpenXR Controller Mapper", nullptr, flags);
ImGui::SetCursorPos({45.0f, 28.0f});
if (m_target.type == OpenXRUtilitySessionTargetType::WiiRemote)
ImGui::Text("OpenXR Wii Remote %d", m_target.port + 1);
else if (m_target.type == OpenXRUtilitySessionTargetType::GameCubeController)
ImGui::Text("OpenXR GameCube Standard Controller %d", m_target.port + 1);
else
ImGui::Text("OpenXR Hotkey Settings");
ImGui::SetCursorPos({45.0f, 68.0f});
ImGui::TextDisabled("Select any control to listen for a new OpenXR input");
if (m_controls.empty())
{
ImGui::SetCursorPos({55.0f, 170.0f});
ImGui::Text("No bindable controls were found.");
}
else
{
const size_t index = m_model->GetIndex();
const auto& control = m_controls[index];
ImGui::SetCursorPos({45.0f, 108.0f});
if (m_detection_phase == DetectionPhase::WaitingForNeutral)
{
ImGui::TextColored({1.0f, 0.78f, 0.2f, 1.0f}, "(%s): release all inputs...",
control.control_name.c_str());
}
else if (m_detection_phase == DetectionPhase::Detecting)
{
ImGui::TextColored({0.35f, 0.85f, 1.0f, 1.0f}, "(%s): press the input to bind now...",
control.control_name.c_str());
}
else if (!m_status.empty())
ImGui::Text("%s", m_status.c_str());
else
ImGui::TextDisabled("Current bindings are staged until Apply and Finish");
const auto snapshot = Common::VR::OpenXRInputState::GetSnapshot();
ImGui::SetCursorPos({45.0f, 145.0f});
ImGui::SetWindowFontScale(0.75f);
for (size_t hand = 0; hand < snapshot.controllers.size(); ++hand)
{
const auto& state = snapshot.controllers[hand];
ImGui::SetCursorPosX(45.0f);
ImGui::TextDisabled("%s: trigger %.2f grip %.2f stick %.2f, %.2f %s",
hand == 0 ? "Left" : "Right", state.trigger_value, state.squeeze_value,
state.thumbstick_x, state.thumbstick_y,
state.connected ? "active" : "waiting");
}
ImGui::SetWindowFontScale(1.0f);
ImGui::SetCursorPos({35.0f, 220.0f});
ImGui::BeginChild("BindingDashboard", {1530.0f, 725.0f}, true);
constexpr ImGuiTableFlags table_flags =
ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_BordersInnerV;
if (ImGui::BeginTable("BindingColumns", static_cast<int>(m_group_columns.size()),
table_flags))
{
for (size_t column = 0; column < m_group_columns.size(); ++column)
{
ImGui::TableNextColumn();
DrawBindingColumn(column);
}
ImGui::EndTable();
}
ImGui::EndChild();
}
const bool detecting = m_detection_phase != DetectionPhase::Idle || m_suppress_ui_clicks;
ImGui::SetCursorPos({45.0f, 985.0f});
ImGui::BeginDisabled(detecting);
if (ImGui::Button("Cancel", {220.0f, 70.0f}))
m_model->Cancel();
ImGui::SameLine(0.0f, 25.0f);
if (ImGui::Button("Clear Selected", {260.0f, 70.0f}))
{
const std::string name = m_controls[m_model->GetIndex()].control_name;
m_model->Clear();
m_status = fmt::format("({}) cleared (pending Apply)", name);
}
ImGui::SameLine(0.0f, 765.0f);
if (ImGui::Button("Apply", {250.0f, 70.0f}))
m_model->Finish();
ImGui::EndDisabled();
ImGui::End();
m_suppress_ui_clicks = false;
return m_model->GetResult();
}
OpenXRPendingBindings BuildPendingBindings() const
{
OpenXRPendingBindings pending;
pending.target = m_target;
pending.default_device = OPENXR_DEVICE;
pending.bindings.reserve(m_controls.size());
const auto& expressions = m_model->GetExpressions();
for (size_t i = 0; i < m_controls.size(); ++i)
pending.bindings.push_back({m_controls[i].reference, expressions[i]});
return pending;
}
private:
enum class DetectionPhase
{
Idle,
WaitingForNeutral,
Detecting,
};
void BuildGroupColumns()
{
std::vector<MappableGroup> groups;
for (size_t i = 0; i < m_controls.size();)
{
const size_t first = i;
while (i < m_controls.size() && m_controls[i].group_name == m_controls[first].group_name)
++i;
groups.push_back({m_controls[first].group_name, first, i - first});
}
std::array<size_t, 3> column_rows{};
for (const auto& group : groups)
{
const size_t column = static_cast<size_t>(
std::distance(column_rows.begin(), std::ranges::min_element(column_rows)));
m_group_columns[column].push_back(group);
column_rows[column] += group.control_count + 1;
}
}
void BeginDetection(size_t index)
{
if (!m_model->Select(index))
return;
m_detector.reset();
m_detection_phase = DetectionPhase::WaitingForNeutral;
m_model->BeginBinding();
m_status.clear();
}
void DrawBindingColumn(size_t column)
{
ImGui::SetWindowFontScale(0.68f);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {8.0f, 4.0f});
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, {0.5f, 0.5f});
const bool detecting = m_detection_phase != DetectionPhase::Idle || m_suppress_ui_clicks;
ImGui::BeginDisabled(detecting);
for (const auto& group : m_group_columns[column])
{
ImGui::TextColored({0.45f, 0.82f, 1.0f, 1.0f}, "%s", group.name.c_str());
ImGui::Separator();
for (size_t offset = 0; offset < group.control_count; ++offset)
{
const size_t index = group.first_control + offset;
const auto& control = m_controls[index];
const float row_y = ImGui::GetCursorPosY();
ImGui::PushID(static_cast<int>(index));
const bool selected = index == m_model->GetIndex();
if (selected)
{
ImGui::PushStyleColor(ImGuiCol_Button, {0.18f, 0.48f, 0.68f, 1.0f});
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, {0.25f, 0.62f, 0.82f, 1.0f});
}
if (ImGui::Button(control.control_name.c_str(), {220.0f, 34.0f}))
BeginDetection(index);
if (selected)
ImGui::PopStyleColor(2);
ImGui::SameLine(0.0f, 14.0f);
const std::string& expression = m_model->GetExpressions()[index];
if (expression.empty())
ImGui::TextDisabled("Not bound");
else
ImGui::TextUnformatted(expression.c_str());
ImGui::SetCursorPosY(std::max(ImGui::GetCursorPosY(), row_y + 38.0f));
ImGui::PopID();
}
ImGui::Spacing();
}
ImGui::EndDisabled();
ImGui::PopStyleVar(2);
ImGui::SetWindowFontScale(1.0f);
}
void UpdateDetection()
{
if (m_detection_phase == DetectionPhase::WaitingForNeutral)
{
const auto now = std::chrono::steady_clock::now();
if (!m_model->UpdateNeutralGate(InputsAreNeutral(Common::VR::OpenXRInputState::GetSnapshot()),
now))
return;
m_detector = std::make_unique<ciface::Core::InputDetector>();
const std::array<std::string, 1> devices{OPENXR_DEVICE};
m_detector->Start(g_controller_interface, devices);
if (m_detector->IsComplete())
{
m_detector.reset();
m_detection_phase = DetectionPhase::Idle;
m_status = "OpenXR controller device is unavailable";
}
else
{
m_detection_phase = DetectionPhase::Detecting;
}
return;
}
if (m_detection_phase != DetectionPhase::Detecting || !m_detector)
return;
m_detector->Update(5s, 350ms, 8s);
if (!m_detector->IsComplete())
return;
auto detections = m_detector->TakeResults();
ciface::MappingCommon::RemoveSpuriousTriggerCombinations(&detections);
if (!detections.empty())
{
ciface::Core::DeviceQualifier default_device;
default_device.FromString(OPENXR_DEVICE);
const std::string expression = ciface::MappingCommon::BuildExpression(
detections, default_device, ciface::MappingCommon::Quote::On);
m_model->AcceptBinding(expression);
m_status =
fmt::format("({}) bound to {}", m_controls[m_model->GetIndex()].control_name, expression);
}
else
{
m_status = "No input detected";
}
m_detector.reset();
m_detection_phase = DetectionPhase::Idle;
m_suppress_ui_clicks = true;
}
OpenXRUtilitySessionTarget m_target;
ControllerEmu::EmulatedController* m_controller = nullptr;
std::vector<MappableControl> m_controls;
std::array<std::vector<MappableGroup>, 3> m_group_columns;
std::unique_ptr<OpenXRBindingWizardModel> m_model;
DetectionPhase m_detection_phase = DetectionPhase::Idle;
std::unique_ptr<ciface::Core::InputDetector> m_detector;
bool m_suppress_ui_clicks = false;
std::string m_status;
};
struct RenderImage
{
VkImage image = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
VkCommandPool command_pool = VK_NULL_HANDLE;
VkCommandBuffer command_buffer = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
bool initialized = false;
};
VkFormat SelectSwapchainFormat(XrSession session)
{
uint32_t count = 0;
if (XR_FAILED(xrEnumerateSwapchainFormats(session, 0, &count, nullptr)) || count == 0)
return VK_FORMAT_UNDEFINED;
std::vector<int64_t> formats(count);
if (XR_FAILED(xrEnumerateSwapchainFormats(session, count, &count, formats.data())))
return VK_FORMAT_UNDEFINED;
constexpr std::array preferred = {VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_B8G8R8A8_SRGB,
VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM};
for (VkFormat wanted : preferred)
{
if (std::ranges::find(formats, static_cast<int64_t>(wanted)) != formats.end())
return wanted;
}
return VK_FORMAT_UNDEFINED;
}
XrVector3f RotateVector(const XrQuaternionf& q, const XrVector3f& v)
{
const XrVector3f qv{q.x, q.y, q.z};
const XrVector3f uv{qv.y * v.z - qv.z * v.y, qv.z * v.x - qv.x * v.z, qv.x * v.y - qv.y * v.x};
const XrVector3f uuv{qv.y * uv.z - qv.z * uv.y, qv.z * uv.x - qv.x * uv.z,
qv.x * uv.y - qv.y * uv.x};
return {v.x + 2.0f * (q.w * uv.x + uuv.x), v.y + 2.0f * (q.w * uv.y + uuv.y),
v.z + 2.0f * (q.w * uv.z + uuv.z)};
}
} // namespace
struct OpenXRUtilitySession::Impl
{
explicit Impl(OpenXRUtilitySession& owner_) : owner(owner_) {}
OpenXRUtilitySession& owner;
std::thread thread;
std::atomic<bool> stop_requested{false};
OpenXRUtilitySessionTarget target;
std::unique_ptr<OpenXRManager> manager;
std::unique_ptr<Vulkan::VulkanContext> vulkan;
bool vulkan_library_loaded = false;
BackendInfo previous_backend_info;
bool backend_info_saved = false;
XrSwapchain swapchain = XR_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkRenderPass render_pass = VK_NULL_HANDLE;
std::vector<XrSwapchainImageVulkanKHR> xr_images;
std::vector<RenderImage> render_images;
ImGuiContext* imgui_context = nullptr;
ImGuiContext* previous_imgui_context = nullptr;
bool imgui_backend_initialized = false;
XrCompositionLayerQuad quad{XR_TYPE_COMPOSITION_LAYER_QUAD};
bool quad_pose_initialized = false;
std::array<ImVec2, 2> pointer_positions{};
std::array<bool, 2> pointer_visible{};
std::array<bool, 2> pointer_pressed{};
size_t active_pointer_hand = 0;
bool previous_pointer_down = false;
void Fail(OpenXRUtilitySessionFailure reason, std::string_view message)
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: {}", message);
{
std::lock_guard lock(owner.m_result_mutex);
owner.m_failure_message = std::string(message);
}
owner.m_failure.store(reason, std::memory_order_release);
owner.m_state.store(OpenXRUtilitySessionState::Failed, std::memory_order_release);
}
bool InitializeOpenXR()
{
manager = std::make_unique<OpenXRManager>();
std::vector<const char*> extensions{XR_KHR_VULKAN_ENABLE_EXTENSION_NAME};
#if defined(ANDROID)
extensions.push_back(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME);
#endif
const auto controller_extensions = OpenXRManager::GetAvailableControllerExtensions();
extensions.insert(extensions.end(), controller_extensions.begin(), controller_extensions.end());
return manager->CreateInstance(extensions) && manager->InitializeSystem() &&
manager->EnumerateViewConfigurations();
}
bool InitializeVulkan()
{
VulkanRequirements requirements;
if (!QueryVulkanRequirements(*manager, &requirements))
return false;
if (!Vulkan::LoadVulkanLibrary())
return false;
vulkan_library_loaded = true;
uint32_t api_version = 0;
VkInstance instance = Vulkan::VulkanContext::CreateVulkanInstance(
WindowSystemType::Headless, false, false, &api_version, requirements.instance_extensions,
requirements.max_api_version);
if (instance == VK_NULL_HANDLE)
return false;
if (!Vulkan::LoadVulkanInstanceFunctions(instance))
{
vkDestroyInstance(instance, nullptr);
return false;
}
if (api_version < requirements.min_api_version)
{
vkDestroyInstance(instance, nullptr);
return false;
}
const auto get_graphics_device = GetXRFunction<PFN_xrGetVulkanGraphicsDeviceKHR>(
manager->GetInstance(), "xrGetVulkanGraphicsDeviceKHR");
VkPhysicalDevice physical_device = VK_NULL_HANDLE;
if (!get_graphics_device ||
XR_FAILED(get_graphics_device(manager->GetInstance(), manager->GetSystemId(), instance,
&physical_device)) ||
physical_device == VK_NULL_HANDLE)
{
vkDestroyInstance(instance, nullptr);
return false;
}
previous_backend_info = g_backend_info;
backend_info_saved = true;
Vulkan::VulkanContext::PopulateBackendInfo(&g_backend_info);
vulkan = Vulkan::VulkanContext::Create(instance, physical_device, VK_NULL_HANDLE, false, false,
api_version, requirements.device_extensions);
return vulkan != nullptr;
}
bool CreateXRSession()
{
XrGraphicsBindingVulkanKHR binding{XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR};
binding.instance = vulkan->GetVulkanInstance();
binding.physicalDevice = vulkan->GetPhysicalDevice();
binding.device = vulkan->GetDevice();
binding.queueFamilyIndex = vulkan->GetGraphicsQueueFamilyIndex();
binding.queueIndex = 0;
XrSessionCreateInfo info{XR_TYPE_SESSION_CREATE_INFO};
info.next = &binding;
info.systemId = manager->GetSystemId();
XrSession session = XR_NULL_HANDLE;
const XrResult create_result = xrCreateSession(manager->GetInstance(), &info, &session);
if (XR_FAILED(create_result))
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: xrCreateSession failed ({})",
static_cast<int>(create_result));
return false;
}
manager->SetSession(session);
if (!manager->CreateReferenceSpace())
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: reference-space creation failed");
return false;
}
return true;
}
bool CreateRenderPass()
{
VkAttachmentDescription attachment{};
attachment.format = format;
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachment.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
attachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &reference;
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
info.attachmentCount = 1;
info.pAttachments = &attachment;
info.subpassCount = 1;
info.pSubpasses = &subpass;
info.dependencyCount = 1;
info.pDependencies = &dependency;
const VkResult result = vkCreateRenderPass(vulkan->GetDevice(), &info, nullptr, &render_pass);
if (result != VK_SUCCESS)
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: vkCreateRenderPass failed ({})",
static_cast<int>(result));
return false;
}
return true;
}
bool CreateSwapchainAndImages()
{
format = SelectSwapchainFormat(manager->GetSession());
if (format == VK_FORMAT_UNDEFINED)
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: no supported RGBA swapchain format");
return false;
}
XrSwapchainCreateInfo info{XR_TYPE_SWAPCHAIN_CREATE_INFO};
info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
info.format = static_cast<int64_t>(format);
info.sampleCount = 1;
info.width = UI_WIDTH;
info.height = UI_HEIGHT;
info.faceCount = 1;
info.arraySize = 1;
info.mipCount = 1;
const XrResult create_result = xrCreateSwapchain(manager->GetSession(), &info, &swapchain);
if (XR_FAILED(create_result))
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: xrCreateSwapchain failed ({})",
static_cast<int>(create_result));
return false;
}
uint32_t count = 0;
const XrResult count_result = xrEnumerateSwapchainImages(swapchain, 0, &count, nullptr);
if (XR_FAILED(count_result) || count == 0)
{
ERROR_LOG_FMT(OPENXR,
"OpenXR controller mapper: swapchain image count query failed ({}, count {})",
static_cast<int>(count_result), count);
return false;
}
INFO_LOG_FMT(OPENXR, "OpenXR controller mapper: swapchain format {} with {} image(s)",
static_cast<int>(format), count);
xr_images.resize(count, {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR});
const XrResult images_result = xrEnumerateSwapchainImages(
swapchain, count, &count, reinterpret_cast<XrSwapchainImageBaseHeader*>(xr_images.data()));
if (XR_FAILED(images_result))
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: swapchain image enumeration failed ({})",
static_cast<int>(images_result));
return false;
}
if (!CreateRenderPass())
return false;
render_images.resize(count);
for (uint32_t i = 0; i < count; ++i)
{
auto& image = render_images[i];
image.image = xr_images[i].image;
VkImageViewCreateInfo view_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_info.image = image.image;
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.format = format;
view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view_info.subresourceRange.levelCount = 1;
view_info.subresourceRange.layerCount = 1;
const VkResult view_result =
vkCreateImageView(vulkan->GetDevice(), &view_info, nullptr, &image.view);
if (view_result != VK_SUCCESS)
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: vkCreateImageView failed ({})",
static_cast<int>(view_result));
return false;
}
VkFramebufferCreateInfo framebuffer_info{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
framebuffer_info.renderPass = render_pass;
framebuffer_info.attachmentCount = 1;
framebuffer_info.pAttachments = &image.view;
framebuffer_info.width = UI_WIDTH;
framebuffer_info.height = UI_HEIGHT;
framebuffer_info.layers = 1;
const VkResult framebuffer_result =
vkCreateFramebuffer(vulkan->GetDevice(), &framebuffer_info, nullptr, &image.framebuffer);
if (framebuffer_result != VK_SUCCESS)
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: vkCreateFramebuffer failed ({})",
static_cast<int>(framebuffer_result));
return false;
}
VkCommandPoolCreateInfo pool_info{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
pool_info.queueFamilyIndex = vulkan->GetGraphicsQueueFamilyIndex();
const VkResult pool_result =
vkCreateCommandPool(vulkan->GetDevice(), &pool_info, nullptr, &image.command_pool);
if (pool_result != VK_SUCCESS)
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: vkCreateCommandPool failed ({})",
static_cast<int>(pool_result));
return false;
}
VkCommandBufferAllocateInfo allocate_info{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
allocate_info.commandPool = image.command_pool;
allocate_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocate_info.commandBufferCount = 1;
const VkResult command_result =
vkAllocateCommandBuffers(vulkan->GetDevice(), &allocate_info, &image.command_buffer);
if (command_result != VK_SUCCESS)
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: command-buffer allocation failed ({})",
static_cast<int>(command_result));
return false;
}
VkFenceCreateInfo fence_info{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
const VkResult fence_result =
vkCreateFence(vulkan->GetDevice(), &fence_info, nullptr, &image.fence);
if (fence_result != VK_SUCCESS)
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: vkCreateFence failed ({})",
static_cast<int>(fence_result));
return false;
}
}
quad.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
quad.space = manager->GetReferenceSpace();
quad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
quad.pose.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
quad.pose.position = {0.0f, UI_VERTICAL_OFFSET_METERS, -UI_DISTANCE_METERS};
quad.size.height = UI_HEIGHT_METERS;
quad.size.width =
UI_HEIGHT_METERS * static_cast<float>(UI_WIDTH) / static_cast<float>(UI_HEIGHT);
quad.subImage.swapchain = swapchain;
quad.subImage.imageRect.extent = {static_cast<int32_t>(UI_WIDTH),
static_cast<int32_t>(UI_HEIGHT)};
return true;
}
bool InitializeImGui()
{
previous_imgui_context = ImGui::GetCurrentContext();
imgui_context = ImGui::CreateContext();
ImGui::SetCurrentContext(imgui_context);
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr;
io.LogFilename = nullptr;
io.DisplaySize = {static_cast<float>(UI_WIDTH), static_cast<float>(UI_HEIGHT)};
std::string font_path = File::GetUserPath(D_LOAD_IDX) + "OSD_Font.ttf";
if (!File::Exists(font_path))
font_path = File::GetSysDirectory() + DIR_SEP + RESOURCES_DIR + DIR_SEP + "OSD_Font.ttf";
if (File::Exists(font_path))
io.Fonts->AddFontFromFileTTF(font_path.c_str(), 30.0f);
else
io.Fonts->AddFontDefault();
if (!ImGui_ImplVulkan_LoadFunctions(
VK_API_VERSION_1_1,
[](const char* name, void* user_data) {
return vkGetInstanceProcAddr(reinterpret_cast<VkInstance>(user_data), name);
},
vulkan->GetVulkanInstance()))
{
ERROR_LOG_FMT(
OPENXR,
"OpenXR controller mapper: ImGui Vulkan function loading failed (surface/swapchain "
"entry points may be unavailable)");
return false;
}
ImGui_ImplVulkan_InitInfo info{};
info.ApiVersion = vulkan->GetDeviceInfo().apiVersion;
info.Instance = vulkan->GetVulkanInstance();
info.PhysicalDevice = vulkan->GetPhysicalDevice();
info.Device = vulkan->GetDevice();
info.QueueFamily = vulkan->GetGraphicsQueueFamilyIndex();
info.Queue = vulkan->GetGraphicsQueue();
info.RenderPass = render_pass;
// ImGui uses this count only for its rotating transient buffers. OpenXR is allowed to expose
// a one-image swapchain, while the renderer backend requires at least two transient slots.
info.MinImageCount = std::max(2u, static_cast<uint32_t>(render_images.size()));
info.ImageCount = info.MinImageCount;
info.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
info.DescriptorPoolSize = IMGUI_IMPL_VULKAN_MINIMUM_IMAGE_SAMPLER_POOL_SIZE;
info.CheckVkResultFn = RecordImGuiVulkanResult;
s_imgui_vulkan_error = VK_SUCCESS;
if (!ImGui_ImplVulkan_Init(&info))
{
ERROR_LOG_FMT(OPENXR, "OpenXR controller mapper: ImGui Vulkan initialization failed");
return false;
}
if (s_imgui_vulkan_error != VK_SUCCESS)
return false;
imgui_backend_initialized = true;
ImGuiStyle& style = ImGui::GetStyle();
ImGui::StyleColorsDark(&style);
style.WindowPadding = {0.0f, 0.0f};
style.FrameRounding = 10.0f;
style.FramePadding = {18.0f, 14.0f};
style.ItemSpacing = {18.0f, 18.0f};
style.ScrollbarSize = 24.0f;
style.ScrollbarRounding = 10.0f;
style.Colors[ImGuiCol_WindowBg] = {0.035f, 0.045f, 0.06f, 0.97f};
return true;
}
void UpdatePointer()
{
ImGuiIO& io = ImGui::GetIO();
const auto snapshot = Common::VR::OpenXRInputState::GetSnapshot();
pointer_visible.fill(false);
pointer_pressed.fill(false);
for (size_t hand = 0; hand < snapshot.controllers.size(); ++hand)
{
const auto& controller = snapshot.controllers[hand];
if (!controller.connected || !controller.aim_pose.valid)
continue;
const auto& pose = controller.aim_pose;
const XrQuaternionf orientation{pose.orientation[0], pose.orientation[1], pose.orientation[2],
pose.orientation[3]};
const XrVector3f origin{pose.position[0], pose.position[1], pose.position[2]};
const XrVector3f direction = RotateVector(orientation, {0.0f, 0.0f, -1.0f});
if (std::abs(direction.z) < 0.0001f)
continue;
const float t = (quad.pose.position.z - origin.z) / direction.z;
if (t <= 0.0f)
continue;
const float x = origin.x + direction.x * t - quad.pose.position.x;
const float y = origin.y + direction.y * t - quad.pose.position.y;
const float u = x / quad.size.width + 0.5f;
const float v = 0.5f - y / quad.size.height;
if (u < 0.0f || u > 1.0f || v < 0.0f || v > 1.0f)
continue;
pointer_visible[hand] = true;
pointer_positions[hand] = {u * UI_WIDTH, v * UI_HEIGHT};
pointer_pressed[hand] = controller.trigger_value > 0.55f || controller.trigger_button;
}
std::optional<size_t> selected_hand;
for (size_t hand = 0; hand < pointer_pressed.size(); ++hand)
{
if (pointer_visible[hand] && pointer_pressed[hand])
{
selected_hand = hand;
break;
}
}
if (!selected_hand && pointer_visible[active_pointer_hand])
selected_hand = active_pointer_hand;
if (!selected_hand)
{
for (size_t hand = 0; hand < pointer_visible.size(); ++hand)
{
if (pointer_visible[hand])
{
selected_hand = hand;
break;
}
}
}
const bool pointer_down = selected_hand && pointer_pressed[*selected_hand];
if (selected_hand)
{
active_pointer_hand = *selected_hand;
io.AddMousePosEvent(pointer_positions[*selected_hand].x, pointer_positions[*selected_hand].y);
}
else
{
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
}
if (pointer_down != previous_pointer_down)
{
io.AddMouseButtonEvent(0, pointer_down);
previous_pointer_down = pointer_down;
}
}
void DrawPointers() const
{
ImDrawList* draw_list = ImGui::GetForegroundDrawList();
constexpr std::array<ImU32, 2> colors{IM_COL32(35, 210, 255, 255), IM_COL32(255, 170, 35, 255)};
for (size_t hand = 0; hand < pointer_visible.size(); ++hand)
{
if (!pointer_visible[hand])
continue;
const float radius = pointer_pressed[hand] ? 15.0f : 11.0f;
const ImVec2 position = pointer_positions[hand];
draw_list->AddCircleFilled(position, radius, colors[hand]);
draw_list->AddCircle(position, radius + 4.0f, IM_COL32(255, 255, 255, 255), 0, 3.0f);
draw_list->AddCircleFilled(position, 3.0f, IM_COL32(255, 255, 255, 255));
}
}
bool Render(uint32_t index, BindingWizard& wizard)
{
auto& image = render_images[index];
const VkDevice device = vulkan->GetDevice();
if (vkWaitForFences(device, 1, &image.fence, VK_TRUE, UINT64_MAX) != VK_SUCCESS ||
vkResetFences(device, 1, &image.fence) != VK_SUCCESS ||
vkResetCommandPool(device, image.command_pool, 0) != VK_SUCCESS)
return false;
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (vkBeginCommandBuffer(image.command_buffer, &begin) != VK_SUCCESS)
return false;
if (!image.initialized)
{
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image.image;
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
barrier.subresourceRange.levelCount = 1;
barrier.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(image.command_buffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr,
1, &barrier);
image.initialized = true;
}
ImGui::SetCurrentContext(imgui_context);
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize = {static_cast<float>(UI_WIDTH), static_cast<float>(UI_HEIGHT)};
io.DeltaTime = 1.0f / 72.0f;
UpdatePointer();
ImGui_ImplVulkan_NewFrame();
ImGui::NewFrame();
wizard.DrawAndUpdate();
DrawPointers();
ImGui::Render();
VkClearValue clear{};
clear.color = {{0.035f, 0.045f, 0.06f, 0.97f}};
VkRenderPassBeginInfo render_info{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
render_info.renderPass = render_pass;
render_info.framebuffer = image.framebuffer;
render_info.renderArea.extent = {UI_WIDTH, UI_HEIGHT};
render_info.clearValueCount = 1;
render_info.pClearValues = &clear;
vkCmdBeginRenderPass(image.command_buffer, &render_info, VK_SUBPASS_CONTENTS_INLINE);
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), image.command_buffer);
vkCmdEndRenderPass(image.command_buffer);
if (vkEndCommandBuffer(image.command_buffer) != VK_SUCCESS)
return false;
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &image.command_buffer;
if (vkQueueSubmit(vulkan->GetGraphicsQueue(), 1, &submit, image.fence) != VK_SUCCESS ||
vkWaitForFences(device, 1, &image.fence, VK_TRUE, UINT64_MAX) != VK_SUCCESS)
return false;
return true;
}
bool RunFrame(BindingWizard& wizard)
{
if (!manager->WaitFrame() || !manager->BeginFrame())
return false;
if (manager->ShouldRender())
{
manager->LocateViews();
if (!quad_pose_initialized)
{
const auto& views = manager->GetEyeViews();
quad.pose.position.y = 0.5f * (views[0].pose.position.y + views[1].pose.position.y) +
UI_VERTICAL_OFFSET_METERS;
quad_pose_initialized = true;
}
}
std::vector<XrCompositionLayerBaseHeader*> layers;
if (manager->ShouldRender())
{
uint32_t index = 0;
XrSwapchainImageAcquireInfo acquire{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
if (XR_FAILED(xrAcquireSwapchainImage(swapchain, &acquire, &index)))
return false;
XrSwapchainImageWaitInfo wait{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
wait.timeout = XR_INFINITE_DURATION;
const bool rendered = XR_SUCCEEDED(xrWaitSwapchainImage(swapchain, &wait)) &&
index < render_images.size() && Render(index, wizard);
XrSwapchainImageReleaseInfo release{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
if (XR_FAILED(xrReleaseSwapchainImage(swapchain, &release)) || !rendered)
return false;
layers.push_back(reinterpret_cast<XrCompositionLayerBaseHeader*>(&quad));
}
return manager->EndFrame(layers);
}
void Cleanup()
{
if (vulkan)
vkDeviceWaitIdle(vulkan->GetDevice());
if (imgui_context)
{
ImGui::SetCurrentContext(imgui_context);
if (imgui_backend_initialized)
ImGui_ImplVulkan_Shutdown();
ImGui::DestroyContext(imgui_context);
imgui_context = nullptr;
ImGui::SetCurrentContext(previous_imgui_context);
previous_imgui_context = nullptr;
imgui_backend_initialized = false;
}
if (vulkan)
{
for (auto& image : render_images)
{
if (image.fence)
vkDestroyFence(vulkan->GetDevice(), image.fence, nullptr);
if (image.command_pool)
vkDestroyCommandPool(vulkan->GetDevice(), image.command_pool, nullptr);
if (image.framebuffer)
vkDestroyFramebuffer(vulkan->GetDevice(), image.framebuffer, nullptr);
if (image.view)
vkDestroyImageView(vulkan->GetDevice(), image.view, nullptr);
}
if (render_pass)
vkDestroyRenderPass(vulkan->GetDevice(), render_pass, nullptr);
}
render_images.clear();
xr_images.clear();
if (swapchain != XR_NULL_HANDLE)
{
xrDestroySwapchain(swapchain);
swapchain = XR_NULL_HANDLE;
}
manager.reset();
vulkan.reset();
if (vulkan_library_loaded)
{
Vulkan::UnloadVulkanLibrary();
vulkan_library_loaded = false;
}
if (backend_info_saved)
{
g_backend_info = std::move(previous_backend_info);
backend_info_saved = false;
}
Common::VR::OpenXRInputState::Reset();
s_utility_session_active.store(false, std::memory_order_release);
}
void ThreadMain()
{
Common::VR::OpenXRInputState::Reset();
BindingWizard wizard(target);
if (!wizard.IsValid())
{
Fail(OpenXRUtilitySessionFailure::InitializationFailed,
"selected controller has no bindable controls");
Cleanup();
return;
}
if (!InitializeOpenXR())
{
Fail(OpenXRUtilitySessionFailure::RuntimeUnavailable, "OpenXR runtime initialization failed");
Cleanup();
return;
}
if (!InitializeVulkan())
{
Fail(OpenXRUtilitySessionFailure::VulkanUnavailable,
"Vulkan initialization or runtime GPU selection failed");
Cleanup();
return;
}
if (!CreateXRSession())
{
Fail(OpenXRUtilitySessionFailure::InitializationFailed,
"OpenXR Vulkan session creation failed. See the OpenXR log for the runtime result.");
Cleanup();
return;
}
if (!CreateSwapchainAndImages())
{
Fail(OpenXRUtilitySessionFailure::InitializationFailed,
"OpenXR mapper quad swapchain creation failed. See the OpenXR log for the exact "
"Vulkan or runtime result.");
Cleanup();
return;
}
if (!InitializeImGui())
{
Fail(OpenXRUtilitySessionFailure::InitializationFailed,
"OpenXR mapper UI initialization failed. See the OpenXR log for the exact Vulkan "
"result.");
Cleanup();
return;
}
owner.m_state.store(OpenXRUtilitySessionState::Running, std::memory_order_release);
bool session_failed = false;
bool session_was_running = false;
while (!stop_requested.load(std::memory_order_acquire))
{
if (!manager->PollEvents())
{
session_failed = true;
break;
}
if (!manager->IsSessionRunning())
{
// READY is asynchronous, so an initially non-running session is expected. Once a
// session has begun, however, STOPPING must end the utility rather than leaving it
// indefinitely in the Running state without a frame loop.
if (session_was_running)
{
session_failed = true;
break;
}
std::this_thread::sleep_for(10ms);
continue;
}
session_was_running = true;
if (!RunFrame(wizard))
{
session_failed = true;
break;
}
const BindingWizard::Result result = wizard.GetResult();
if (result == BindingWizard::Result::Apply)
{
std::lock_guard lock(owner.m_result_mutex);
owner.m_pending_bindings = wizard.BuildPendingBindings();
owner.m_state.store(OpenXRUtilitySessionState::ApplyPending, std::memory_order_release);
break;
}
if (result == BindingWizard::Result::Cancel)
{
owner.m_state.store(OpenXRUtilitySessionState::Cancelled, std::memory_order_release);
break;
}
}
if (session_failed && !stop_requested.load(std::memory_order_acquire))
Fail(OpenXRUtilitySessionFailure::SessionLost, "OpenXR session ended unexpectedly");
else if (stop_requested.load(std::memory_order_acquire) &&
owner.m_state.load(std::memory_order_acquire) == OpenXRUtilitySessionState::Running)
owner.m_state.store(OpenXRUtilitySessionState::Cancelled, std::memory_order_release);
Cleanup();
}
};
OpenXRUtilitySession::OpenXRUtilitySession() : m_impl(std::make_unique<Impl>(*this))
{
}
OpenXRUtilitySession::~OpenXRUtilitySession()
{
Stop();
}
bool OpenXRUtilitySession::Start(OpenXRUtilitySessionTarget target)
{
Stop();
{
std::lock_guard lock(m_result_mutex);
m_failure_message.clear();
m_pending_bindings = {};
}
m_failure.store(OpenXRUtilitySessionFailure::None, std::memory_order_release);
m_state.store(OpenXRUtilitySessionState::Starting, std::memory_order_release);
if (target.port < 0 || target.port >= 4 ||
(target.type == OpenXRUtilitySessionTargetType::Hotkeys && target.port != 0) ||
Core::GetState(Core::System::GetInstance()) != Core::State::Uninitialized || g_openxr)
{
m_failure.store(OpenXRUtilitySessionFailure::SessionBusy, std::memory_order_release);
m_state.store(OpenXRUtilitySessionState::Failed, std::memory_order_release);
return false;
}
bool expected = false;
if (!s_utility_session_active.compare_exchange_strong(expected, true))
{
m_failure.store(OpenXRUtilitySessionFailure::SessionBusy, std::memory_order_release);
m_state.store(OpenXRUtilitySessionState::Failed, std::memory_order_release);
return false;
}
m_impl->target = target;
m_impl->stop_requested.store(false, std::memory_order_release);
m_impl->thread = std::thread(&Impl::ThreadMain, m_impl.get());
return true;
}
bool OpenXRUtilitySession::Start(int wiimote_port)
{
return Start({OpenXRUtilitySessionTargetType::WiiRemote, wiimote_port});
}
void OpenXRUtilitySession::RequestStop()
{
m_impl->stop_requested.store(true, std::memory_order_release);
}
void OpenXRUtilitySession::Stop()
{
RequestStop();
if (m_impl->thread.joinable())
m_impl->thread.join();
if (m_state.load(std::memory_order_acquire) == OpenXRUtilitySessionState::Starting)
m_state.store(OpenXRUtilitySessionState::Idle, std::memory_order_release);
}
OpenXRUtilitySessionState OpenXRUtilitySession::GetState() const
{
return m_state.load(std::memory_order_acquire);
}
OpenXRUtilitySessionFailure OpenXRUtilitySession::GetFailureReason() const
{
return m_failure.load(std::memory_order_acquire);
}
std::string OpenXRUtilitySession::GetFailureMessage() const
{
{
std::lock_guard lock(m_result_mutex);
if (!m_failure_message.empty())
return m_failure_message;
}
switch (GetFailureReason())
{
case OpenXRUtilitySessionFailure::None:
return {};
case OpenXRUtilitySessionFailure::RuntimeUnavailable:
return Common::GetStringT("No usable OpenXR runtime was found.");
case OpenXRUtilitySessionFailure::VulkanUnavailable:
return Common::GetStringT("The OpenXR runtime could not create a compatible Vulkan device.");
case OpenXRUtilitySessionFailure::SessionBusy:
return Common::GetStringT("Another game or OpenXR session is already running.");
case OpenXRUtilitySessionFailure::InitializationFailed:
return Common::GetStringT("The OpenXR controller mapper could not be initialized.");
case OpenXRUtilitySessionFailure::SessionLost:
return Common::GetStringT("The OpenXR controller mapper session was lost.");
}
return {};
}
OpenXRPendingBindings OpenXRUtilitySession::TakePendingBindings()
{
std::lock_guard lock(m_result_mutex);
return std::exchange(m_pending_bindings, {});
}
void OpenXRUtilitySession::MarkApplied()
{
if (GetState() == OpenXRUtilitySessionState::ApplyPending)
m_state.store(OpenXRUtilitySessionState::Applied, std::memory_order_release);
}
} // namespace VR
#endif