mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 08:00:25 +02:00
1058 lines
39 KiB
C++
1058 lines
39 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
// The runtime source tree is globbed even in explicitly non-VR builds. Keep
|
|
// this translation unit dependency-free in that configuration; callers that
|
|
// include the public OpenXR headers must use the same feature guard.
|
|
#if defined(MKW_ENABLE_OPENXR)
|
|
|
|
#include "vr/openxr_runtime.h"
|
|
#include "vr/openxr_diagnostics.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <sstream>
|
|
#include <thread>
|
|
#include <utility>
|
|
|
|
namespace mkw::vr {
|
|
namespace {
|
|
|
|
template <typename T>
|
|
bool Contains(const std::vector<T>& values, const T& value) {
|
|
return std::find(values.begin(), values.end(), value) != values.end();
|
|
}
|
|
|
|
void CopyOpenXRName(char* destination, size_t destination_size, std::string_view source) {
|
|
if (destination_size == 0) {
|
|
return;
|
|
}
|
|
const size_t length = std::min(destination_size - 1, source.size());
|
|
std::memcpy(destination, source.data(), length);
|
|
destination[length] = '\0';
|
|
}
|
|
|
|
XrPosef IdentityPose() {
|
|
return {{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}};
|
|
}
|
|
|
|
bool IsFinitePositive(float value) noexcept {
|
|
// mkw_runtime_common uses -ffast-math, where std::isfinite may be folded
|
|
// away. Check the IEEE-754 representation before using the scale in
|
|
// rounding and integer conversion.
|
|
uint32_t bits = 0;
|
|
std::memcpy(&bits, &value, sizeof(bits));
|
|
return (bits & 0x80000000u) == 0 && (bits & 0x7fffffffu) != 0 &&
|
|
(bits & 0x7f800000u) != 0x7f800000u;
|
|
}
|
|
|
|
uint32_t ScaledDimension(uint32_t recommended, uint32_t maximum, float scale) {
|
|
const double scaled = std::round(static_cast<double>(recommended) *
|
|
static_cast<double>(scale));
|
|
const double clamped = std::clamp(
|
|
scaled, 1.0, static_cast<double>(std::max(maximum, 1u)));
|
|
return static_cast<uint32_t>(clamped);
|
|
}
|
|
|
|
const char* ReferenceSpaceName(XrReferenceSpaceType type) {
|
|
switch (type) {
|
|
case XR_REFERENCE_SPACE_TYPE_VIEW:
|
|
return "VIEW";
|
|
case XR_REFERENCE_SPACE_TYPE_LOCAL:
|
|
return "LOCAL";
|
|
case XR_REFERENCE_SPACE_TYPE_STAGE:
|
|
return "STAGE";
|
|
default:
|
|
return "OTHER";
|
|
}
|
|
}
|
|
|
|
const char* SessionStateName(XrSessionState state) {
|
|
switch (state) {
|
|
case XR_SESSION_STATE_UNKNOWN:
|
|
return "UNKNOWN";
|
|
case XR_SESSION_STATE_IDLE:
|
|
return "IDLE";
|
|
case XR_SESSION_STATE_READY:
|
|
return "READY";
|
|
case XR_SESSION_STATE_SYNCHRONIZED:
|
|
return "SYNCHRONIZED";
|
|
case XR_SESSION_STATE_VISIBLE:
|
|
return "VISIBLE";
|
|
case XR_SESSION_STATE_FOCUSED:
|
|
return "FOCUSED";
|
|
case XR_SESSION_STATE_STOPPING:
|
|
return "STOPPING";
|
|
case XR_SESSION_STATE_LOSS_PENDING:
|
|
return "LOSS_PENDING";
|
|
case XR_SESSION_STATE_EXITING:
|
|
return "EXITING";
|
|
default:
|
|
return "INVALID";
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
OpenXRRuntime::OpenXRRuntime(OpenXRLogCallback logger)
|
|
: m_logger(std::move(logger)) {}
|
|
|
|
OpenXRRuntime::~OpenXRRuntime() {
|
|
Shutdown();
|
|
}
|
|
|
|
bool OpenXRRuntime::Initialize(const OpenXRConfig& config) {
|
|
ClearError();
|
|
if (IsInitialized()) {
|
|
return Fail(XR_ERROR_CALL_ORDER_INVALID, "Initialize",
|
|
"OpenXR is already initialized");
|
|
}
|
|
if (config.application_name.empty()) {
|
|
return Fail(XR_ERROR_VALIDATION_FAILURE, "Initialize",
|
|
"application_name must not be empty");
|
|
}
|
|
if (!IsFinitePositive(config.resolution_scale)) {
|
|
return Fail(XR_ERROR_VALIDATION_FAILURE, "Initialize",
|
|
"resolution_scale must be finite and greater than zero");
|
|
}
|
|
|
|
m_config = config;
|
|
if (!EnumerateInstanceCapabilities() || !CreateInstance() ||
|
|
!InitializeSystem() || !EnumerateViewConfiguration() ||
|
|
!SelectEnvironmentBlendMode()) {
|
|
if (m_instance != XR_NULL_HANDLE) {
|
|
xrDestroyInstance(m_instance);
|
|
}
|
|
ResetInstanceState();
|
|
return false;
|
|
}
|
|
|
|
std::ostringstream message;
|
|
message << "OpenXR initialized: runtime '" << m_runtime_info.runtime_name
|
|
<< "', system '" << m_runtime_info.system_name << "'";
|
|
Log(OpenXRLogLevel::Info, message.str());
|
|
return true;
|
|
}
|
|
|
|
bool OpenXRRuntime::EnumerateInstanceCapabilities() {
|
|
uint32_t extension_count = 0;
|
|
if (!Check(xrEnumerateInstanceExtensionProperties(
|
|
nullptr, 0, &extension_count, nullptr),
|
|
"xrEnumerateInstanceExtensionProperties(count)")) {
|
|
return false;
|
|
}
|
|
|
|
std::vector<XrExtensionProperties> extension_properties(
|
|
extension_count, XrExtensionProperties{XR_TYPE_EXTENSION_PROPERTIES});
|
|
if (extension_count != 0 &&
|
|
!Check(xrEnumerateInstanceExtensionProperties(
|
|
nullptr, extension_count, &extension_count,
|
|
extension_properties.data()),
|
|
"xrEnumerateInstanceExtensionProperties")) {
|
|
return false;
|
|
}
|
|
|
|
m_available_extensions.clear();
|
|
m_available_extensions.reserve(extension_count);
|
|
for (const XrExtensionProperties& extension : extension_properties) {
|
|
m_available_extensions.emplace_back(extension.extensionName);
|
|
}
|
|
|
|
uint32_t layer_count = 0;
|
|
if (!Check(xrEnumerateApiLayerProperties(0, &layer_count, nullptr),
|
|
"xrEnumerateApiLayerProperties(count)")) {
|
|
return false;
|
|
}
|
|
std::vector<XrApiLayerProperties> layer_properties(
|
|
layer_count, XrApiLayerProperties{XR_TYPE_API_LAYER_PROPERTIES});
|
|
if (layer_count != 0 &&
|
|
!Check(xrEnumerateApiLayerProperties(
|
|
layer_count, &layer_count, layer_properties.data()),
|
|
"xrEnumerateApiLayerProperties")) {
|
|
return false;
|
|
}
|
|
|
|
m_available_api_layers.clear();
|
|
m_available_api_layers.reserve(layer_count);
|
|
for (const XrApiLayerProperties& layer : layer_properties) {
|
|
m_available_api_layers.emplace_back(layer.layerName);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OpenXRRuntime::CreateInstance() {
|
|
m_enabled_extensions.clear();
|
|
for (const std::string& extension : m_config.required_extensions) {
|
|
if (!Contains(m_available_extensions, extension)) {
|
|
return Fail(XR_ERROR_EXTENSION_NOT_PRESENT, "xrCreateInstance",
|
|
"required extension is unavailable: " + extension);
|
|
}
|
|
if (!Contains(m_enabled_extensions, extension)) {
|
|
m_enabled_extensions.push_back(extension);
|
|
}
|
|
}
|
|
for (const std::string& extension : m_config.optional_extensions) {
|
|
if (Contains(m_available_extensions, extension) &&
|
|
!Contains(m_enabled_extensions, extension)) {
|
|
m_enabled_extensions.push_back(extension);
|
|
}
|
|
}
|
|
|
|
m_enabled_api_layers.clear();
|
|
for (const std::string& layer : m_config.required_api_layers) {
|
|
if (!Contains(m_available_api_layers, layer)) {
|
|
return Fail(XR_ERROR_API_LAYER_NOT_PRESENT, "xrCreateInstance",
|
|
"required API layer is unavailable: " + layer);
|
|
}
|
|
if (!Contains(m_enabled_api_layers, layer)) {
|
|
m_enabled_api_layers.push_back(layer);
|
|
}
|
|
}
|
|
for (const std::string& layer : m_config.optional_api_layers) {
|
|
if (Contains(m_available_api_layers, layer) &&
|
|
!Contains(m_enabled_api_layers, layer)) {
|
|
m_enabled_api_layers.push_back(layer);
|
|
}
|
|
}
|
|
|
|
std::vector<const char*> extension_names;
|
|
extension_names.reserve(m_enabled_extensions.size());
|
|
for (const std::string& extension : m_enabled_extensions) {
|
|
extension_names.push_back(extension.c_str());
|
|
}
|
|
std::vector<const char*> layer_names;
|
|
layer_names.reserve(m_enabled_api_layers.size());
|
|
for (const std::string& layer : m_enabled_api_layers) {
|
|
layer_names.push_back(layer.c_str());
|
|
}
|
|
|
|
XrInstanceCreateInfo create_info{XR_TYPE_INSTANCE_CREATE_INFO};
|
|
create_info.next = m_config.instance_create_next;
|
|
CopyOpenXRName(create_info.applicationInfo.applicationName,
|
|
XR_MAX_APPLICATION_NAME_SIZE, m_config.application_name);
|
|
create_info.applicationInfo.applicationVersion = m_config.application_version;
|
|
CopyOpenXRName(create_info.applicationInfo.engineName,
|
|
XR_MAX_ENGINE_NAME_SIZE, m_config.engine_name);
|
|
create_info.applicationInfo.engineVersion = m_config.engine_version;
|
|
create_info.applicationInfo.apiVersion = m_config.api_version;
|
|
create_info.enabledExtensionCount =
|
|
static_cast<uint32_t>(extension_names.size());
|
|
create_info.enabledExtensionNames = extension_names.data();
|
|
create_info.enabledApiLayerCount = static_cast<uint32_t>(layer_names.size());
|
|
create_info.enabledApiLayerNames = layer_names.data();
|
|
|
|
if (!Check(xrCreateInstance(&create_info, &m_instance), "xrCreateInstance")) {
|
|
return false;
|
|
}
|
|
|
|
XrInstanceProperties instance_properties{XR_TYPE_INSTANCE_PROPERTIES};
|
|
if (!Check(xrGetInstanceProperties(m_instance, &instance_properties),
|
|
"xrGetInstanceProperties")) {
|
|
return false;
|
|
}
|
|
m_runtime_info.runtime_name = instance_properties.runtimeName;
|
|
m_runtime_info.runtime_version = instance_properties.runtimeVersion;
|
|
return true;
|
|
}
|
|
|
|
bool OpenXRRuntime::InitializeSystem() {
|
|
XrSystemGetInfo get_info{XR_TYPE_SYSTEM_GET_INFO};
|
|
get_info.formFactor = m_config.form_factor;
|
|
if (!Check(xrGetSystem(m_instance, &get_info, &m_system_id), "xrGetSystem")) {
|
|
return false;
|
|
}
|
|
|
|
XrSystemProperties properties{XR_TYPE_SYSTEM_PROPERTIES};
|
|
if (!Check(xrGetSystemProperties(m_instance, m_system_id, &properties),
|
|
"xrGetSystemProperties")) {
|
|
return false;
|
|
}
|
|
m_runtime_info.system_name = properties.systemName;
|
|
m_runtime_info.vendor_id = properties.vendorId;
|
|
m_runtime_info.max_layer_count = properties.graphicsProperties.maxLayerCount;
|
|
m_runtime_info.supports_orientation_tracking =
|
|
properties.trackingProperties.orientationTracking == XR_TRUE;
|
|
m_runtime_info.supports_position_tracking =
|
|
properties.trackingProperties.positionTracking == XR_TRUE;
|
|
return true;
|
|
}
|
|
|
|
bool OpenXRRuntime::EnumerateViewConfiguration() {
|
|
uint32_t view_count = 0;
|
|
if (!Check(xrEnumerateViewConfigurationViews(
|
|
m_instance, m_system_id, m_config.view_configuration,
|
|
0, &view_count, nullptr),
|
|
"xrEnumerateViewConfigurationViews(count)")) {
|
|
return false;
|
|
}
|
|
if (view_count != kOpenXREyeCount) {
|
|
std::ostringstream detail;
|
|
detail << "PRIMARY_STEREO must expose exactly " << kOpenXREyeCount
|
|
<< " views, runtime returned " << view_count;
|
|
return Fail(XR_ERROR_VIEW_CONFIGURATION_TYPE_UNSUPPORTED,
|
|
"xrEnumerateViewConfigurationViews", detail.str());
|
|
}
|
|
|
|
std::array<XrViewConfigurationView, kOpenXREyeCount> properties{};
|
|
for (XrViewConfigurationView& property : properties) {
|
|
property.type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
|
|
}
|
|
if (!Check(xrEnumerateViewConfigurationViews(
|
|
m_instance, m_system_id, m_config.view_configuration,
|
|
view_count, &view_count, properties.data()),
|
|
"xrEnumerateViewConfigurationViews")) {
|
|
return false;
|
|
}
|
|
|
|
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
|
OpenXRViewConfiguration& destination = m_view_configuration[eye];
|
|
destination.properties = properties[eye];
|
|
destination.render_width = ScaledDimension(
|
|
properties[eye].recommendedImageRectWidth,
|
|
properties[eye].maxImageRectWidth, m_config.resolution_scale);
|
|
destination.render_height = ScaledDimension(
|
|
properties[eye].recommendedImageRectHeight,
|
|
properties[eye].maxImageRectHeight, m_config.resolution_scale);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OpenXRRuntime::SelectEnvironmentBlendMode() {
|
|
uint32_t blend_mode_count = 0;
|
|
if (!Check(xrEnumerateEnvironmentBlendModes(
|
|
m_instance, m_system_id, m_config.view_configuration,
|
|
0, &blend_mode_count, nullptr),
|
|
"xrEnumerateEnvironmentBlendModes(count)")) {
|
|
return false;
|
|
}
|
|
if (blend_mode_count == 0) {
|
|
return Fail(XR_ERROR_ENVIRONMENT_BLEND_MODE_UNSUPPORTED,
|
|
"xrEnumerateEnvironmentBlendModes",
|
|
"runtime returned no environment blend modes");
|
|
}
|
|
|
|
m_supported_blend_modes.resize(blend_mode_count);
|
|
if (!Check(xrEnumerateEnvironmentBlendModes(
|
|
m_instance, m_system_id, m_config.view_configuration,
|
|
blend_mode_count, &blend_mode_count,
|
|
m_supported_blend_modes.data()),
|
|
"xrEnumerateEnvironmentBlendModes")) {
|
|
return false;
|
|
}
|
|
m_supported_blend_modes.resize(blend_mode_count);
|
|
|
|
if (Contains(m_supported_blend_modes, m_config.preferred_blend_mode)) {
|
|
m_blend_mode = m_config.preferred_blend_mode;
|
|
return true;
|
|
}
|
|
if (Contains(m_supported_blend_modes, XR_ENVIRONMENT_BLEND_MODE_OPAQUE)) {
|
|
m_blend_mode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
|
} else {
|
|
m_blend_mode = m_supported_blend_modes.front();
|
|
}
|
|
Log(OpenXRLogLevel::Warning,
|
|
"preferred environment blend mode is unavailable; using runtime fallback");
|
|
return true;
|
|
}
|
|
|
|
bool OpenXRRuntime::CreateSession(const void* graphics_binding) {
|
|
ClearError();
|
|
if (!IsInitialized()) {
|
|
return Fail(XR_ERROR_CALL_ORDER_INVALID, "CreateSession",
|
|
"Initialize must succeed first");
|
|
}
|
|
if (m_instance_loss_pending) {
|
|
return Fail(XR_ERROR_INSTANCE_LOST, "CreateSession",
|
|
"the OpenXR instance is loss-pending");
|
|
}
|
|
if (m_session_loss_pending) {
|
|
return Fail(XR_ERROR_SESSION_LOST, "CreateSession",
|
|
"session loss requires a full OpenXR reinitialization");
|
|
}
|
|
if (HasSession()) {
|
|
return Fail(XR_ERROR_CALL_ORDER_INVALID, "CreateSession",
|
|
"a session already exists");
|
|
}
|
|
if (graphics_binding == nullptr) {
|
|
return Fail(XR_ERROR_GRAPHICS_DEVICE_INVALID, "CreateSession",
|
|
"graphics binding must not be null");
|
|
}
|
|
|
|
XrSessionCreateInfo create_info{XR_TYPE_SESSION_CREATE_INFO};
|
|
create_info.next = graphics_binding;
|
|
create_info.systemId = m_system_id;
|
|
if (!Check(xrCreateSession(m_instance, &create_info, &m_session),
|
|
"xrCreateSession")) {
|
|
m_session = XR_NULL_HANDLE;
|
|
return false;
|
|
}
|
|
|
|
if (!CreateReferenceSpaces() || !EnumerateSwapchainFormats()) {
|
|
DestroyReferenceSpaces();
|
|
xrDestroySession(m_session);
|
|
m_session = XR_NULL_HANDLE;
|
|
ResetSessionState();
|
|
return false;
|
|
}
|
|
|
|
m_session_state = XR_SESSION_STATE_UNKNOWN;
|
|
m_exit_requested = false;
|
|
Log(OpenXRLogLevel::Info,
|
|
"OpenXR session created; waiting for the runtime READY event");
|
|
return true;
|
|
}
|
|
|
|
bool OpenXRRuntime::GetInstanceProcAddress(
|
|
const char* name, PFN_xrVoidFunction* function) {
|
|
ClearError();
|
|
if (!IsInitialized()) {
|
|
return Fail(XR_ERROR_CALL_ORDER_INVALID, "xrGetInstanceProcAddr",
|
|
"OpenXR is not initialized");
|
|
}
|
|
if (name == nullptr || name[0] == '\0' || function == nullptr) {
|
|
return Fail(XR_ERROR_VALIDATION_FAILURE, "xrGetInstanceProcAddr",
|
|
"function name and output pointer must be valid");
|
|
}
|
|
*function = nullptr;
|
|
return Check(xrGetInstanceProcAddr(m_instance, name, function),
|
|
"xrGetInstanceProcAddr");
|
|
}
|
|
|
|
bool OpenXRRuntime::CreateReferenceSpaces() {
|
|
uint32_t space_count = 0;
|
|
if (!Check(xrEnumerateReferenceSpaces(m_session, 0, &space_count, nullptr),
|
|
"xrEnumerateReferenceSpaces(count)")) {
|
|
return false;
|
|
}
|
|
m_supported_reference_spaces.resize(space_count);
|
|
if (space_count != 0 &&
|
|
!Check(xrEnumerateReferenceSpaces(
|
|
m_session, space_count, &space_count,
|
|
m_supported_reference_spaces.data()),
|
|
"xrEnumerateReferenceSpaces")) {
|
|
return false;
|
|
}
|
|
m_supported_reference_spaces.resize(space_count);
|
|
|
|
XrReferenceSpaceCreateInfo view_info{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
|
view_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
|
|
view_info.poseInReferenceSpace = IdentityPose();
|
|
if (!Check(xrCreateReferenceSpace(m_session, &view_info, &m_view_space),
|
|
"xrCreateReferenceSpace(VIEW)")) {
|
|
return false;
|
|
}
|
|
|
|
m_app_space_type = m_config.reference_space;
|
|
if (!Contains(m_supported_reference_spaces, m_app_space_type)) {
|
|
if (Contains(m_supported_reference_spaces, XR_REFERENCE_SPACE_TYPE_LOCAL)) {
|
|
m_app_space_type = XR_REFERENCE_SPACE_TYPE_LOCAL;
|
|
Log(OpenXRLogLevel::Warning,
|
|
"requested reference space is unavailable; using LOCAL");
|
|
} else if (Contains(m_supported_reference_spaces,
|
|
XR_REFERENCE_SPACE_TYPE_STAGE)) {
|
|
m_app_space_type = XR_REFERENCE_SPACE_TYPE_STAGE;
|
|
Log(OpenXRLogLevel::Warning,
|
|
"requested reference space is unavailable; using STAGE");
|
|
} else {
|
|
return Fail(XR_ERROR_REFERENCE_SPACE_UNSUPPORTED,
|
|
"xrCreateReferenceSpace",
|
|
"runtime exposes neither requested, LOCAL, nor STAGE space");
|
|
}
|
|
}
|
|
|
|
XrReferenceSpaceCreateInfo app_info{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
|
app_info.referenceSpaceType = m_app_space_type;
|
|
app_info.poseInReferenceSpace = IdentityPose();
|
|
return Check(xrCreateReferenceSpace(m_session, &app_info, &m_app_space),
|
|
"xrCreateReferenceSpace(application)");
|
|
}
|
|
|
|
bool OpenXRRuntime::EnumerateSwapchainFormats() {
|
|
uint32_t format_count = 0;
|
|
if (!Check(xrEnumerateSwapchainFormats(
|
|
m_session, 0, &format_count, nullptr),
|
|
"xrEnumerateSwapchainFormats(count)")) {
|
|
return false;
|
|
}
|
|
if (format_count == 0) {
|
|
return Fail(XR_ERROR_SWAPCHAIN_FORMAT_UNSUPPORTED,
|
|
"xrEnumerateSwapchainFormats",
|
|
"runtime returned no swapchain formats");
|
|
}
|
|
m_swapchain_formats.resize(format_count);
|
|
if (!Check(xrEnumerateSwapchainFormats(
|
|
m_session, format_count, &format_count,
|
|
m_swapchain_formats.data()),
|
|
"xrEnumerateSwapchainFormats")) {
|
|
return false;
|
|
}
|
|
m_swapchain_formats.resize(format_count);
|
|
return true;
|
|
}
|
|
|
|
OpenXREventStatus OpenXRRuntime::PollEvents() {
|
|
if (!IsInitialized()) {
|
|
Fail(XR_ERROR_CALL_ORDER_INVALID, "PollEvents",
|
|
"OpenXR is not initialized");
|
|
return OpenXREventStatus::Error;
|
|
}
|
|
|
|
for (;;) {
|
|
XrEventDataBuffer event{XR_TYPE_EVENT_DATA_BUFFER};
|
|
const XrResult result = xrPollEvent(m_instance, &event);
|
|
if (result == XR_EVENT_UNAVAILABLE) {
|
|
return ShouldExit() ? OpenXREventStatus::ExitRequested
|
|
: OpenXREventStatus::Continue;
|
|
}
|
|
if (XR_FAILED(result)) {
|
|
Check(result, "xrPollEvent");
|
|
if (result == XR_ERROR_INSTANCE_LOST) {
|
|
m_instance_loss_pending = true;
|
|
}
|
|
return OpenXREventStatus::Error;
|
|
}
|
|
|
|
switch (event.type) {
|
|
case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: {
|
|
const auto& state_event =
|
|
*reinterpret_cast<const XrEventDataSessionStateChanged*>(&event);
|
|
if (state_event.session == m_session &&
|
|
!HandleSessionStateChanged(state_event)) {
|
|
return OpenXREventStatus::Error;
|
|
}
|
|
break;
|
|
}
|
|
case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING:
|
|
m_instance_loss_pending = true;
|
|
m_session_running = false;
|
|
ResetFrameState();
|
|
Log(OpenXRLogLevel::Warning,
|
|
"OpenXR runtime reported instance loss pending");
|
|
break;
|
|
case XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING: {
|
|
const auto& space_event =
|
|
*reinterpret_cast<const XrEventDataReferenceSpaceChangePending*>(&event);
|
|
if (space_event.session == m_session) {
|
|
diagnostics::OnReferenceSpaceChange(ReferenceSpaceName(space_event.referenceSpaceType),
|
|
space_event.poseValid == XR_TRUE,
|
|
space_event.changeTime);
|
|
}
|
|
// This slot is consumed to invalidate transforms located in the
|
|
// application space. Events for VIEW or another supported type
|
|
// must not overwrite a pending LOCAL/STAGE change.
|
|
if (space_event.session == m_session &&
|
|
space_event.referenceSpaceType == m_app_space_type) {
|
|
if (++m_reference_space_change.serial == 0) {
|
|
++m_reference_space_change.serial;
|
|
}
|
|
m_reference_space_change.type = space_event.referenceSpaceType;
|
|
m_reference_space_change.change_time = space_event.changeTime;
|
|
m_reference_space_change.pose_in_previous_space_valid =
|
|
space_event.poseValid == XR_TRUE;
|
|
m_reference_space_change.pose_in_previous_space =
|
|
space_event.poseInPreviousSpace;
|
|
m_pending_app_space_changes.push_back(m_reference_space_change);
|
|
}
|
|
break;
|
|
}
|
|
case XR_TYPE_EVENT_DATA_EVENTS_LOST: {
|
|
const auto& lost_event =
|
|
*reinterpret_cast<const XrEventDataEventsLost*>(&event);
|
|
std::ostringstream message;
|
|
message << "OpenXR runtime lost " << lost_event.lostEventCount
|
|
<< " event(s)";
|
|
Log(OpenXRLogLevel::Warning, message.str());
|
|
break;
|
|
}
|
|
case XR_TYPE_EVENT_DATA_PERF_SETTINGS_EXT: {
|
|
// XR_EXT_performance_settings: the runtime reports when a domain's compositing,
|
|
// rendering or thermal state moves between normal, warning and impaired. Logged so a
|
|
// throttled headset explains a frame-rate drop in the session log.
|
|
const auto& perf_event =
|
|
*reinterpret_cast<const XrEventDataPerfSettingsEXT*>(&event);
|
|
const auto sub_domain = [](XrPerfSettingsSubDomainEXT value) {
|
|
switch (value) {
|
|
case XR_PERF_SETTINGS_SUB_DOMAIN_COMPOSITING_EXT: return "compositing";
|
|
case XR_PERF_SETTINGS_SUB_DOMAIN_RENDERING_EXT: return "rendering";
|
|
case XR_PERF_SETTINGS_SUB_DOMAIN_THERMAL_EXT: return "thermal";
|
|
default: return "unknown";
|
|
}
|
|
};
|
|
const auto notification = [](XrPerfSettingsNotificationLevelEXT value) {
|
|
switch (value) {
|
|
case XR_PERF_SETTINGS_NOTIF_LEVEL_NORMAL_EXT: return "normal";
|
|
case XR_PERF_SETTINGS_NOTIF_LEVEL_WARNING_EXT: return "warning";
|
|
case XR_PERF_SETTINGS_NOTIF_LEVEL_IMPAIRED_EXT: return "impaired";
|
|
default: return "unknown";
|
|
}
|
|
};
|
|
std::ostringstream message;
|
|
message << "OpenXR performance notification: "
|
|
<< (perf_event.domain == XR_PERF_SETTINGS_DOMAIN_CPU_EXT ? "CPU" : "GPU") << " "
|
|
<< sub_domain(perf_event.subDomain) << " " << notification(perf_event.fromLevel)
|
|
<< " -> " << notification(perf_event.toLevel);
|
|
Log(perf_event.toLevel == XR_PERF_SETTINGS_NOTIF_LEVEL_NORMAL_EXT ? OpenXRLogLevel::Info
|
|
: OpenXRLogLevel::Warning,
|
|
message.str());
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool OpenXRRuntime::HandleSessionStateChanged(
|
|
const XrEventDataSessionStateChanged& event) {
|
|
m_session_state = event.state;
|
|
std::ostringstream message;
|
|
message << "OpenXR session state -> " << SessionStateName(event.state);
|
|
Log(OpenXRLogLevel::Info, message.str());
|
|
|
|
switch (event.state) {
|
|
case XR_SESSION_STATE_READY: {
|
|
if (m_shutting_down_session || m_session_running) {
|
|
return true;
|
|
}
|
|
XrSessionBeginInfo begin_info{XR_TYPE_SESSION_BEGIN_INFO};
|
|
begin_info.primaryViewConfigurationType = m_config.view_configuration;
|
|
if (!Check(xrBeginSession(m_session, &begin_info), "xrBeginSession")) {
|
|
return false;
|
|
}
|
|
ResetFrameState();
|
|
m_session_running = true;
|
|
if (++m_session_run_serial == 0) {
|
|
++m_session_run_serial;
|
|
}
|
|
return true;
|
|
}
|
|
case XR_SESSION_STATE_STOPPING: {
|
|
if (m_session_running) {
|
|
const XrResult result = xrEndSession(m_session);
|
|
// The OpenXR session is no longer running after any xrEndSession
|
|
// call, including one that returns an error.
|
|
m_session_running = false;
|
|
ResetFrameState();
|
|
if (!Check(result, "xrEndSession")) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
case XR_SESSION_STATE_EXITING:
|
|
m_exit_requested = true;
|
|
m_session_running = false;
|
|
ResetFrameState();
|
|
return true;
|
|
case XR_SESSION_STATE_LOSS_PENDING:
|
|
m_session_loss_pending = true;
|
|
m_session_running = false;
|
|
ResetFrameState();
|
|
return true;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool OpenXRRuntime::RequestExitSession() {
|
|
ClearError();
|
|
if (!HasSession() || !m_session_running) {
|
|
return true;
|
|
}
|
|
return Check(xrRequestExitSession(m_session), "xrRequestExitSession");
|
|
}
|
|
|
|
void OpenXRRuntime::ObserveResult(XrResult result) noexcept {
|
|
if (result == XR_SESSION_LOSS_PENDING) {
|
|
if (!m_session_loss_pending) {
|
|
Log(OpenXRLogLevel::Warning,
|
|
"OpenXR reported XR_SESSION_LOSS_PENDING");
|
|
}
|
|
m_session_loss_pending = true;
|
|
} else if (result == XR_ERROR_SESSION_LOST) {
|
|
m_session_loss_pending = true;
|
|
m_session_running = false;
|
|
ResetFrameState();
|
|
} else if (result == XR_ERROR_INSTANCE_LOST) {
|
|
m_instance_loss_pending = true;
|
|
m_session_running = false;
|
|
ResetFrameState();
|
|
}
|
|
}
|
|
|
|
OpenXRFrameStatus OpenXRRuntime::WaitFrame(OpenXRFrame& frame) {
|
|
ClearError();
|
|
if (ShouldExit()) {
|
|
return OpenXRFrameStatus::ExitRequested;
|
|
}
|
|
if (!HasSession() || !m_session_running) {
|
|
return OpenXRFrameStatus::SessionNotRunning;
|
|
}
|
|
if (m_frame_phase != FramePhase::Idle) {
|
|
Fail(XR_ERROR_CALL_ORDER_INVALID, "xrWaitFrame",
|
|
"the previous frame has not been ended");
|
|
return OpenXRFrameStatus::Error;
|
|
}
|
|
|
|
XrFrameWaitInfo wait_info{XR_TYPE_FRAME_WAIT_INFO};
|
|
XrFrameState state{XR_TYPE_FRAME_STATE};
|
|
const diagnostics::Stopwatch wait_timer;
|
|
if (!Check(xrWaitFrame(m_session, &wait_info, &state), "xrWaitFrame")) {
|
|
return OpenXRFrameStatus::Error;
|
|
}
|
|
diagnostics::OnWaitFrame(wait_timer, state.predictedDisplayTime, state.predictedDisplayPeriod);
|
|
|
|
frame = {};
|
|
frame.serial = m_next_frame_serial++;
|
|
frame.predicted_display_time = state.predictedDisplayTime;
|
|
frame.predicted_display_period = state.predictedDisplayPeriod;
|
|
frame.should_render = state.shouldRender == XR_TRUE;
|
|
m_active_frame_serial = frame.serial;
|
|
m_active_frame_display_time = frame.predicted_display_time;
|
|
m_frame_phase = FramePhase::Waited;
|
|
return OpenXRFrameStatus::Ready;
|
|
}
|
|
|
|
bool OpenXRRuntime::BeginFrame(const OpenXRFrame& frame) {
|
|
ClearError();
|
|
if (!IsFrameTokenCurrent(frame, FramePhase::Waited)) {
|
|
return Fail(XR_ERROR_CALL_ORDER_INVALID, "xrBeginFrame",
|
|
"frame token is stale or xrWaitFrame was not called");
|
|
}
|
|
|
|
XrFrameBeginInfo begin_info{XR_TYPE_FRAME_BEGIN_INFO};
|
|
const XrResult result = diagnostics::Measure(diagnostics::Stage::BeginCall, [&] {
|
|
return xrBeginFrame(m_session, &begin_info);
|
|
});
|
|
if (XR_FAILED(result)) {
|
|
m_frame_phase = FramePhase::Idle;
|
|
m_active_frame_serial = 0;
|
|
m_active_frame_display_time = 0;
|
|
return Check(result, "xrBeginFrame");
|
|
}
|
|
m_frame_phase = FramePhase::Begun;
|
|
diagnostics::OnBeginFrame();
|
|
return true;
|
|
}
|
|
|
|
bool OpenXRRuntime::LocateViews(OpenXRFrame& frame) {
|
|
ClearError();
|
|
if (!IsFrameTokenCurrent(frame, FramePhase::Begun)) {
|
|
return Fail(XR_ERROR_CALL_ORDER_INVALID, "xrLocateViews",
|
|
"frame token is stale or xrBeginFrame was not called");
|
|
}
|
|
return LocateViewsForFrame(frame);
|
|
}
|
|
|
|
bool OpenXRRuntime::LocateViewsAt(XrTime display_time, OpenXRFrame& frame) {
|
|
ClearError();
|
|
if (!HasSession() || !m_session_running) {
|
|
return Fail(XR_ERROR_SESSION_NOT_RUNNING, "xrLocateViews", "the session is not running");
|
|
}
|
|
frame.predicted_display_time = display_time;
|
|
return LocateViewsForFrame(frame);
|
|
}
|
|
|
|
bool OpenXRRuntime::LocateViewsForFrame(OpenXRFrame& frame) {
|
|
frame.views_valid = false;
|
|
frame.view_state_flags = 0;
|
|
if (!frame.should_render) {
|
|
return true;
|
|
}
|
|
|
|
for (XrView& view : frame.views) {
|
|
view = {XR_TYPE_VIEW};
|
|
}
|
|
XrViewLocateInfo locate_info{XR_TYPE_VIEW_LOCATE_INFO};
|
|
locate_info.viewConfigurationType = m_config.view_configuration;
|
|
locate_info.displayTime = frame.predicted_display_time;
|
|
locate_info.space = m_app_space;
|
|
XrViewState view_state{XR_TYPE_VIEW_STATE};
|
|
uint32_t view_count = 0;
|
|
const XrResult locate_result = diagnostics::Measure(diagnostics::Stage::LocateViews, [&] {
|
|
return xrLocateViews(m_session, &locate_info, &view_state,
|
|
kOpenXREyeCount, &view_count, frame.views.data());
|
|
});
|
|
if (!Check(locate_result, "xrLocateViews")) {
|
|
return false;
|
|
}
|
|
if (view_count != kOpenXREyeCount) {
|
|
return Fail(XR_ERROR_RUNTIME_FAILURE, "xrLocateViews",
|
|
"runtime returned an unexpected stereo view count");
|
|
}
|
|
|
|
frame.view_state_flags = view_state.viewStateFlags;
|
|
frame.views_valid =
|
|
(view_state.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) != 0;
|
|
return true;
|
|
}
|
|
|
|
bool OpenXRRuntime::EndFrame(
|
|
const OpenXRFrame& frame,
|
|
const XrCompositionLayerBaseHeader* const* layers,
|
|
uint32_t layer_count) {
|
|
ClearError();
|
|
if (!IsFrameTokenCurrent(frame, FramePhase::Begun)) {
|
|
return Fail(XR_ERROR_CALL_ORDER_INVALID, "xrEndFrame",
|
|
"frame token is stale or xrBeginFrame was not called");
|
|
}
|
|
if (layer_count != 0 && layers == nullptr) {
|
|
return Fail(XR_ERROR_VALIDATION_FAILURE, "xrEndFrame",
|
|
"non-zero layer_count requires a layer array");
|
|
}
|
|
|
|
// The runtime explicitly requested no application rendering. Ending with an
|
|
// empty layer list preserves the frame protocol without presenting stale work.
|
|
if (!frame.should_render) {
|
|
layers = nullptr;
|
|
layer_count = 0;
|
|
}
|
|
|
|
XrFrameEndInfo end_info{XR_TYPE_FRAME_END_INFO};
|
|
end_info.displayTime = frame.predicted_display_time;
|
|
end_info.environmentBlendMode = m_blend_mode;
|
|
end_info.layerCount = layer_count;
|
|
end_info.layers = layers;
|
|
const diagnostics::Stopwatch end_timer;
|
|
const XrResult result = xrEndFrame(m_session, &end_info);
|
|
diagnostics::OnEndFrame(end_timer);
|
|
m_frame_phase = FramePhase::Idle;
|
|
m_active_frame_serial = 0;
|
|
m_active_frame_display_time = 0;
|
|
return Check(result, "xrEndFrame");
|
|
}
|
|
|
|
bool OpenXRRuntime::EndFrame(
|
|
const OpenXRFrame& frame,
|
|
const std::vector<const XrCompositionLayerBaseHeader*>& layers) {
|
|
return EndFrame(frame, layers.data(), static_cast<uint32_t>(layers.size()));
|
|
}
|
|
|
|
bool OpenXRRuntime::EndFrameWithoutLayers(const OpenXRFrame& frame) {
|
|
return EndFrame(frame, nullptr, 0);
|
|
}
|
|
|
|
bool OpenXRRuntime::ResetAppSpace(const XrPosef& pose_in_reference_space) {
|
|
ClearError();
|
|
if (!HasSession()) {
|
|
return Fail(XR_ERROR_CALL_ORDER_INVALID, "ResetAppSpace",
|
|
"no OpenXR session exists");
|
|
}
|
|
if (m_frame_phase != FramePhase::Idle) {
|
|
return Fail(XR_ERROR_CALL_ORDER_INVALID, "ResetAppSpace",
|
|
"reference space cannot change during a frame");
|
|
}
|
|
|
|
XrReferenceSpaceCreateInfo create_info{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
|
create_info.referenceSpaceType = m_app_space_type;
|
|
create_info.poseInReferenceSpace = pose_in_reference_space;
|
|
XrSpace replacement = XR_NULL_HANDLE;
|
|
if (!Check(xrCreateReferenceSpace(m_session, &create_info, &replacement),
|
|
"xrCreateReferenceSpace(recenter)")) {
|
|
return false;
|
|
}
|
|
if (m_app_space != XR_NULL_HANDLE) {
|
|
xrDestroySpace(m_app_space);
|
|
}
|
|
m_app_space = replacement;
|
|
if (++m_reference_space_change.serial == 0) {
|
|
++m_reference_space_change.serial;
|
|
}
|
|
m_reference_space_change.type = m_app_space_type;
|
|
m_reference_space_change.change_time = 0;
|
|
m_reference_space_change.pose_in_previous_space_valid = false;
|
|
m_reference_space_change.pose_in_previous_space = IdentityPose();
|
|
m_pending_app_space_changes.push_back(m_reference_space_change);
|
|
return true;
|
|
}
|
|
|
|
bool OpenXRRuntime::ConsumeAppSpaceChangesThrough(XrTime display_time) {
|
|
bool consumed = false;
|
|
std::erase_if(m_pending_app_space_changes,
|
|
[&](const OpenXRReferenceSpaceChange& change) {
|
|
const bool due = change.change_time == 0 ||
|
|
display_time >= change.change_time;
|
|
consumed = consumed || due;
|
|
return due;
|
|
});
|
|
return consumed;
|
|
}
|
|
|
|
void OpenXRRuntime::DestroySession() {
|
|
if (!HasSession()) {
|
|
ResetSessionState();
|
|
return;
|
|
}
|
|
|
|
if (m_frame_phase == FramePhase::Begun) {
|
|
Log(OpenXRLogLevel::Warning,
|
|
"ending an active OpenXR frame without layers during teardown");
|
|
XrFrameEndInfo end_info{XR_TYPE_FRAME_END_INFO};
|
|
end_info.displayTime = m_active_frame_display_time;
|
|
end_info.environmentBlendMode = m_blend_mode;
|
|
const XrResult end_result = xrEndFrame(m_session, &end_info);
|
|
if (XR_FAILED(end_result)) {
|
|
Log(OpenXRLogLevel::Warning,
|
|
"xrEndFrame failed during session teardown");
|
|
}
|
|
m_frame_phase = FramePhase::Idle;
|
|
m_active_frame_serial = 0;
|
|
m_active_frame_display_time = 0;
|
|
}
|
|
|
|
m_shutting_down_session = true;
|
|
if (m_session_running) {
|
|
const XrResult request_result = xrRequestExitSession(m_session);
|
|
if (XR_FAILED(request_result)) {
|
|
Log(OpenXRLogLevel::Warning,
|
|
"xrRequestExitSession failed during bounded teardown");
|
|
} else {
|
|
const auto timeout =
|
|
std::chrono::milliseconds(m_config.shutdown_timeout_ms);
|
|
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
|
bool event_error = false;
|
|
while (m_session_running &&
|
|
std::chrono::steady_clock::now() < deadline) {
|
|
const OpenXREventStatus status = PollEvents();
|
|
if (status != OpenXREventStatus::Continue) {
|
|
event_error = status == OpenXREventStatus::Error;
|
|
break;
|
|
}
|
|
if (m_session_running) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
}
|
|
if (m_session_running) {
|
|
Log(OpenXRLogLevel::Warning, event_error
|
|
? "OpenXR event processing failed during session teardown"
|
|
: "OpenXR runtime did not finish session exit before timeout");
|
|
}
|
|
}
|
|
}
|
|
|
|
DestroyReferenceSpaces();
|
|
const XrResult result = xrDestroySession(m_session);
|
|
if (XR_FAILED(result)) {
|
|
Log(OpenXRLogLevel::Warning, "xrDestroySession failed");
|
|
}
|
|
m_session = XR_NULL_HANDLE;
|
|
ResetSessionState();
|
|
}
|
|
|
|
void OpenXRRuntime::Shutdown() {
|
|
DestroySession();
|
|
if (m_instance != XR_NULL_HANDLE) {
|
|
const XrResult result = xrDestroyInstance(m_instance);
|
|
if (XR_FAILED(result)) {
|
|
Log(OpenXRLogLevel::Warning, "xrDestroyInstance failed");
|
|
}
|
|
}
|
|
ResetInstanceState();
|
|
}
|
|
|
|
void OpenXRRuntime::DestroyReferenceSpaces() {
|
|
if (m_view_space != XR_NULL_HANDLE) {
|
|
xrDestroySpace(m_view_space);
|
|
m_view_space = XR_NULL_HANDLE;
|
|
}
|
|
if (m_app_space != XR_NULL_HANDLE) {
|
|
xrDestroySpace(m_app_space);
|
|
m_app_space = XR_NULL_HANDLE;
|
|
}
|
|
}
|
|
|
|
void OpenXRRuntime::ResetSessionState() {
|
|
m_session_state = XR_SESSION_STATE_UNKNOWN;
|
|
m_app_space_type = m_config.reference_space;
|
|
m_session_running = false;
|
|
m_exit_requested = false;
|
|
m_shutting_down_session = false;
|
|
ResetFrameState();
|
|
m_supported_reference_spaces.clear();
|
|
m_swapchain_formats.clear();
|
|
m_reference_space_change = {};
|
|
m_pending_app_space_changes.clear();
|
|
}
|
|
|
|
void OpenXRRuntime::ResetInstanceState() {
|
|
m_instance = XR_NULL_HANDLE;
|
|
m_system_id = XR_NULL_SYSTEM_ID;
|
|
m_session_loss_pending = false;
|
|
m_instance_loss_pending = false;
|
|
m_session_run_serial = 0;
|
|
m_runtime_info = {};
|
|
m_view_configuration = {};
|
|
m_available_extensions.clear();
|
|
m_available_api_layers.clear();
|
|
m_enabled_extensions.clear();
|
|
m_enabled_api_layers.clear();
|
|
m_supported_blend_modes.clear();
|
|
ResetSessionState();
|
|
}
|
|
|
|
bool OpenXRRuntime::IsFrameTokenCurrent(
|
|
const OpenXRFrame& frame, FramePhase expected) const {
|
|
return m_frame_phase == expected && frame.serial != 0 &&
|
|
frame.serial == m_active_frame_serial;
|
|
}
|
|
|
|
void OpenXRRuntime::ResetFrameState() {
|
|
m_frame_phase = FramePhase::Idle;
|
|
m_active_frame_serial = 0;
|
|
m_active_frame_display_time = 0;
|
|
}
|
|
|
|
bool OpenXRRuntime::Check(XrResult result, std::string_view operation) {
|
|
ObserveResult(result);
|
|
if (XR_SUCCEEDED(result)) {
|
|
return true;
|
|
}
|
|
return Fail(result, operation, {});
|
|
}
|
|
|
|
bool OpenXRRuntime::Fail(
|
|
XrResult result, std::string_view operation, std::string_view detail) {
|
|
m_last_error.result = result;
|
|
m_last_error.operation.assign(operation);
|
|
std::ostringstream message;
|
|
message << operation << " failed: " << ResultString(result);
|
|
if (!detail.empty()) {
|
|
message << " (" << detail << ')';
|
|
}
|
|
m_last_error.message = message.str();
|
|
Log(OpenXRLogLevel::Error, m_last_error.message);
|
|
return false;
|
|
}
|
|
|
|
void OpenXRRuntime::ClearError() {
|
|
m_last_error = {};
|
|
}
|
|
|
|
void OpenXRRuntime::Log(
|
|
OpenXRLogLevel level, std::string_view message) const noexcept {
|
|
if (!m_logger) {
|
|
return;
|
|
}
|
|
try {
|
|
m_logger(level, message);
|
|
} catch (...) {
|
|
// Diagnostic callbacks must never make XR teardown throw.
|
|
}
|
|
}
|
|
|
|
std::string OpenXRRuntime::ResultString(XrResult result) const {
|
|
if (m_instance != XR_NULL_HANDLE) {
|
|
char text[XR_MAX_RESULT_STRING_SIZE]{};
|
|
if (XR_SUCCEEDED(xrResultToString(m_instance, result, text))) {
|
|
return text;
|
|
}
|
|
}
|
|
return std::to_string(static_cast<int32_t>(result));
|
|
}
|
|
|
|
} // namespace mkw::vr
|
|
|
|
#endif // MKW_ENABLE_OPENXR
|