884 lines
36 KiB
C++
884 lines
36 KiB
C++
/*
|
|
* Copyright (c) 2026 Mykhailo Mamedov. All rights reserved.
|
|
*
|
|
* RESEARCH PREVIEW / REFERENCE ONLY:
|
|
* This source code is provided solely for the purpose of reviewing
|
|
* the author's research methods and implementation.
|
|
*
|
|
* NO LICENSE GRANTED:
|
|
* This code is NOT for distribution, modification, or use in any
|
|
* project (commercial or otherwise). Unauthorized copying or
|
|
* use of this code is strictly prohibited.
|
|
*
|
|
* For inquiries regarding use or licensing, contact: ua.modin@gmail.com
|
|
*
|
|
* Description:
|
|
*
|
|
* The OpenXR Interface serves as an application-to-hardware bridge through OpenXR,
|
|
* currently optimized for integration with VR systems like SteamVR.
|
|
*
|
|
* Current Iteration:
|
|
* - Connection via OpenXR, basic session, instance.
|
|
* - Provides HMD transforms and hand tracking data.
|
|
*
|
|
* Future Improvements:
|
|
* - Structure changes.
|
|
* - Device/API compatibility checking improvements.
|
|
*
|
|
*/
|
|
|
|
|
|
#ifndef OPENXR_HEADER
|
|
#define OPENXR_HEADER
|
|
|
|
#define XR_USE_PLATFORM_WIN32
|
|
#define XR_USE_GRAPHICS_API_VULKAN
|
|
#include <openxr/openxr.h>
|
|
#include <openxr/openxr_platform.h>
|
|
#include <iostream>
|
|
#include <vector>
|
|
#include <functional>
|
|
|
|
namespace cfgXr {
|
|
;
|
|
}
|
|
|
|
namespace M_CORE {
|
|
struct sem;
|
|
struct mDevice;
|
|
struct PhyDevice;
|
|
}
|
|
|
|
struct SwapchainResources {
|
|
XrSwapchain handle = nullptr;
|
|
std::vector<VkImage> images;
|
|
std::vector<VkImageView> imageViews;
|
|
std::vector<M_CORE::m_texture> m_depthImages;
|
|
VkFormat format = VkFormat::VK_FORMAT_UNDEFINED;
|
|
};
|
|
|
|
enum HandIndex {
|
|
HAND_LEFT = 0,
|
|
HAND_RIGHT = 1,
|
|
HAND_COUNT = 2
|
|
};
|
|
|
|
struct HandTrackingState {
|
|
XrHandJointLocationEXT jointLocations[2][XR_HAND_JOINT_COUNT_EXT];
|
|
XrHandJointLocationsEXT handLocations[2];
|
|
};
|
|
|
|
/**
|
|
* CB signature : It passes the views(for camera transforms)
|
|
* and the index the swapchain wants app to render to.
|
|
*/
|
|
using RenderCallback = std::function<void(
|
|
const std::vector<XrCompositionLayerProjectionView>& projectionViews,
|
|
uint32_t imageIndex
|
|
)>;
|
|
|
|
|
|
class OpenXrInterface {
|
|
public:
|
|
|
|
void SetRenderCallback(RenderCallback cb) { m_renderCallback = cb; }
|
|
void CreateXrSession(VkInstance vulkanInstance, M_CORE::mDevice& m_device, u32 queueFamilyIndex, RENDERDOC_API_1_6_0* rdoc_api);
|
|
bool checkDeviceCompatible(M_CORE::mDevice& m_device);
|
|
void PrepareSwapchainImageViews(M_CORE::mCore& core, M_CORE::mDevice& device, SwapchainResources& resources);
|
|
void CreateXrSwapchain(M_CORE::mCore& core, M_CORE::mDevice& device);
|
|
void RenderXrScene();
|
|
|
|
OpenXrInterface() {
|
|
PrintInstanceExtensions();
|
|
m_currentInstance = createXrInstance();
|
|
XrSystemGetInfo info{ XR_TYPE_SYSTEM_GET_INFO };
|
|
info.formFactor = XrFormFactor::XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
|
|
XrResult res = xrGetSystem(m_currentInstance, &info, &m_xrSystemId);
|
|
assert(!XR_FAILED(res) && "xrGetSystem failed!");
|
|
SetRequiredInstanceExtensions();
|
|
SetVulkanXrDeviceExtensions();
|
|
getVulkanGfxRequirement();
|
|
GetViewConfigViews(m_currentInstance, m_xrSystemId);
|
|
SetEnvironmentBlendModes();
|
|
|
|
XrResult result = xrGetInstanceProcAddr(
|
|
m_currentInstance, "xrLocateHandJointsEXT", (PFN_xrVoidFunction*)&pfnxrLocateHandJointsEXT);
|
|
assert(!XR_FAILED(result));
|
|
|
|
}
|
|
|
|
// Example: Get configuration for the primary stereo view
|
|
void GetViewConfigViews(XrInstance instance, XrSystemId systemId) {
|
|
uint32_t viewCount = 0;
|
|
|
|
// Get the required count
|
|
XrResult result = xrEnumerateViewConfigurationViews(
|
|
instance, systemId, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
|
|
0, &viewCount, nullptr);
|
|
assert(!XR_FAILED(result));
|
|
|
|
// Allocate and get data
|
|
m_views.resize(viewCount, { XR_TYPE_VIEW_CONFIGURATION_VIEW });
|
|
result = xrEnumerateViewConfigurationViews(
|
|
instance, systemId, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
|
|
viewCount, &viewCount, m_views.data());
|
|
assert(!XR_FAILED(result));
|
|
|
|
// Set defaults:
|
|
assert(m_views.size());
|
|
m_samples = m_views[0].recommendedSwapchainSampleCount;
|
|
m_width = m_views[0].recommendedImageRectWidth;
|
|
m_height = m_views[0].recommendedImageRectHeight;
|
|
}
|
|
|
|
XrInstance GetXrInstance() {
|
|
assert(m_currentInstance);
|
|
return m_currentInstance;
|
|
}
|
|
|
|
void BeginXrSession() {
|
|
XrSessionBeginInfo beginInfo{ XR_TYPE_SESSION_BEGIN_INFO };
|
|
beginInfo.primaryViewConfigurationType = XrViewConfigurationType::XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
|
beginInfo.next = nullptr;
|
|
|
|
XrResult result = xrBeginSession(m_xrSession, &beginInfo);
|
|
assert(XR_SUCCEEDED(result));
|
|
m_sessionLaunched = true;
|
|
}
|
|
|
|
XrInstance createXrInstance() {
|
|
XrInstanceCreateInfo instanceInfo{ XR_TYPE_INSTANCE_CREATE_INFO };
|
|
instanceInfo.applicationInfo = {
|
|
.applicationName = "TheApp",
|
|
.applicationVersion = 1,
|
|
.engineName = "TheEngine",
|
|
.engineVersion = 1,
|
|
.apiVersion = XR_API_VERSION_1_0 // XR_CURRENT_API_VERSION // Forward compat.
|
|
};
|
|
|
|
std::vector<const char*> enabledExtensions = {
|
|
XR_KHR_VULKAN_ENABLE_EXTENSION_NAME,
|
|
XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME,
|
|
XR_EXT_HAND_TRACKING_EXTENSION_NAME
|
|
};
|
|
|
|
// XR_ERROR_EXTENSION_NOT_PRESENT TODO.
|
|
instanceInfo.enabledExtensionCount = enabledExtensions.size();
|
|
instanceInfo.enabledExtensionNames = enabledExtensions.data();
|
|
|
|
XrInstance instance;
|
|
XrResult result = xrCreateInstance(&instanceInfo, &instance);
|
|
assert(!XR_FAILED(result) && "OpenXR Runtime not found or failed!");
|
|
|
|
result = xrGetInstanceProperties(instance, &m_instanceProps);
|
|
assert(!XR_FAILED(result));
|
|
|
|
std::cout << "OpenXR Instance created successfully!" << std::endl;
|
|
std::cout << "Runtime Name: " << m_instanceProps.runtimeName << std::endl;
|
|
std::cout << "Runtime Version: " << XR_VERSION_MAJOR(m_instanceProps.runtimeVersion) << "."
|
|
<< XR_VERSION_MINOR(m_instanceProps.runtimeVersion) << "."
|
|
<< XR_VERSION_PATCH(m_instanceProps.runtimeVersion) << std::endl;
|
|
return instance;
|
|
}
|
|
|
|
void PrintInstanceExtensions() {
|
|
uint32_t extensionCount = 0;
|
|
xrEnumerateInstanceExtensionProperties(nullptr, 0, &extensionCount, nullptr);
|
|
std::vector<XrExtensionProperties> extensionProperties(extensionCount, { XR_TYPE_EXTENSION_PROPERTIES });
|
|
xrEnumerateInstanceExtensionProperties(nullptr, extensionCount, &extensionCount, extensionProperties.data());
|
|
|
|
std::cout << "Xr Extensions:" << std::endl;
|
|
for (const auto& ext : extensionProperties) {
|
|
std::cout << std::string(ext.extensionName) << std::endl;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Retrieves hand tracking information from the specified context.
|
|
*
|
|
* This function retrieves details about hand tracking, such as joint locations,
|
|
*/
|
|
void getHandTrackingInfo(XrHandTrackerEXT hand_tracker, const std::vector<XrView>& views, XrTime predictedDisplayTime, HandIndex hand_idx) {
|
|
|
|
m_handTrack.handLocations[hand_idx] = { XR_TYPE_HAND_JOINT_LOCATIONS_EXT };
|
|
m_handTrack.handLocations[hand_idx].jointCount = XR_HAND_JOINT_COUNT_EXT;
|
|
m_handTrack.handLocations[hand_idx].jointLocations = m_handTrack.jointLocations[hand_idx];
|
|
|
|
XrHandJointsLocateInfoEXT locateInfo{ XR_TYPE_HAND_JOINTS_LOCATE_INFO_EXT };
|
|
locateInfo.baseSpace = m_appSpace;
|
|
locateInfo.time = predictedDisplayTime;
|
|
|
|
XrResult result = pfnxrLocateHandJointsEXT(hand_tracker, &locateInfo, &m_handTrack.handLocations[hand_idx]);
|
|
assert(XR_SUCCEEDED(result));
|
|
|
|
/*
|
|
if (m_jointLocationsInfo.isActive) {
|
|
std::cout << "\n=== Hand Tracking Frame Data ===" << std::endl;
|
|
|
|
std::cout << std::fixed << std::setprecision(4);
|
|
|
|
for (uint32_t i = 0; i < XR_HAND_JOINT_COUNT_EXT; ++i) {
|
|
constexpr XrSpaceLocationFlags validFlags =
|
|
XR_SPACE_LOCATION_POSITION_VALID_BIT |
|
|
XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
|
|
|
if ((m_jointLocations[i].locationFlags & validFlags) == validFlags) {
|
|
const XrVector3f& pos = m_jointLocations[i].pose.position;
|
|
const XrQuaternionf& rot = m_jointLocations[i].pose.orientation;
|
|
float radius = m_jointLocations[i].radius;
|
|
|
|
std::cout << "Joint [" << std::setw(2) << i << "] "
|
|
<< "Pos: (" << std::setw(7) << pos.x << ", "
|
|
<< std::setw(7) << pos.y << ", "
|
|
<< std::setw(7) << pos.z << ") | "
|
|
<< "Rot: (" << std::setw(7) << rot.x << ", "
|
|
<< std::setw(7) << rot.y << ", "
|
|
<< std::setw(7) << rot.z << ", "
|
|
<< std::setw(7) << rot.w << ") | "
|
|
<< "Rad: " << radius << "\n";
|
|
}
|
|
else {
|
|
std::cout << "Joint [" << std::setw(2) << i << "] Not tracked (invalid flags)\n";
|
|
}
|
|
}
|
|
std::cout << "================================\n" << std::endl;
|
|
}
|
|
else {
|
|
std::cout << "Hand tracker is currently INACTIVE." << std::endl;
|
|
}
|
|
*/
|
|
}
|
|
|
|
/**
|
|
* @brief Creates and initializes hand trackers.
|
|
*
|
|
* This function creates instances of XrHandTrackerEXT,
|
|
* It sets up the necessary configurations to start tracking both left and right hands in the current session.
|
|
* The created trackers are stored internally within this class instance.
|
|
*
|
|
*/
|
|
void CreateHandTrackers() {
|
|
|
|
XrHandTrackerCreateInfoEXT createInfo = { XrStructureType::XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT };
|
|
createInfo.hand = XrHandEXT::XR_HAND_LEFT_EXT;
|
|
createInfo.handJointSet = XrHandJointSetEXT::XR_HAND_JOINT_SET_DEFAULT_EXT;
|
|
|
|
PFN_xrCreateHandTrackerEXT pfnxrCreateHandTrackerEXT = nullptr;
|
|
XrResult result = xrGetInstanceProcAddr(m_currentInstance, "xrCreateHandTrackerEXT", (PFN_xrVoidFunction*)&pfnxrCreateHandTrackerEXT);
|
|
assert(!XR_FAILED(result));
|
|
|
|
result = pfnxrCreateHandTrackerEXT(m_xrSession, &createInfo, &m_leftHandTracker);
|
|
assert(!XR_FAILED(result));
|
|
|
|
createInfo.hand = XR_HAND_RIGHT_EXT;
|
|
result = pfnxrCreateHandTrackerEXT(m_xrSession, &createInfo, &m_rightHandTracker);
|
|
assert(!XR_FAILED(result));
|
|
}
|
|
|
|
void SetEnvironmentBlendModes() {
|
|
uint32_t count = 0;
|
|
XrViewConfigurationType viewType = XrViewConfigurationType::XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
|
|
|
// First call to get the required count
|
|
XrResult result = xrEnumerateEnvironmentBlendModes(m_currentInstance, m_xrSystemId, viewType, 0, &count, nullptr);
|
|
assert(XR_SUCCEEDED(result));
|
|
|
|
m_blendModes.resize(count);
|
|
// Second call to get the actual data
|
|
result = xrEnumerateEnvironmentBlendModes(m_currentInstance, m_xrSystemId, viewType, count, &count, m_blendModes.data());
|
|
assert(XR_SUCCEEDED(result));
|
|
}
|
|
|
|
void SetRequiredInstanceExtensions() {
|
|
PFN_xrGetVulkanInstanceExtensionsKHR pfnGetVulkanInstanceExtensionsKHR = nullptr;
|
|
XrResult result = xrGetInstanceProcAddr(m_currentInstance, "xrGetVulkanInstanceExtensionsKHR", (PFN_xrVoidFunction*)&pfnGetVulkanInstanceExtensionsKHR);
|
|
assert(!XR_FAILED(result));
|
|
|
|
uint32_t length = 0;
|
|
result = pfnGetVulkanInstanceExtensionsKHR(m_currentInstance, m_xrSystemId, 0, &length, nullptr);
|
|
assert(!XR_FAILED(result));
|
|
if (length == 0) return;
|
|
|
|
std::string extensions(length, ' ');
|
|
result = pfnGetVulkanInstanceExtensionsKHR(m_currentInstance, m_xrSystemId, length, &length, extensions.data());
|
|
assert(!XR_FAILED(result));
|
|
// Parse the space-separated string
|
|
std::stringstream ss(extensions);
|
|
std::string ext;
|
|
while (ss >> ext) {
|
|
m_requiredVulkanInstanceExtensions.push_back(ext);
|
|
}
|
|
}
|
|
|
|
void SetVulkanXrDeviceExtensions() {
|
|
PFN_xrGetVulkanDeviceExtensionsKHR pfnGetVulkanDeviceExtensionsKHR = nullptr;
|
|
XrResult result = xrGetInstanceProcAddr(m_currentInstance, "xrGetVulkanDeviceExtensionsKHR", (PFN_xrVoidFunction*)&pfnGetVulkanDeviceExtensionsKHR);
|
|
assert(!XR_FAILED(result));
|
|
|
|
uint32_t count = 0;
|
|
|
|
// Get the required buffer size
|
|
result = pfnGetVulkanDeviceExtensionsKHR(m_currentInstance, m_xrSystemId, 0, &count, nullptr);
|
|
assert(!XR_FAILED(result));
|
|
if (count == 0) return;
|
|
|
|
// Allocate buffer
|
|
std::string buffer(count, ' ');
|
|
|
|
// Populate buffer
|
|
result = pfnGetVulkanDeviceExtensionsKHR(m_currentInstance, m_xrSystemId, count, &count, &buffer[0]);
|
|
assert(!XR_FAILED(result));
|
|
// Parse just like the instance extensions
|
|
std::stringstream ss(buffer);
|
|
std::string ext;
|
|
while (ss >> ext) {
|
|
m_requiredVulkanDeviceExtensions.push_back(ext);
|
|
}
|
|
}
|
|
|
|
VkPhysicalDevice GetVulkanPhysicalDevice(VkInstance vkInstance) {
|
|
assert(m_currentInstance && m_xrSystemId && "Wrong order of execution.");
|
|
PFN_xrGetVulkanGraphicsDevice2KHR pfnGetVulkanGraphicsDevice2KHR = nullptr;
|
|
XrResult res = xrGetInstanceProcAddr(m_currentInstance, "xrGetVulkanGraphicsDevice2KHR", (PFN_xrVoidFunction*)&pfnGetVulkanGraphicsDevice2KHR);
|
|
assert(!XR_FAILED(res) && "xrGetInstanceProcAddr failed!");
|
|
|
|
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
|
|
XrVulkanGraphicsDeviceGetInfoKHR getInfo{ XR_TYPE_VULKAN_GRAPHICS_DEVICE_GET_INFO_KHR };
|
|
getInfo.vulkanInstance = vkInstance;
|
|
getInfo.systemId = m_xrSystemId;
|
|
res = pfnGetVulkanGraphicsDevice2KHR(m_currentInstance, &getInfo, &physicalDevice);
|
|
assert(!XR_FAILED(res) && "pfnGetVulkanGraphicsDevice2 failed!");
|
|
|
|
return physicalDevice;
|
|
}
|
|
|
|
void getVulkanGfxRequirement(){
|
|
PFN_xrGetVulkanGraphicsRequirementsKHR pfnGetVulkanGraphicsRequirementsKHR = nullptr;
|
|
|
|
XrResult result = xrGetInstanceProcAddr(m_currentInstance, "xrGetVulkanGraphicsRequirementsKHR",
|
|
(PFN_xrVoidFunction*)&pfnGetVulkanGraphicsRequirementsKHR);
|
|
|
|
assert(!XR_FAILED(result) && "xrGetInstanceProcAddr failed!");
|
|
assert(pfnGetVulkanGraphicsRequirementsKHR &&
|
|
"Extension (xrGetVulkanGraphicsRequirementsKHR) not supported by runtime!");
|
|
|
|
// (must be done before xrCreateSession)
|
|
result = pfnGetVulkanGraphicsRequirementsKHR(m_currentInstance, m_xrSystemId, &m_graphicsRequirements);
|
|
assert(!XR_FAILED(result) && "pfnGetVulkanGraphicsRequirementsKHR failed!");
|
|
}
|
|
|
|
void ResetState(XrEventDataBuffer& eventData) {
|
|
std::memset(&eventData, 0, sizeof(XrEventDataBuffer));
|
|
eventData.type = XR_TYPE_EVENT_DATA_BUFFER;
|
|
eventData.next = nullptr;
|
|
}
|
|
|
|
bool IsInstanceStateInvalid() {
|
|
XrInstanceProperties instanceProperties{ XR_TYPE_INSTANCE_PROPERTIES };
|
|
if (!m_currentInstance)
|
|
return true;
|
|
XrResult result = xrGetInstanceProperties(m_currentInstance, &instanceProperties);
|
|
assert(XR_SUCCEEDED(result));
|
|
if (result == XR_ERROR_HANDLE_INVALID) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void HandshakeSimulatorSwapchains() { // TODO: Check if necessary.
|
|
XrSwapchain sw_chain = swapChainResources->handle;
|
|
assert(sw_chain);
|
|
|
|
uint32_t imageIndex = 0;
|
|
XrSwapchainImageAcquireInfo acquireInfo{ XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO };
|
|
XrSwapchainImageReleaseInfo releaseInfo{ XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
|
|
|
|
// Check out the texture array from the runtime
|
|
XrResult res = xrAcquireSwapchainImage(sw_chain, &acquireInfo, &imageIndex);
|
|
|
|
if (res == XR_SUCCESS) {
|
|
|
|
XrSwapchainImageWaitInfo waitInfo{ XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO };
|
|
waitInfo.timeout = XR_INFINITE_DURATION; // Let it block until ready
|
|
res = xrWaitSwapchainImage(sw_chain, &waitInfo);
|
|
assert(XR_SUCCEEDED(res));
|
|
|
|
// Immediately hand it right back to unblock the compositor queue
|
|
res = xrReleaseSwapchainImage(sw_chain, &releaseInfo);
|
|
assert(XR_SUCCEEDED(res));
|
|
}
|
|
}
|
|
|
|
void SetupXrSpace() {
|
|
XrReferenceSpaceCreateInfo spaceInfo = { XR_TYPE_REFERENCE_SPACE_CREATE_INFO };
|
|
spaceInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; // XR_REFERENCE_SPACE_TYPE_LOCAL
|
|
spaceInfo.poseInReferenceSpace = { {0,0,0,1}, {0,0,0} };
|
|
assert(xrCreateReferenceSpace(m_xrSession, &spaceInfo, &m_appSpace) == XR_SUCCESS);
|
|
}
|
|
|
|
~OpenXrInterface() {
|
|
if(m_currentInstance)
|
|
xrDestroyInstance(m_currentInstance);
|
|
if (m_xrSession)
|
|
xrDestroySession(m_xrSession);
|
|
}
|
|
|
|
u64 m_scheduledWorkloadCounter = 0;
|
|
u32 m_previousIndex = 20;
|
|
u32 m_width = 0;
|
|
u32 m_height = 0;
|
|
u32 m_samples = 0;
|
|
u32 m_arraySizeInitial = 2;
|
|
u32 m_frame = 0;
|
|
bool m_sessionLaunched = false;
|
|
bool r_running = true;
|
|
bool r_ready = false;
|
|
bool r_idle = false;
|
|
bool r_isSessionRunning = false;
|
|
bool capture_xr = false;
|
|
u32 m_captured_frames = 0;
|
|
|
|
M_CORE::mDevice* m_deviceGfx = nullptr;
|
|
RENDERDOC_API_1_6_0* m_rdoc_api;
|
|
|
|
XrSessionState r_latestState = XR_SESSION_STATE_UNKNOWN;
|
|
XrSession m_xrSession = nullptr;
|
|
XrSystemId m_xrSystemId = 0;
|
|
XrSpace m_appSpace;
|
|
|
|
XrInstance m_currentInstance = nullptr;
|
|
XrInstanceProperties m_instanceProps{ XR_TYPE_INSTANCE_PROPERTIES };
|
|
XrHandTrackerEXT m_leftHandTracker;
|
|
XrHandTrackerEXT m_rightHandTracker;
|
|
XrGraphicsRequirementsVulkanKHR m_graphicsRequirements = { XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR };
|
|
XrActionSetCreateInfo m_actionSetInfo{ XR_TYPE_ACTION_SET_CREATE_INFO };
|
|
XrSessionActionSetsAttachInfo m_attachInfo{ XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO };
|
|
PFN_xrLocateHandJointsEXT pfnxrLocateHandJointsEXT = nullptr;
|
|
|
|
std::vector<XrEnvironmentBlendMode> m_blendModes;
|
|
std::vector<XrViewConfigurationView> m_views;
|
|
std::vector <std::string> m_requiredVulkanInstanceExtensions;
|
|
std::vector <std::string> m_requiredVulkanDeviceExtensions;
|
|
|
|
SwapchainResources* swapChainResources = nullptr;
|
|
VkCommandPool xrCommandPool = nullptr;
|
|
RenderCallback m_renderCallback;
|
|
HandTrackingState m_handTrack{};
|
|
|
|
};
|
|
|
|
#endif // OPENXR_HEADER
|
|
|
|
|
|
#ifdef OPENXR_COMPONENT
|
|
|
|
void OpenXrInterface::CreateXrSession(VkInstance vulkanInstance, M_CORE::mDevice& m_device, u32 queueFamilyIndex, RENDERDOC_API_1_6_0* rdoc_api) {
|
|
XrGraphicsBindingVulkanKHR graphicsBinding = { XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR };
|
|
|
|
graphicsBinding.instance = vulkanInstance;
|
|
graphicsBinding.physicalDevice = m_device.phyDeviceData.m_physDevice;
|
|
graphicsBinding.device = m_device.virtualDevice;
|
|
graphicsBinding.queueFamilyIndex = queueFamilyIndex;
|
|
graphicsBinding.queueIndex = 0; // ?
|
|
|
|
XrSessionCreateInfo sessionCreateInfo = { XR_TYPE_SESSION_CREATE_INFO };
|
|
sessionCreateInfo.next = &graphicsBinding;
|
|
sessionCreateInfo.systemId = m_xrSystemId;
|
|
|
|
XrResult res = xrCreateSession(m_currentInstance, &sessionCreateInfo, &m_xrSession);
|
|
assert(res == XR_SUCCESS && m_xrSession);
|
|
|
|
SetupXrSpace();
|
|
|
|
// Create action set (TEST)
|
|
strcpy(m_actionSetInfo.actionSetName, "gameplay");
|
|
strcpy(m_actionSetInfo.localizedActionSetName, "Gameplay");
|
|
XrActionSet actionSet;
|
|
res = xrCreateActionSet(m_currentInstance, &m_actionSetInfo, &actionSet);
|
|
assert(XR_SUCCEEDED(res));
|
|
|
|
// Attach to the session
|
|
m_attachInfo.countActionSets = 1;
|
|
m_attachInfo.actionSets = &actionSet;
|
|
res = xrAttachSessionActionSets(m_xrSession, &m_attachInfo);
|
|
assert(XR_SUCCEEDED(res));
|
|
|
|
m_deviceGfx = &m_device;
|
|
m_rdoc_api = rdoc_api;
|
|
}
|
|
|
|
bool OpenXrInterface::checkDeviceCompatible(M_CORE::mDevice& m_device) {
|
|
|
|
// Ensure the device supports at least the minimum version
|
|
uint32_t xrMinMajor = (m_graphicsRequirements.minApiVersionSupported >> 48) & 0xFFFF;
|
|
uint32_t xrMinMinor = (m_graphicsRequirements.minApiVersionSupported >> 32) & 0xFFFF;
|
|
|
|
uint32_t vkMajor = (m_device.phyDeviceData.m_devProps.apiVersion >> 22) & 0x3FF;
|
|
uint32_t vkMinor = (m_device.phyDeviceData.m_devProps.apiVersion >> 12) & 0x3FF;
|
|
|
|
bool versionOk = vkMajor >= xrMinMajor && vkMinor >= xrMinMinor;
|
|
|
|
if (!versionOk) {
|
|
auto err_string = std::format("Api version {} not supported on {}, required {}",
|
|
m_device.phyDeviceData.m_devProps.apiVersion,
|
|
m_device.phyDeviceData.m_devProps.deviceName,
|
|
m_graphicsRequirements.minApiVersionSupported
|
|
);
|
|
assert(versionOk && err_string.c_str());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Create XR swap-chain.
|
|
*/
|
|
void OpenXrInterface::CreateXrSwapchain(M_CORE::mCore& core, M_CORE::mDevice& device) {
|
|
|
|
assert(m_currentInstance && m_xrSession);
|
|
|
|
// Create Swapchain
|
|
swapChainResources = new SwapchainResources();
|
|
swapChainResources->format = VK_FORMAT_R8G8B8A8_SRGB;
|
|
|
|
XrSwapchainCreateInfo swapchainInfo = { XR_TYPE_SWAPCHAIN_CREATE_INFO };
|
|
swapchainInfo.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT; // Adding XR_SWAPCHAIN_USAGE_SAMPLED_BIT
|
|
swapchainInfo.format = swapChainResources->format;
|
|
swapchainInfo.sampleCount = 1; // m_samples
|
|
swapchainInfo.width = m_width;
|
|
swapchainInfo.height = m_height;
|
|
swapchainInfo.faceCount = 1;
|
|
swapchainInfo.arraySize = m_arraySizeInitial; // Stereo
|
|
swapchainInfo.mipCount = 1;
|
|
swapchainInfo.createFlags = 0;
|
|
|
|
XrResult res = xrCreateSwapchain(m_xrSession, &swapchainInfo, &swapChainResources->handle);
|
|
assert(XR_SUCCEEDED(res));
|
|
|
|
// Prepare views
|
|
PrepareSwapchainImageViews(core, device, *swapChainResources);
|
|
}
|
|
|
|
/**
|
|
* Function to prepare Vulkan Image and Depth Views.
|
|
*/
|
|
void OpenXrInterface::PrepareSwapchainImageViews(M_CORE::mCore& core, M_CORE::mDevice& device, SwapchainResources& resources) {
|
|
u32 imageCount = 0;
|
|
XrResult res = xrEnumerateSwapchainImages(resources.handle, 0, &imageCount, nullptr);
|
|
assert(XR_SUCCEEDED(res));
|
|
|
|
std::vector<XrSwapchainImageVulkanKHR> swapchainImages(imageCount, { XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR });
|
|
res = xrEnumerateSwapchainImages(
|
|
resources.handle, imageCount, &imageCount, reinterpret_cast<XrSwapchainImageBaseHeader*>(swapchainImages.data()));
|
|
assert(XR_SUCCEEDED(res));
|
|
|
|
resources.images.clear();
|
|
resources.imageViews.clear();
|
|
resources.m_depthImages.resize(imageCount);
|
|
u32 counter = 0;
|
|
for (int i = 0; i < imageCount; i++) {
|
|
auto& image = swapchainImages[i];
|
|
resources.images.push_back(image.image);
|
|
|
|
VkImageViewCreateInfo viewInfo = { VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
|
|
viewInfo.image = image.image;
|
|
viewInfo.viewType = VkImageViewType::VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
|
viewInfo.format = resources.format;
|
|
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
viewInfo.subresourceRange.baseMipLevel = 0;
|
|
viewInfo.subresourceRange.levelCount = 1;
|
|
viewInfo.subresourceRange.baseArrayLayer = 0;
|
|
viewInfo.subresourceRange.layerCount = m_arraySizeInitial;
|
|
|
|
VkImageView imageView;
|
|
if (vkCreateImageView(device.virtualDevice, &viewInfo, nullptr, &imageView) != VK_SUCCESS) {
|
|
throw std::runtime_error("failed to create image view!");
|
|
}
|
|
core.setObjectName(device.virtualDevice, imageView, std::format("xrImage_{}", counter).c_str());
|
|
|
|
resources.imageViews.push_back(imageView);
|
|
|
|
VkImageLayout OldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
VkImageLayout NewLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
core.TransitionImageLayout(viewInfo.image, resources.format, OldLayout, NewLayout, 1);
|
|
vkDeviceWaitIdle(device.virtualDevice);
|
|
counter++;
|
|
}
|
|
|
|
// Depth resources maintained by us, not XR api.
|
|
VkFormat DepthFormat = device.phyDeviceData.m_depthFormat;
|
|
for (int i = 0; i < imageCount; i++) {
|
|
|
|
M_CORE::ImageParams parms{
|
|
.device = device,
|
|
.Tex = resources.m_depthImages[i],
|
|
.ImageWidth = m_width,
|
|
.ImageHeight = m_height,
|
|
.TexFormat = DepthFormat,
|
|
.UsageFlags = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
|
|
.PropertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
|
|
.ImageDepth = 1,
|
|
.name = std::format("xrDepthImage_{}", i).c_str(),
|
|
.shareMode = VK_SHARING_MODE_EXCLUSIVE,
|
|
.sharedQueueFamilies = {0, 0},
|
|
.IsCubemap = false,
|
|
.Is3dImage = false,
|
|
.IsStereo = true
|
|
};
|
|
|
|
core.CreateImage(parms);
|
|
|
|
VkImageLayout OldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
VkImageLayout NewLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
|
|
|
M_CORE::ImageViewParams params{};
|
|
params.Device = device.virtualDevice;
|
|
params.Image = resources.m_depthImages[i].m_image;
|
|
params.Format = DepthFormat;
|
|
params.AspectFlags = VK_IMAGE_ASPECT_DEPTH_BIT;
|
|
params.IsStereo = true;
|
|
|
|
resources.m_depthImages[i].m_view = core.CreateImageView(params);
|
|
core.setObjectName(device.virtualDevice,
|
|
resources.m_depthImages[i].m_view, std::format("xrDepthImage_{}", i).c_str());
|
|
core.TransitionImageLayout(resources.m_depthImages[i].m_image, DepthFormat, OldLayout, NewLayout, 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief RenderXrScene is the function responsible for rendering scenes within an OpenXR application context,
|
|
* facilitating interaction between VR and graphics pipelines.
|
|
*
|
|
* @details It processes:
|
|
* - Camera transforms to ensure accurate alignment of virtual content with user's field of view.
|
|
* - Hand tracking data for realistic interactions in VR scenarios.
|
|
* - Swapchain images captured during each frame cycle to provide real-time visual feedback.
|
|
* This function integrates hardware inputs like camera movements, hand positions,
|
|
* orientations, and joint locations into the rendering process.
|
|
*/
|
|
void OpenXrInterface::RenderXrScene() {
|
|
|
|
XrResult result = XrResult::XR_SUCCESS;
|
|
assert(swapChainResources);
|
|
|
|
if (!r_running)
|
|
return;
|
|
|
|
while (true) {
|
|
|
|
if (IsInstanceStateInvalid()) {
|
|
r_running = false;
|
|
break;
|
|
}
|
|
|
|
// --- Event Handling ---
|
|
XrEventDataBuffer eventData = { XR_TYPE_EVENT_DATA_BUFFER, nullptr };
|
|
while (true) {
|
|
result = xrPollEvent(m_currentInstance, &eventData);
|
|
if (result != XR_SUCCESS) { // XR_ERROR_VALIDATION_FAILURE
|
|
if (result == XR_EVENT_UNAVAILABLE) {
|
|
ResetState(eventData);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
break;
|
|
}
|
|
char errorString[XR_MAX_RESULT_STRING_SIZE];
|
|
XrResult result2 = xrResultToString(m_currentInstance, result, errorString);
|
|
assert(XR_SUCCEEDED(result2));
|
|
printf(std::format("xrPollEvent throws: {}\n", errorString).c_str());
|
|
ResetState(eventData);
|
|
break;
|
|
}
|
|
|
|
if (eventData.type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) {
|
|
auto* stateChanged = reinterpret_cast<XrEventDataSessionStateChanged*>(&eventData);
|
|
r_latestState = stateChanged->state;
|
|
switch (stateChanged->state) {
|
|
case XR_SESSION_STATE_READY: {
|
|
BeginXrSession();
|
|
HandshakeSimulatorSwapchains();
|
|
ResetState(eventData);
|
|
break;
|
|
}
|
|
case XR_SESSION_STATE_SYNCHRONIZED: {
|
|
printf(std::format("XR_SESSION_STATE_SYNCHRONIZED\n").c_str());
|
|
r_isSessionRunning = true;
|
|
ResetState(eventData);
|
|
break;
|
|
}
|
|
case XR_SESSION_STATE_VISIBLE: {
|
|
printf(std::format("XR_SESSION_STATE_VISIBLE\n").c_str());
|
|
r_isSessionRunning = true;
|
|
ResetState(eventData);
|
|
break;
|
|
}
|
|
case XR_SESSION_STATE_FOCUSED: {
|
|
r_isSessionRunning = true;
|
|
ResetState(eventData);
|
|
break;
|
|
}
|
|
case XR_SESSION_STATE_IDLE: {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
ResetState(eventData);
|
|
break;
|
|
}
|
|
case XR_SESSION_STATE_STOPPING: {
|
|
r_isSessionRunning = false;
|
|
m_sessionLaunched = false;
|
|
if (stateChanged->state == XR_SESSION_STATE_STOPPING) {
|
|
xrEndSession(m_xrSession); // Clean up if required by spec
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
printf(std::format("Unhandled other event: {}\n", (int)eventData.type).c_str());
|
|
}
|
|
}
|
|
|
|
if (!m_sessionLaunched) {
|
|
break;
|
|
}
|
|
|
|
if (m_rdoc_api && capture_xr) m_rdoc_api->StartFrameCapture(m_deviceGfx->virtualDevice, nullptr);
|
|
|
|
// --- Frame Synchronization ---
|
|
XrFrameWaitInfo waitInfo = { XR_TYPE_FRAME_WAIT_INFO };
|
|
XrFrameState frameState = { XR_TYPE_FRAME_STATE };
|
|
result = xrWaitFrame(m_xrSession, &waitInfo, &frameState);
|
|
assert(!XR_FAILED(result));
|
|
|
|
XrFrameBeginInfo beginInfo = { XR_TYPE_FRAME_BEGIN_INFO };
|
|
result = xrBeginFrame(m_xrSession, &beginInfo);
|
|
assert(!XR_FAILED(result));
|
|
|
|
// --- Camera Pose Location ---
|
|
XrViewLocateInfo viewLocateInfo = { XR_TYPE_VIEW_LOCATE_INFO };
|
|
viewLocateInfo.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; // Assuming Stereo
|
|
viewLocateInfo.displayTime = frameState.predictedDisplayTime;
|
|
viewLocateInfo.space = m_appSpace;
|
|
|
|
XrViewState viewState = { XR_TYPE_VIEW_STATE };
|
|
u32 viewCount{ 0 };
|
|
std::vector<XrView> views;
|
|
result = xrLocateViews(m_xrSession, &viewLocateInfo, &viewState, viewCount, &viewCount, nullptr);
|
|
assert(!XR_FAILED(result));
|
|
assert(viewCount==2);
|
|
views.resize(viewCount, { XR_TYPE_VIEW });
|
|
result = xrLocateViews(m_xrSession, &viewLocateInfo, &viewState, viewCount, &viewCount, views.data());
|
|
assert(!XR_FAILED(result));
|
|
bool invalid_view = false;
|
|
if ((viewState.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT) == 0) {
|
|
invalid_view = true; // Pose is invalid, do not submit this layer
|
|
}
|
|
if ((viewState.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) == 0) {
|
|
invalid_view = true;
|
|
}
|
|
|
|
// --- Pull hand tracking.
|
|
getHandTrackingInfo(m_leftHandTracker, views, frameState.predictedDisplayTime, HAND_LEFT);
|
|
getHandTrackingInfo(m_rightHandTracker, views, frameState.predictedDisplayTime, HAND_RIGHT);
|
|
|
|
// --- Rendering ---
|
|
XrCompositionLayerProjection projectionLayer = { XR_TYPE_COMPOSITION_LAYER_PROJECTION };
|
|
std::vector<XrCompositionLayerProjectionView> projectionViews(viewCount, { XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW });
|
|
|
|
if (frameState.shouldRender) {
|
|
|
|
XrRect2Di singleEyeRect;
|
|
singleEyeRect.offset.x = 0;
|
|
singleEyeRect.offset.y = 0;
|
|
singleEyeRect.extent.width = m_width;
|
|
singleEyeRect.extent.height = m_height;
|
|
|
|
if(true) {
|
|
u32 imageIndex;
|
|
XrSwapchainImageAcquireInfo acquireInfo = { XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO };
|
|
result = xrAcquireSwapchainImage(swapChainResources->handle, &acquireInfo, &imageIndex);
|
|
assert(!XR_FAILED(result));
|
|
|
|
XrSwapchainImageWaitInfo waitInfo = { XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO };
|
|
waitInfo.timeout = XR_INFINITE_DURATION;
|
|
if (xrWaitSwapchainImage(swapChainResources->handle, &waitInfo) == XR_SUCCESS) {
|
|
|
|
// Prepare projection views.
|
|
for (uint32_t i = 0; i < viewCount; ++i) {
|
|
projectionViews[i].pose = views[i].pose;
|
|
projectionViews[i].fov = views[i].fov;
|
|
projectionViews[i].subImage.swapchain = swapChainResources->handle;
|
|
projectionViews[i].subImage.imageArrayIndex = i;
|
|
projectionViews[i].subImage.imageRect = singleEyeRect;
|
|
}
|
|
|
|
// Use registered CB, inherited from the parent instance.
|
|
// See who called SetRenderCallback() declared above.
|
|
assert(m_renderCallback);
|
|
m_renderCallback(projectionViews, 0);
|
|
m_frame++;
|
|
}
|
|
else {
|
|
throw std::runtime_error("xrWaitSwapchainImage failed!"); // TODO Check handling.
|
|
}
|
|
|
|
XrSwapchainImageReleaseInfo releaseInfo = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
|
|
result = xrReleaseSwapchainImage(swapChainResources->handle, &releaseInfo);
|
|
assert(!XR_FAILED(result));
|
|
}
|
|
|
|
// End Frame
|
|
projectionLayer.space = m_appSpace;
|
|
projectionLayer.viewCount = viewCount;
|
|
projectionLayer.views = projectionViews.data();
|
|
|
|
const XrCompositionLayerBaseHeader* layers[] = { (XrCompositionLayerBaseHeader*)&projectionLayer };
|
|
XrFrameEndInfo endInfo = { XR_TYPE_FRAME_END_INFO };
|
|
endInfo.displayTime = frameState.predictedDisplayTime;
|
|
endInfo.layerCount = 1;
|
|
endInfo.layers = layers;
|
|
endInfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; // XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND;
|
|
|
|
assert(!not_contains(endInfo.environmentBlendMode, m_blendModes));
|
|
|
|
if (invalid_view || !r_isSessionRunning || !frameState.shouldRender) {
|
|
endInfo.layers = nullptr;
|
|
endInfo.layerCount = 0;
|
|
}
|
|
|
|
result = xrEndFrame(m_xrSession, &endInfo);
|
|
|
|
if (m_rdoc_api && capture_xr) m_rdoc_api->EndFrameCapture(m_deviceGfx->virtualDevice, nullptr);
|
|
m_captured_frames++;
|
|
if (m_captured_frames > 3) {
|
|
m_captured_frames = 0;
|
|
capture_xr = false;
|
|
}
|
|
|
|
if (result == XR_SESSION_LOSS_PENDING) {
|
|
printf(std::format("Session loss pending\n").c_str());
|
|
r_running = false;
|
|
}
|
|
|
|
if (result == XR_ERROR_SESSION_NOT_RUNNING) {
|
|
printf(std::format("Session is not running. Pausing frame loop.\n").c_str());
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
}
|
|
|
|
if (XR_FAILED(result)) {
|
|
char errorString[XR_MAX_RESULT_STRING_SIZE];
|
|
result = xrResultToString(m_currentInstance, result, errorString);
|
|
assert(XR_SUCCEEDED(result));
|
|
printf(std::format("xrEndFrame throws: {}\n", errorString).c_str());
|
|
}
|
|
|
|
}
|
|
else {
|
|
printf(std::format("Skipping render.\n").c_str());
|
|
}
|
|
|
|
break; // Not a loop.
|
|
}
|
|
|
|
// --- Cleanup ---
|
|
// Destroy resources here as this is the end of the thread - TODO
|
|
}
|
|
|
|
#endif |