Added Android Meta Quest Support

This commit is contained in:
iChris4 committed 2026-09-16 22:06:34 +02:00
1 parent 49b8602543
commit e52227baf5
67 files changed
+5662 -176

No files matched your search

+171
View File
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#if defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)
#include "vr/openxr_android.h"
#include <SDL3/SDL_system.h>
#include <jni.h>
#include <sys/syscall.h>
#include <unistd.h>
#define XR_USE_PLATFORM_ANDROID
#include <openxr/openxr_platform.h>
#include <mutex>
#include <sstream>
namespace mkw::vr {
namespace {
std::mutex g_mutex;
JavaVM* g_vm = nullptr;
jobject g_activity = nullptr; // global reference, never released: it lives as long as the process
bool g_loader_initialized = false;
XrInstanceCreateInfoAndroidKHR g_instance_create{XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR};
void Emit(const OpenXRLogCallback& logger, OpenXRLogLevel level, const std::string& message) noexcept {
if (!logger) {
return;
}
try {
logger(level, message);
} catch (...) {
}
}
bool ResolveAndroidObjectsLocked(std::string* error) {
if (g_vm != nullptr && g_activity != nullptr) {
return true;
}
// SDL attaches the calling thread to the VM on demand and hands back the
// activity as a local reference the caller owns.
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
auto activity = static_cast<jobject>(SDL_GetAndroidActivity());
if (env == nullptr || activity == nullptr) {
if (error != nullptr) {
*error = "SDL has not published the Android activity yet";
}
return false;
}
JavaVM* vm = nullptr;
if (env->GetJavaVM(&vm) != JNI_OK || vm == nullptr) {
env->DeleteLocalRef(activity);
if (error != nullptr) {
*error = "JNIEnv::GetJavaVM failed";
}
return false;
}
g_vm = vm;
g_activity = env->NewGlobalRef(activity);
env->DeleteLocalRef(activity);
if (g_activity == nullptr) {
if (error != nullptr) {
*error = "could not pin the Android activity";
}
return false;
}
return true;
}
} // namespace
bool OpenXRAndroidInitializeLoader(OpenXRLogCallback logger, std::string* error) {
std::lock_guard lock(g_mutex);
if (g_loader_initialized) {
return true;
}
if (!ResolveAndroidObjectsLocked(error)) {
return false;
}
PFN_xrInitializeLoaderKHR initialize_loader = nullptr;
XrResult result = xrGetInstanceProcAddr(
XR_NULL_HANDLE, "xrInitializeLoaderKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&initialize_loader));
if (XR_FAILED(result) || initialize_loader == nullptr) {
if (error != nullptr) {
std::ostringstream message;
message << "xrInitializeLoaderKHR is unavailable (" << static_cast<int>(result) << ')';
*error = message.str();
}
return false;
}
// The activity is deliberately used as the loader context (an Activity is
// a Context). Meta's runtime keys activity readiness off it; with a plain
// application context the session parks in IDLE forever (DolphinXR finding).
XrLoaderInitInfoAndroidKHR loader_init{XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR};
loader_init.applicationVM = g_vm;
loader_init.applicationContext = g_activity;
result = initialize_loader(reinterpret_cast<const XrLoaderInitInfoBaseHeaderKHR*>(&loader_init));
if (XR_FAILED(result)) {
if (error != nullptr) {
std::ostringstream message;
message << "xrInitializeLoaderKHR failed (" << static_cast<int>(result) << ')';
*error = message.str();
}
return false;
}
g_instance_create.next = nullptr;
g_instance_create.applicationVM = g_vm;
g_instance_create.applicationActivity = g_activity;
g_loader_initialized = true;
Emit(logger, OpenXRLogLevel::Info, "OpenXR Android loader initialized against the SDL activity");
return true;
}
const void* OpenXRAndroidInstanceCreateNext() {
std::lock_guard lock(g_mutex);
return g_loader_initialized ? &g_instance_create : nullptr;
}
bool OpenXRAndroidRegisterThread(OpenXRRuntime& runtime, OpenXRAndroidThreadType type) {
if (!runtime.HasSession()) {
return false;
}
const auto& extensions = runtime.EnabledExtensions();
bool enabled = false;
for (const auto& extension : extensions) {
if (extension == XR_KHR_ANDROID_THREAD_SETTINGS_EXTENSION_NAME) {
enabled = true;
break;
}
}
if (!enabled) {
return false;
}
PFN_xrSetAndroidApplicationThreadKHR set_thread = nullptr;
if (!runtime.LoadFunction("xrSetAndroidApplicationThreadKHR", &set_thread) || set_thread == nullptr) {
return false;
}
XrAndroidThreadTypeKHR xr_type = XR_ANDROID_THREAD_TYPE_APPLICATION_WORKER_KHR;
switch (type) {
case OpenXRAndroidThreadType::ApplicationMain:
xr_type = XR_ANDROID_THREAD_TYPE_APPLICATION_MAIN_KHR;
break;
case OpenXRAndroidThreadType::ApplicationWorker:
xr_type = XR_ANDROID_THREAD_TYPE_APPLICATION_WORKER_KHR;
break;
case OpenXRAndroidThreadType::RendererMain:
xr_type = XR_ANDROID_THREAD_TYPE_RENDERER_MAIN_KHR;
break;
case OpenXRAndroidThreadType::RendererWorker:
xr_type = XR_ANDROID_THREAD_TYPE_RENDERER_WORKER_KHR;
break;
}
const auto thread_id = static_cast<uint32_t>(syscall(SYS_gettid));
XrResult result = set_thread(runtime.Session(), xr_type, thread_id);
if (XR_FAILED(result) && type == OpenXRAndroidThreadType::RendererWorker) {
// Some Quest runtime builds advertise the extension but reject the
// renderer-worker type; the renderer-main hint still raises priority.
result = set_thread(runtime.Session(), XR_ANDROID_THREAD_TYPE_RENDERER_MAIN_KHR, thread_id);
}
runtime.ObserveResult(result);
return XR_SUCCEEDED(result);
}
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)
+450
View File
@@ -0,0 +1,450 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#if defined(MKW_ENABLE_OPENXR)
#include "vr/openxr_input.h"
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_joystick.h>
#include <SDL3/SDL_stdinc.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <sstream>
#include <vector>
#if defined(__ANDROID__)
#include <sys/system_properties.h>
#endif
namespace mkw::vr {
namespace {
#if defined(__ANDROID__)
// Debug-only remote button presses for headset experiments driven over adb, so
// a menu can be reached without someone wearing the headset:
// adb shell setprop debug.wiicompiled.inject <sequence>:<button>
// A new sequence number holds the button for kInjectHoldFrames XR frames.
// Buttons: a, b, x, y, start, up, down, left, right. The property is unset in
// normal use, so this costs one property read every few frames.
constexpr uint32_t kInjectHoldFrames = 12;
constexpr uint32_t kInjectPollFrames = 4;
struct InjectedPress {
long sequence = -1;
std::string button;
uint32_t frames_left = 0;
uint32_t poll_countdown = 0;
};
InjectedPress& Injection() {
static InjectedPress press;
return press;
}
void PollInjection() {
InjectedPress& press = Injection();
if (press.frames_left > 0) {
--press.frames_left;
}
if (press.poll_countdown > 0) {
--press.poll_countdown;
return;
}
press.poll_countdown = kInjectPollFrames;
char value[PROP_VALUE_MAX]{};
if (__system_property_get("debug.wiicompiled.inject", value) <= 0) {
return;
}
char* end = nullptr;
const long sequence = std::strtol(value, &end, 10);
if (end == value || *end != ':' || sequence == press.sequence) {
return;
}
const bool first_read = press.sequence < 0;
press.sequence = sequence;
if (first_read) {
return; // A value left over from an earlier run is not a new press.
}
press.button = end + 1;
press.frames_left = kInjectHoldFrames;
}
bool Injected(const char* button) {
const InjectedPress& press = Injection();
return press.frames_left > 0 && press.button == button;
}
#else
void PollInjection() {}
bool Injected(const char*) { return false; }
#endif
constexpr uint32_t kHandCount = 2;
struct Binding {
XrAction* action;
const char* path;
};
Sint16 ToAxis(float value) noexcept {
const float clamped = std::clamp(value, -1.0f, 1.0f);
return static_cast<Sint16>(std::lround(clamped * 32767.0f));
}
// Trigger axes are reported by SDL gamepads on the positive half only.
Sint16 ToTrigger(float value) noexcept {
const float clamped = std::clamp(value, 0.0f, 1.0f);
return static_cast<Sint16>(std::lround(clamped * 32767.0f));
}
} // namespace
OpenXRInput::OpenXRInput(OpenXRLogCallback logger) : m_logger(std::move(logger)) {}
OpenXRInput::~OpenXRInput() {
Destroy();
}
bool OpenXRInput::Create(OpenXRRuntime& runtime) {
m_last_error.clear();
if (m_created) {
return true;
}
if (!runtime.IsInitialized() || !runtime.HasSession()) {
m_last_error = "OpenXR input needs an initialized runtime with a session";
return false;
}
m_runtime = &runtime;
if (!Check(xrStringToPath(runtime.Instance(), "/user/hand/left", &m_hand_paths[0]),
"xrStringToPath(/user/hand/left)") ||
!Check(xrStringToPath(runtime.Instance(), "/user/hand/right", &m_hand_paths[1]),
"xrStringToPath(/user/hand/right)")) {
Destroy();
return false;
}
if (!CreateActions() || !SuggestBindings()) {
Destroy();
return false;
}
XrSessionActionSetsAttachInfo attach{XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
attach.countActionSets = 1;
attach.actionSets = &m_action_set;
if (!Check(xrAttachSessionActionSets(runtime.Session(), &attach), "xrAttachSessionActionSets")) {
Destroy();
return false;
}
m_created = true;
if (!AttachVirtualGamepad()) {
Log(OpenXRLogLevel::Warning,
"SDL refused the virtual gamepad; OpenXR controllers will not reach the game");
}
Log(OpenXRLogLevel::Info, "OpenXR controller actions attached");
return true;
}
bool OpenXRInput::CreateActions() {
XrActionSetCreateInfo set_info{XR_TYPE_ACTION_SET_CREATE_INFO};
std::strncpy(set_info.actionSetName, "mkw_gameplay", XR_MAX_ACTION_SET_NAME_SIZE - 1);
std::strncpy(set_info.localizedActionSetName, "Gameplay", XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE - 1);
set_info.priority = 0;
if (!Check(xrCreateActionSet(m_runtime->Instance(), &set_info, &m_action_set), "xrCreateActionSet")) {
return false;
}
struct Spec {
XrAction* action;
const char* name;
const char* localized;
XrActionType type;
};
const std::array<Spec, 8> specs{{
{&m_thumbstick, "thumbstick", "Thumbstick", XR_ACTION_TYPE_VECTOR2F_INPUT},
{&m_thumbstick_click, "thumbstick_click", "Thumbstick Click", XR_ACTION_TYPE_BOOLEAN_INPUT},
{&m_trigger, "trigger", "Trigger", XR_ACTION_TYPE_FLOAT_INPUT},
{&m_squeeze, "squeeze", "Grip", XR_ACTION_TYPE_FLOAT_INPUT},
{&m_button_primary, "button_primary", "A / X", XR_ACTION_TYPE_BOOLEAN_INPUT},
{&m_button_secondary, "button_secondary", "B / Y", XR_ACTION_TYPE_BOOLEAN_INPUT},
{&m_menu, "menu", "Menu", XR_ACTION_TYPE_BOOLEAN_INPUT},
{&m_haptic, "haptic", "Haptic", XR_ACTION_TYPE_VIBRATION_OUTPUT},
}};
for (const Spec& spec : specs) {
XrActionCreateInfo info{XR_TYPE_ACTION_CREATE_INFO};
info.actionType = spec.type;
std::strncpy(info.actionName, spec.name, XR_MAX_ACTION_NAME_SIZE - 1);
std::strncpy(info.localizedActionName, spec.localized, XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
info.countSubactionPaths = kHandCount;
info.subactionPaths = m_hand_paths;
if (!Check(xrCreateAction(m_action_set, &info, spec.action), spec.name)) {
return false;
}
}
return true;
}
bool OpenXRInput::SuggestBindings() {
const auto suggest = [&](const char* profile, const std::vector<Binding>& bindings, bool required) {
XrPath profile_path = XR_NULL_PATH;
if (!Check(xrStringToPath(m_runtime->Instance(), profile, &profile_path), profile)) {
return false;
}
std::vector<XrActionSuggestedBinding> suggested;
suggested.reserve(bindings.size());
for (const Binding& binding : bindings) {
XrPath path = XR_NULL_PATH;
if (XR_FAILED(xrStringToPath(m_runtime->Instance(), binding.path, &path))) {
continue;
}
suggested.push_back({*binding.action, path});
}
XrInteractionProfileSuggestedBinding info{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
info.interactionProfile = profile_path;
info.countSuggestedBindings = static_cast<uint32_t>(suggested.size());
info.suggestedBindings = suggested.data();
const XrResult result = xrSuggestInteractionProfileBindings(m_runtime->Instance(), &info);
m_runtime->ObserveResult(result);
if (XR_FAILED(result)) {
std::ostringstream message;
message << "xrSuggestInteractionProfileBindings(" << profile << ") failed (" << result << ')';
if (required) {
m_last_error = message.str();
Log(OpenXRLogLevel::Error, m_last_error);
return false;
}
Log(OpenXRLogLevel::Warning, message.str());
}
return true;
};
// Meta Quest Touch controllers (Quest 2 / 3 / Pro all expose this profile).
const std::vector<Binding> touch{
{&m_thumbstick, "/user/hand/left/input/thumbstick"},
{&m_thumbstick, "/user/hand/right/input/thumbstick"},
{&m_thumbstick_click, "/user/hand/left/input/thumbstick/click"},
{&m_thumbstick_click, "/user/hand/right/input/thumbstick/click"},
{&m_trigger, "/user/hand/left/input/trigger/value"},
{&m_trigger, "/user/hand/right/input/trigger/value"},
{&m_squeeze, "/user/hand/left/input/squeeze/value"},
{&m_squeeze, "/user/hand/right/input/squeeze/value"},
{&m_button_primary, "/user/hand/left/input/x/click"},
{&m_button_primary, "/user/hand/right/input/a/click"},
{&m_button_secondary, "/user/hand/left/input/y/click"},
{&m_button_secondary, "/user/hand/right/input/b/click"},
{&m_menu, "/user/hand/left/input/menu/click"},
{&m_haptic, "/user/hand/left/output/haptic"},
{&m_haptic, "/user/hand/right/output/haptic"},
};
if (!suggest("/interaction_profiles/oculus/touch_controller", touch, true)) {
return false;
}
// Minimal fallback so an unfamiliar runtime still offers a select and a menu.
const std::vector<Binding> simple{
{&m_button_primary, "/user/hand/right/input/select/click"},
{&m_button_secondary, "/user/hand/left/input/select/click"},
{&m_menu, "/user/hand/left/input/menu/click"},
{&m_haptic, "/user/hand/left/output/haptic"},
{&m_haptic, "/user/hand/right/output/haptic"},
};
suggest("/interaction_profiles/khr/simple_controller", simple, false);
return true;
}
bool OpenXRInput::AttachVirtualGamepad() {
SDL_VirtualJoystickDesc desc;
SDL_INIT_INTERFACE(&desc);
desc.type = SDL_JOYSTICK_TYPE_GAMEPAD;
desc.naxes = SDL_GAMEPAD_AXIS_COUNT;
desc.nbuttons = SDL_GAMEPAD_BUTTON_COUNT;
desc.button_mask = (1u << SDL_GAMEPAD_BUTTON_SOUTH) | (1u << SDL_GAMEPAD_BUTTON_EAST) |
(1u << SDL_GAMEPAD_BUTTON_WEST) | (1u << SDL_GAMEPAD_BUTTON_NORTH) |
(1u << SDL_GAMEPAD_BUTTON_START) | (1u << SDL_GAMEPAD_BUTTON_LEFT_STICK) |
(1u << SDL_GAMEPAD_BUTTON_RIGHT_STICK) |
(1u << SDL_GAMEPAD_BUTTON_LEFT_SHOULDER) |
(1u << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER);
desc.axis_mask = (1u << SDL_GAMEPAD_AXIS_LEFTX) | (1u << SDL_GAMEPAD_AXIS_LEFTY) |
(1u << SDL_GAMEPAD_AXIS_RIGHTX) | (1u << SDL_GAMEPAD_AXIS_RIGHTY) |
(1u << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1u << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER);
desc.name = "OpenXR Touch Controllers";
const SDL_JoystickID id = SDL_AttachVirtualJoystick(&desc);
if (id == 0) {
m_last_error = std::string("SDL_AttachVirtualJoystick failed: ") + SDL_GetError();
Log(OpenXRLogLevel::Warning, m_last_error);
return false;
}
SDL_Joystick* joystick = SDL_OpenJoystick(id);
if (joystick == nullptr) {
m_last_error = std::string("SDL_OpenJoystick failed: ") + SDL_GetError();
Log(OpenXRLogLevel::Warning, m_last_error);
SDL_DetachVirtualJoystick(id);
return false;
}
m_joystick_id = id;
m_joystick = joystick;
return true;
}
void OpenXRInput::DetachVirtualGamepad() {
if (m_joystick != nullptr) {
SDL_CloseJoystick(static_cast<SDL_Joystick*>(m_joystick));
m_joystick = nullptr;
}
if (m_joystick_id != 0) {
SDL_DetachVirtualJoystick(m_joystick_id);
m_joystick_id = 0;
}
}
void OpenXRInput::Destroy() {
DetachVirtualGamepad();
if (m_action_set != XR_NULL_HANDLE) {
// Destroying the set destroys every action created from it.
xrDestroyActionSet(m_action_set);
m_action_set = XR_NULL_HANDLE;
}
m_thumbstick = m_thumbstick_click = m_trigger = m_squeeze = XR_NULL_HANDLE;
m_button_primary = m_button_secondary = m_menu = m_haptic = XR_NULL_HANDLE;
m_hand_paths[0] = m_hand_paths[1] = XR_NULL_PATH;
m_created = false;
m_runtime = nullptr;
}
void OpenXRInput::Sync(XrTime) {
if (!m_created || m_runtime == nullptr || !m_runtime->IsSessionFocused()) {
return;
}
XrActiveActionSet active{m_action_set, XR_NULL_PATH};
XrActionsSyncInfo sync{XR_TYPE_ACTIONS_SYNC_INFO};
sync.countActiveActionSets = 1;
sync.activeActionSets = &active;
const XrResult result = xrSyncActions(m_runtime->Session(), &sync);
m_runtime->ObserveResult(result);
if (XR_FAILED(result)) {
if (!m_logged_sync_failure) {
m_logged_sync_failure = true;
std::ostringstream message;
message << "xrSyncActions failed (" << result << ')';
Log(OpenXRLogLevel::Warning, message.str());
}
return;
}
if (m_joystick == nullptr) {
return;
}
auto* joystick = static_cast<SDL_Joystick*>(m_joystick);
const auto boolean = [&](XrAction action, uint32_t hand) {
XrActionStateGetInfo info{XR_TYPE_ACTION_STATE_GET_INFO};
info.action = action;
info.subactionPath = m_hand_paths[hand];
XrActionStateBoolean state{XR_TYPE_ACTION_STATE_BOOLEAN};
return XR_SUCCEEDED(xrGetActionStateBoolean(m_runtime->Session(), &info, &state)) &&
state.isActive == XR_TRUE && state.currentState == XR_TRUE;
};
const auto scalar = [&](XrAction action, uint32_t hand) {
XrActionStateGetInfo info{XR_TYPE_ACTION_STATE_GET_INFO};
info.action = action;
info.subactionPath = m_hand_paths[hand];
XrActionStateFloat state{XR_TYPE_ACTION_STATE_FLOAT};
if (XR_FAILED(xrGetActionStateFloat(m_runtime->Session(), &info, &state)) ||
state.isActive != XR_TRUE) {
return 0.0f;
}
return state.currentState;
};
const auto vector = [&](XrAction action, uint32_t hand) {
XrActionStateGetInfo info{XR_TYPE_ACTION_STATE_GET_INFO};
info.action = action;
info.subactionPath = m_hand_paths[hand];
XrActionStateVector2f state{XR_TYPE_ACTION_STATE_VECTOR2F};
if (XR_FAILED(xrGetActionStateVector2f(m_runtime->Session(), &info, &state)) ||
state.isActive != XR_TRUE) {
return XrVector2f{0.0f, 0.0f};
}
return state.currentState;
};
PollInjection();
XrVector2f left = vector(m_thumbstick, 0);
const XrVector2f right = vector(m_thumbstick, 1);
if (Injected("up")) {
left.y = 1.0f;
} else if (Injected("down")) {
left.y = -1.0f;
} else if (Injected("left")) {
left.x = -1.0f;
} else if (Injected("right")) {
left.x = 1.0f;
}
// OpenXR thumbsticks report +Y up; SDL gamepads report +Y down.
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFTX, ToAxis(left.x));
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFTY, ToAxis(-left.y));
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHTX, ToAxis(right.x));
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHTY, ToAxis(-right.y));
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, ToTrigger(scalar(m_trigger, 0)));
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, ToTrigger(scalar(m_trigger, 1)));
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_SOUTH,
boolean(m_button_primary, 1) || Injected("a"));
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_EAST,
boolean(m_button_secondary, 1) || Injected("b"));
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_WEST,
boolean(m_button_primary, 0) || Injected("x"));
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_NORTH,
boolean(m_button_secondary, 0) || Injected("y"));
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_START, boolean(m_menu, 0) || Injected("start"));
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, boolean(m_thumbstick_click, 0));
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, boolean(m_thumbstick_click, 1));
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, scalar(m_squeeze, 0) > 0.5f);
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, scalar(m_squeeze, 1) > 0.5f);
}
void OpenXRInput::ApplyHaptic(uint32_t hand, float amplitude, XrDuration duration) {
if (!m_created || m_runtime == nullptr || hand >= kHandCount || m_haptic == XR_NULL_HANDLE) {
return;
}
XrHapticVibration vibration{XR_TYPE_HAPTIC_VIBRATION};
vibration.amplitude = std::clamp(amplitude, 0.0f, 1.0f);
vibration.duration = duration;
vibration.frequency = XR_FREQUENCY_UNSPECIFIED;
XrHapticActionInfo info{XR_TYPE_HAPTIC_ACTION_INFO};
info.action = m_haptic;
info.subactionPath = m_hand_paths[hand];
if (vibration.amplitude <= 0.0f) {
xrStopHapticFeedback(m_runtime->Session(), &info);
return;
}
xrApplyHapticFeedback(m_runtime->Session(), &info,
reinterpret_cast<const XrHapticBaseHeader*>(&vibration));
}
bool OpenXRInput::Check(XrResult result, const char* operation) {
if (m_runtime != nullptr) {
m_runtime->ObserveResult(result);
}
if (XR_SUCCEEDED(result)) {
return true;
}
std::ostringstream message;
message << operation << " failed (" << result << ')';
m_last_error = message.str();
Log(OpenXRLogLevel::Error, m_last_error);
return false;
}
void OpenXRInput::Log(OpenXRLogLevel level, const std::string& message) const noexcept {
if (!m_logger) {
return;
}
try {
m_logger(level, message);
} catch (...) {
}
}
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR)
+184 -63
View File
@@ -27,12 +27,24 @@
#include <thread>
#if defined(MKW_ENABLE_OPENXR)
#include "vr/openxr_backend.h"
#include "vr/openxr_input.h"
#include "vr/openxr_runtime.h"
#if defined(_WIN32)
#include "vr/openxr_d3d12.h"
#define MKW_OPENXR_GRAPHICS_BACKEND 1
#elif defined(__ANDROID__)
#include "vr/openxr_android.h"
#include "vr/openxr_vulkan.h"
#include <time.h>
#define XR_USE_TIMESPEC
#include <openxr/openxr_platform.h>
#define MKW_OPENXR_GRAPHICS_BACKEND 1
#else
#include "vr/openxr_vulkan_backend.h"
#define MKW_OPENXR_GRAPHICS_BACKEND 0
#endif
#else
#define MKW_OPENXR_GRAPHICS_BACKEND 0
#endif
namespace mkw::vr {
@@ -40,6 +52,14 @@ namespace {
inline constexpr float kDegreesToRadians = 0.01745329252f;
// A standalone headset has no desktop to fall back to, so VR is on unless the
// user's configuration turns it off. Desktop builds keep the opt-in default.
#if defined(__ANDROID__)
inline constexpr bool kVrEnabledDefault = true;
#else
inline constexpr bool kVrEnabledDefault = false;
#endif
void ConfigurePolicy(bool enabled) noexcept {
MkwVRPolicyReset();
MkwVRPolicyConfig config{};
@@ -54,7 +74,15 @@ void ConfigurePolicy(bool enabled) noexcept {
MkwVRFirstPersonApplyConfiguredSettings();
}
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#if MKW_OPENXR_GRAPHICS_BACKEND
#if defined(_WIN32)
using GraphicsBackend = OpenXRD3D12Backend;
inline constexpr const char* kGraphicsBackendName = "D3D12";
#else
using GraphicsBackend = OpenXRVulkanBackend;
inline constexpr const char* kGraphicsBackendName = "Vulkan";
#endif
struct Quaternion {
float x = 0.0f;
@@ -240,17 +268,16 @@ public:
last_error_.clear();
}
if (graphics_retained_) {
SetError("OpenXR cannot be restarted after an unfenceable D3D12 submission");
SetError(std::string("OpenXR cannot be restarted after an unfenceable ") +
kGraphicsBackendName + " submission");
return OpenXRStartupResult::Unavailable;
}
requested_ = RuntimeConfigFile::VrEnabled(false);
requested_ = RuntimeConfigFile::VrEnabled(kVrEnabledDefault);
ConfigurePolicy(requested_);
if (!requested_) {
return OpenXRStartupResult::Disabled;
}
if (aurora_config.desiredBackend != BACKEND_AUTO &&
aurora_config.desiredBackend != BACKEND_D3D12) {
SetError("OpenXR currently requires the D3D12 graphics backend on Windows");
if (!BackendMatchesConfiguredGraphicsApi(aurora_config)) {
return OpenXRStartupResult::Unavailable;
}
@@ -259,27 +286,56 @@ public:
level == OpenXRLogLevel::Warning ? "warning" : "info";
RT_LOG(RT_TAG_RUNTIME) << "[openxr::" << name << "] " << message << std::endl;
};
#if defined(__ANDROID__)
{
std::string loader_error;
if (!OpenXRAndroidInitializeLoader(logger_, &loader_error)) {
SetError("OpenXR Android loader initialization failed: " + loader_error);
return OpenXRStartupResult::Unavailable;
}
}
#endif
runtime_ = std::make_unique<OpenXRRuntime>(logger_);
backend_ = std::make_unique<OpenXRD3D12Backend>(logger_);
backend_ = std::make_unique<GraphicsBackend>(logger_);
OpenXRConfig config{};
config.application_name = aurora_config.appName != nullptr ? aurora_config.appName
: "WiiCompiled";
config.engine_name = "Aurora";
config.resolution_scale = RuntimeConfigFile::VrRenderScale(1.0f);
#if defined(_WIN32)
config.required_extensions = {"XR_KHR_D3D12_enable"};
config.optional_extensions = {"XR_KHR_win32_convert_performance_counter_time", "XR_FB_display_refresh_rate"};
config.optional_extensions = {"XR_KHR_win32_convert_performance_counter_time",
"XR_FB_display_refresh_rate"};
#else
// Either Vulkan binding extension is acceptable; the backend picks
// whichever the runtime enabled, preferring enable2.
config.required_extensions = {"XR_KHR_android_create_instance"};
config.optional_extensions = {"XR_KHR_vulkan_enable2", "XR_KHR_vulkan_enable",
"XR_KHR_convert_timespec_time",
"XR_KHR_android_thread_settings",
"XR_FB_display_refresh_rate"};
config.instance_create_next = OpenXRAndroidInstanceCreateNext();
#endif
if (!runtime_->Initialize(config)) {
SetError("OpenXR instance initialization failed: " + runtime_->LastError().message);
ResetPreparedObjects();
return OpenXRStartupResult::Unavailable;
}
const auto& extensions = runtime_->EnabledExtensions();
if (std::find(extensions.begin(), extensions.end(),
"XR_KHR_win32_convert_performance_counter_time") != extensions.end()) {
const auto has_extension = [&](const char* name) {
return std::find(extensions.begin(), extensions.end(), name) != extensions.end();
};
#if defined(_WIN32)
if (has_extension("XR_KHR_win32_convert_performance_counter_time")) {
runtime_->LoadFunction("xrConvertTimeToWin32PerformanceCounterKHR", &convert_display_time_);
}
if (std::find(extensions.begin(), extensions.end(), "XR_FB_display_refresh_rate") != extensions.end()) {
#else
if (has_extension("XR_KHR_convert_timespec_time")) {
runtime_->LoadFunction("xrConvertTimeToTimespecTimeKHR", &convert_display_time_);
}
#endif
if (has_extension("XR_FB_display_refresh_rate")) {
runtime_->LoadFunction("xrGetDisplayRefreshRateFB", &get_display_refresh_rate_);
}
interpolation_available_.store(convert_display_time_ != nullptr, std::memory_order_release);
@@ -289,12 +345,7 @@ public:
return OpenXRStartupResult::Unavailable;
}
const auto& requirements = backend_->GraphicsRequirements();
aurora_config.desiredBackend = BACKEND_D3D12;
aurora_config.xrInterop = true;
aurora_config.hasD3D12AdapterLuid = true;
aurora_config.d3d12AdapterLuidLow = requirements.adapter_luid_low;
aurora_config.d3d12AdapterLuidHigh = requirements.adapter_luid_high;
ApplyGraphicsRequirements(aurora_config);
prepared_ = true;
return OpenXRStartupResult::Prepared;
}
@@ -303,8 +354,9 @@ public:
if (!prepared_ || runtime_ == nullptr || backend_ == nullptr) {
return !requested_;
}
if (active_backend != BACKEND_D3D12) {
SetError("Aurora could not create the OpenXR-required D3D12 backend");
if (active_backend != kRequiredAuroraBackend) {
SetError(std::string("Aurora could not create the OpenXR-required ") +
kGraphicsBackendName + " backend");
ResetPreparedObjects();
return false;
}
@@ -313,6 +365,12 @@ public:
ResetPreparedObjects();
return false;
}
input_ = std::make_unique<OpenXRInput>(logger_);
if (!input_->Create(*runtime_)) {
RT_LOG(RT_TAG_RUNTIME) << "OpenXR controller input unavailable: " << input_->LastError()
<< std::endl;
input_.reset();
}
stop_.store(false, std::memory_order_release);
{
@@ -334,7 +392,8 @@ public:
ResetPreparedObjects();
return false;
}
RT_LOG(RT_TAG_RUNTIME) << "OpenXR asynchronous D3D12 presentation started" << std::endl;
RT_LOG(RT_TAG_RUNTIME) << "OpenXR asynchronous " << kGraphicsBackendName
<< " presentation started" << std::endl;
return true;
}
@@ -426,17 +485,51 @@ private:
AuroraStereoFrame frame{};
};
#if defined(_WIN32)
static constexpr AuroraBackend kRequiredAuroraBackend = BACKEND_D3D12;
#else
static constexpr AuroraBackend kRequiredAuroraBackend = BACKEND_VULKAN;
#endif
bool BackendMatchesConfiguredGraphicsApi(const AuroraConfig& aurora_config) {
if (aurora_config.desiredBackend == BACKEND_AUTO ||
aurora_config.desiredBackend == kRequiredAuroraBackend) {
return true;
}
SetError(std::string("OpenXR requires the ") + kGraphicsBackendName +
" graphics backend on this platform");
return false;
}
void ApplyGraphicsRequirements(AuroraConfig& aurora_config) {
aurora_config.desiredBackend = kRequiredAuroraBackend;
aurora_config.xrInterop = true;
#if defined(_WIN32)
const auto& requirements = backend_->GraphicsRequirements();
aurora_config.hasD3D12AdapterLuid = true;
aurora_config.d3d12AdapterLuidLow = requirements.adapter_luid_low;
aurora_config.d3d12AdapterLuidHigh = requirements.adapter_luid_high;
#endif
}
void ResetPreparedObjects() {
ShutdownOrRetainGraphicsObjects();
input_.reset();
backend_.reset();
runtime_.reset();
prepared_ = false;
}
bool ShutdownOrRetainGraphicsObjects() noexcept {
if (input_ != nullptr) {
// Actions belong to the session and must go before it does.
input_->Destroy();
input_.reset();
}
if (backend_ != nullptr && !backend_->Shutdown()) {
RT_LOG(RT_TAG_RUNTIME)
<< "OpenXR D3D12 queue completion is unknown; retaining the backend, "
<< "OpenXR " << kGraphicsBackendName
<< " queue completion is unknown; retaining the backend, "
"runtime, session, and graphics resources until process exit"
<< std::endl;
(void)backend_.release();
@@ -468,6 +561,11 @@ private:
}
void PacingThread() noexcept {
#if defined(__ANDROID__)
if (runtime_ != nullptr) {
OpenXRAndroidRegisterThread(*runtime_, OpenXRAndroidThreadType::RendererMain);
}
#endif
bool fatal = false;
bool presentation_logged = false;
VRPresentationMode logged_presentation = VRPresentationMode::Desktop;
@@ -489,7 +587,7 @@ private:
}
}
if (events == OpenXREventStatus::ExitRequested) {
SetError("OpenXR runtime requested session exit; continuing on the desktop mirror");
SetError("OpenXR runtime requested session exit; continuing on the mirror output");
break;
}
if (events == OpenXREventStatus::Error) {
@@ -525,10 +623,10 @@ private:
<< ", bindings=0x" << std::hex << policy.available_bindings
<< std::dec << std::endl;
}
OpenXRD3D12Presentation presentation{};
OpenXRPresentation presentation{};
const bool immersive = policy.presentation == VRPresentationMode::ImmersiveRace;
presentation.mode = immersive ? OpenXRD3D12FrameMode::ImmersiveProjection
: OpenXRD3D12FrameMode::VirtualScreen;
presentation.mode = immersive ? OpenXRFrameMode::ImmersiveProjection
: OpenXRFrameMode::VirtualScreen;
presentation.quad_distance_meters = policy.config.hud_distance_meters;
presentation.quad_width_meters = policy.config.hud_width_meters;
@@ -537,23 +635,26 @@ private:
const uint32_t interpolation_target = frame_interpolation_fps_.load(std::memory_order_relaxed);
SetInterpolationActive(immersive && FrameInterpolationAvailable() && interpolation_target != 0);
OpenXRD3D12Frame frame{};
const OpenXRD3D12BeginStatus begin = backend_->BeginFrame(presentation, frame);
if (begin == OpenXRD3D12BeginStatus::SessionNotRunning) {
OpenXRBackendFrame frame{};
const OpenXRBeginStatus begin = backend_->BeginFrame(presentation, frame);
if (begin == OpenXRBeginStatus::SessionNotRunning) {
MkwVRPolicySetSessionActive(false);
continue;
}
if (begin == OpenXRD3D12BeginStatus::ExitRequested) {
SetError("OpenXR runtime requested session exit; continuing on the desktop mirror");
if (begin == OpenXRBeginStatus::ExitRequested) {
SetError("OpenXR runtime requested session exit; continuing on the mirror output");
break;
}
if (begin == OpenXRD3D12BeginStatus::Error) {
if (begin == OpenXRBeginStatus::Error) {
SetError(backend_->LastError());
fatal = true;
break;
}
UpdateFrameTiming(frame.xr_frame);
if (input_ != nullptr) {
input_->Sync(frame.xr_frame.predicted_display_time);
}
// Both of these read this frame's located head pose and must run
// before FinishFrame submits a layer built from it.
ServiceRecenterRequest();
@@ -588,16 +689,16 @@ private:
}
aurora_notify_stereo_frame();
OpenXRD3D12SubmissionStatus submission = OpenXRD3D12SubmissionStatus::Timeout;
OpenXRSubmissionStatus submission = OpenXRSubmissionStatus::Timeout;
bool canceled_before_encode = false;
const auto cancel_after =
std::chrono::steady_clock::now() + std::chrono::milliseconds(50);
while (!stop_.load(std::memory_order_acquire) &&
submission == OpenXRD3D12SubmissionStatus::Timeout) {
submission == OpenXRSubmissionStatus::Timeout) {
// Fresh rendering wakes us immediately. A 50 ms keep-alive
// protects stalls without issuing eager repeats during GPU work.
submission = backend_->WaitForSubmission(frame, 50);
if (submission == OpenXRD3D12SubmissionStatus::Timeout) {
if (submission == OpenXRSubmissionStatus::Timeout) {
// A pause, minimized window, or guest stall may leave no GX
// frame to consume this packet. Withdraw it, then cancel the
// matching bridge target only if Encode has not taken ownership.
@@ -632,12 +733,13 @@ private:
if (fatal) {
break;
}
const bool submit = submission == OpenXRD3D12SubmissionStatus::Success;
const bool submit = submission == OpenXRSubmissionStatus::Success;
if (!backend_->FinishFrame(frame, submit)) {
SetError(backend_->LastError());
fatal = true;
} else if (!submit) {
SetError("Aurora's D3D12 stereo copy failed; continuing on the desktop mirror");
SetError(std::string("Aurora's ") + kGraphicsBackendName +
" stereo copy failed; continuing on the mirror output");
fatal = true;
} else {
++timing_submissions_;
@@ -665,7 +767,7 @@ private:
ShutdownOrRetainGraphicsObjects();
}
void BuildPublishedFrame(const OpenXRD3D12Frame& source, bool immersive,
void BuildPublishedFrame(const OpenXRBackendFrame& source, bool immersive,
float units_per_meter, uint64_t content_tag) noexcept {
ApplyPendingReferenceSpaceChange(source.xr_frame);
auto& destination = published_frame_.frame;
@@ -722,8 +824,8 @@ private:
// Anchors the menu screen in the application space and holds it there. The
// pose is captured once, from the first frame whose head pose is good enough
// to place it, and released again by a recenter or an origin change.
void UpdateVirtualScreenPose(OpenXRD3D12Frame& frame) noexcept {
if (frame.presentation.mode != OpenXRD3D12FrameMode::VirtualScreen) {
void UpdateVirtualScreenPose(OpenXRBackendFrame& frame) noexcept {
if (frame.presentation.mode != OpenXRFrameMode::VirtualScreen) {
return;
}
constexpr XrViewStateFlags kPoseUsable =
@@ -775,17 +877,31 @@ private:
}
}
// Converts the compositor's predicted display time onto the runtime's
// steady clock, which is what Aurora's interpolation deadlines are paced by.
uint64_t DisplayTimeNanos(XrTime display_time) noexcept {
if (convert_display_time_ == nullptr) return 0;
const auto now = std::chrono::steady_clock::now();
const auto now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count();
#if defined(_WIN32)
LARGE_INTEGER display_counter{}, counter{}, frequency{};
if (XR_FAILED(convert_display_time_(runtime_->Instance(), display_time, &display_counter)) ||
!QueryPerformanceFrequency(&frequency) || frequency.QuadPart <= 0 ||
!QueryPerformanceCounter(&counter)) return 0;
const auto now = std::chrono::steady_clock::now();
const auto now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count();
const auto delta = static_cast<int64_t>(
(static_cast<double>(display_counter.QuadPart) - static_cast<double>(counter.QuadPart)) *
1.0e9 / static_cast<double>(frequency.QuadPart));
#else
// XR_KHR_convert_timespec_time yields CLOCK_MONOTONIC, the clock behind
// libc++'s steady_clock, so the delta is measured on that clock too.
timespec display_spec{};
timespec now_spec{};
if (XR_FAILED(convert_display_time_(runtime_->Instance(), display_time, &display_spec)) ||
clock_gettime(CLOCK_MONOTONIC, &now_spec) != 0) return 0;
const int64_t display_ns = static_cast<int64_t>(display_spec.tv_sec) * 1'000'000'000ll + display_spec.tv_nsec;
const int64_t monotonic_ns = static_cast<int64_t>(now_spec.tv_sec) * 1'000'000'000ll + now_spec.tv_nsec;
const int64_t delta = display_ns - monotonic_ns;
#endif
return now_ns + delta > 0 ? static_cast<uint64_t>(now_ns + delta) : 0;
}
@@ -810,7 +926,8 @@ private:
OpenXRLogCallback logger_;
std::unique_ptr<OpenXRRuntime> runtime_;
std::unique_ptr<OpenXRD3D12Backend> backend_;
std::unique_ptr<GraphicsBackend> backend_;
std::unique_ptr<OpenXRInput> input_;
std::thread pacing_thread_;
std::atomic_bool stop_{false};
std::atomic_bool running_{false};
@@ -827,7 +944,11 @@ private:
std::chrono::steady_clock::time_point timing_start_ = std::chrono::steady_clock::now();
uint32_t timing_submissions_ = 0;
PFN_xrGetDisplayRefreshRateFB get_display_refresh_rate_ = nullptr;
#if defined(_WIN32)
using ConvertDisplayTime = XrResult (XRAPI_PTR*)(XrInstance, XrTime, LARGE_INTEGER*);
#else
using ConvertDisplayTime = XrResult (XRAPI_PTR*)(XrInstance, XrTime, struct timespec*);
#endif
ConvertDisplayTime convert_display_time_ = nullptr;
std::atomic<PublishedFrame*> published_{nullptr};
PublishedFrame published_frame_{};
@@ -849,7 +970,7 @@ private:
bool graphics_retained_ = false;
};
#endif // defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#endif // MKW_OPENXR_GRAPHICS_BACKEND
} // namespace
@@ -857,32 +978,32 @@ OpenXRStartupResult OpenXRPrepareAurora(AuroraConfig& config) {
#if !defined(MKW_ENABLE_OPENXR)
(void)config;
ConfigurePolicy(false);
return RuntimeConfigFile::VrEnabled(false) ? OpenXRStartupResult::Unavailable
: OpenXRStartupResult::Disabled;
#elif defined(_WIN32)
return RuntimeConfigFile::VrEnabled(kVrEnabledDefault) ? OpenXRStartupResult::Unavailable
: OpenXRStartupResult::Disabled;
#elif MKW_OPENXR_GRAPHICS_BACKEND
return OpenXRIntegration::Get().Prepare(config);
#else
ConfigurePolicy(RuntimeConfigFile::VrEnabled(false));
if (!RuntimeConfigFile::VrEnabled(false)) {
(void)config;
ConfigurePolicy(RuntimeConfigFile::VrEnabled(kVrEnabledDefault));
if (!RuntimeConfigFile::VrEnabled(kVrEnabledDefault)) {
return OpenXRStartupResult::Disabled;
}
const OpenXRVulkanCapabilityInfo capability = OpenXRVulkanBackend::DawnInteropCapability();
RT_LOG(RT_TAG_RUNTIME) << "OpenXR Vulkan unavailable: " << capability.reason << std::endl;
RT_LOG(RT_TAG_RUNTIME) << "OpenXR is not wired to a graphics backend on this platform" << std::endl;
return OpenXRStartupResult::Unavailable;
#endif
}
bool OpenXRStartAfterAurora(AuroraBackend active_backend) {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#if MKW_OPENXR_GRAPHICS_BACKEND
return OpenXRIntegration::Get().Start(active_backend);
#else
(void)active_backend;
return !RuntimeConfigFile::VrEnabled(false);
return !RuntimeConfigFile::VrEnabled(kVrEnabledDefault);
#endif
}
void OpenXRShutdownBeforeAurora() noexcept {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#if MKW_OPENXR_GRAPHICS_BACKEND
OpenXRIntegration::Get().Shutdown();
#else
MkwVRPolicySetSessionActive(false);
@@ -890,13 +1011,13 @@ void OpenXRShutdownBeforeAurora() noexcept {
}
void OpenXRServiceProducerFrameBoundary() noexcept {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#if MKW_OPENXR_GRAPHICS_BACKEND
OpenXRIntegration::Get().ServiceProducerFrameBoundary();
#endif
}
bool OpenXRIsRunning() noexcept {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#if MKW_OPENXR_GRAPHICS_BACKEND
return OpenXRIntegration::Get().IsRunning();
#else
return false;
@@ -904,13 +1025,13 @@ bool OpenXRIsRunning() noexcept {
}
void OpenXRRequestRecenter() noexcept {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#if MKW_OPENXR_GRAPHICS_BACKEND
OpenXRIntegration::Get().RequestRecenter();
#endif
}
void OpenXRSetLeanBackDegrees(float degrees) noexcept {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#if MKW_OPENXR_GRAPHICS_BACKEND
OpenXRIntegration::Get().SetLeanBackDegrees(degrees);
#else
(void)degrees;
@@ -918,7 +1039,7 @@ void OpenXRSetLeanBackDegrees(float degrees) noexcept {
}
void OpenXRSetFrameInterpolationFps(uint32_t target) noexcept {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#if MKW_OPENXR_GRAPHICS_BACKEND
OpenXRIntegration::Get().SetFrameInterpolationFps(target);
#else
(void)target;
@@ -926,7 +1047,7 @@ void OpenXRSetFrameInterpolationFps(uint32_t target) noexcept {
}
OpenXRFrameTiming OpenXRGetFrameTiming() noexcept {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#if MKW_OPENXR_GRAPHICS_BACKEND
return OpenXRIntegration::Get().FrameTiming();
#else
return {};
@@ -934,7 +1055,7 @@ OpenXRFrameTiming OpenXRGetFrameTiming() noexcept {
}
bool OpenXRFrameInterpolationAvailable() noexcept {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#if MKW_OPENXR_GRAPHICS_BACKEND
return OpenXRIntegration::Get().FrameInterpolationAvailable();
#else
return false;
@@ -944,10 +1065,10 @@ bool OpenXRFrameInterpolationAvailable() noexcept {
std::string OpenXRLastError() {
#if !defined(MKW_ENABLE_OPENXR)
return "this build was compiled without OpenXR support";
#elif defined(_WIN32)
#elif MKW_OPENXR_GRAPHICS_BACKEND
return OpenXRIntegration::Get().LastError();
#else
return OpenXRVulkanBackend::DawnInteropCapability().reason;
return "OpenXR is not wired to a graphics backend on this platform";
#endif
}
+1
View File
@@ -215,6 +215,7 @@ bool OpenXRRuntime::CreateInstance() {
}
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;
File diff suppressed because it is too large. Load diff