Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion antora/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@
** The OpenXR-Vulkan 1.3 Handshake
*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc[Introduction]
*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc[System Integration]
*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc[Hardware Alignment (LUID)]
*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc[Hardware Alignment]
*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc[Vulkan 1.3 Feature Requirements]
*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc[Incorporating into the Engine]
** Runtime-Owned Swapchains
Expand Down
24 changes: 14 additions & 10 deletions attachments/openxr_engine/renderer_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,9 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer
return false;
}

// In XR mode pick the device BEFORE creating the surface. The OpenXR runtime
// identifies the correct GPU via LUID/UUID, so no surface is needed for selection.
// In XR mode pick the device BEFORE creating the surface. The OpenXR runtime hands us
// the exact VkPhysicalDevice it requires (it enforces this — it's tied to whatever GPU
// the headset's display is wired to), so no surface is needed for selection.
// Creating the GLFW surface first causes the XR runtime's Vulkan layer to intercept
// vkCreateXxxSurfaceKHR before the validation layer sees it, producing an untracked
// surface handle that crashes vkGetPhysicalDeviceSurfaceSupportKHR.
Expand Down Expand Up @@ -753,14 +754,17 @@ bool Renderer::pickPhysicalDevice() {
<< " (Type: " << vk::to_string(deviceProperties.deviceType) << ")" << std::endl;

if (xrMode) {
// Match the LUID provided by OpenXR
auto props2 = _device.getProperties2<vk::PhysicalDeviceProperties2, vk::PhysicalDeviceIDProperties>();
const auto& idProps = props2.get<vk::PhysicalDeviceIDProperties>();

const uint8_t* requiredLuid = xrContext.getRequiredLUID();
if (requiredLuid && std::memcmp(idProps.deviceLUID, requiredLuid, VK_LUID_SIZE) != 0) {
std::cout << " - LUID mismatch for OpenXR" << std::endl;
continue; // Not the right GPU for XR!
// The OpenXR runtime enforces which GPU we must use — it already handed us the
// exact VkPhysicalDevice via xrGetVulkanGraphicsDevice2KHR. Skip anything else;
// there is no matching or scoring left to do, the runtime made the choice for us.
vk::PhysicalDevice required = xrContext.getRequiredPhysicalDevice();
if (static_cast<VkPhysicalDevice>(required) == VK_NULL_HANDLE) {
std::cerr << " - Could not obtain required physical device from OpenXR runtime" << std::endl;
continue;
}
if (*_device != required) {
std::cout << " - Not the GPU OpenXR requires" << std::endl;
continue;
}
}

Expand Down
77 changes: 54 additions & 23 deletions attachments/openxr_engine/xr_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ bool XrContext::createInstance(const std::string& appName) {
}

// Load Vulkan extension functions
xrGetInstanceProcAddr(instance, "xrGetVulkanInstanceExtensionsKHR", (PFN_xrVoidFunction*)&pfnGetVulkanInstanceExtensionsKHR);
xrGetInstanceProcAddr(instance, "xrGetVulkanDeviceExtensionsKHR", (PFN_xrVoidFunction*)&pfnGetVulkanDeviceExtensionsKHR);
xrGetInstanceProcAddr(instance, "xrGetVulkanGraphicsRequirements2KHR", (PFN_xrVoidFunction*)&pfnGetVulkanGraphicsRequirements2KHR);
xrGetInstanceProcAddr(instance, "xrGetVulkanGraphicsDevice2KHR", (PFN_xrVoidFunction*)&pfnGetVulkanGraphicsDevice2KHR);

Expand Down Expand Up @@ -302,15 +304,61 @@ void XrContext::cleanup() {
}
}

const uint8_t* XrContext::getRequiredLUID() {
if (!luidValid && vkInstance && instance != XR_NULL_HANDLE && systemId != XR_NULL_SYSTEM_ID) {
std::vector<const char*> XrContext::getVulkanInstanceExtensions() {
if (instance == XR_NULL_HANDLE) return { "XR_KHR_vulkan_enable2" };
uint32_t size = 0;
if (!pfnGetVulkanInstanceExtensionsKHR) return {};
pfnGetVulkanInstanceExtensionsKHR(instance, systemId, 0, &size, nullptr);
std::vector<char> buffer(size);
pfnGetVulkanInstanceExtensionsKHR(instance, systemId, size, &size, buffer.data());

static std::vector<std::string> extStrings;
extStrings.clear();
std::string extensions(buffer.data());
std::istringstream iss(extensions);
std::string ext;
while (iss >> ext) extStrings.push_back(ext);

static std::vector<const char*> extPtrs;
extPtrs.clear();
for (const auto& s : extStrings) extPtrs.push_back(s.c_str());
return extPtrs;
}

std::vector<const char*> XrContext::getVulkanDeviceExtensions(vk::PhysicalDevice physicalDevice) {
if (instance == XR_NULL_HANDLE) return { "VK_KHR_external_memory", "VK_KHR_external_semaphore" };
uint32_t size = 0;
if (!pfnGetVulkanDeviceExtensionsKHR) return {};
pfnGetVulkanDeviceExtensionsKHR(instance, systemId, 0, &size, nullptr);
std::vector<char> buffer(size);
pfnGetVulkanDeviceExtensionsKHR(instance, systemId, size, &size, buffer.data());

static std::vector<std::string> devExtStrings;
devExtStrings.clear();
std::string extensions(buffer.data());
std::istringstream iss(extensions);
std::string ext;
while (iss >> ext) devExtStrings.push_back(ext);

static std::vector<const char*> devExtPtrs;
devExtPtrs.clear();
for (const auto& s : devExtStrings) devExtPtrs.push_back(s.c_str());
return devExtPtrs;
}

vk::PhysicalDevice XrContext::getRequiredPhysicalDevice() {
if (!requiredPhysicalDeviceQueried && vkInstance && instance != XR_NULL_HANDLE && systemId != XR_NULL_SYSTEM_ID) {
requiredPhysicalDeviceQueried = true;

// Step 1: Call graphics requirements as mandated by spec before getting graphics device
XrGraphicsRequirementsVulkanKHR graphicsRequirements{XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR};
if (pfnGetVulkanGraphicsRequirements2KHR) {
pfnGetVulkanGraphicsRequirements2KHR(instance, systemId, &graphicsRequirements);
}

// Step 2: Get the physical device from OpenXR
// Step 2: Ask the runtime which physical device to use. The runtime enforces this
// choice (it is tied to the display the headset is connected to), so the returned
// handle IS the device to use — there is nothing left to match or search for.
VkPhysicalDevice vkPhysicalDevice = VK_NULL_HANDLE;
XrResult result = XR_ERROR_FUNCTION_UNSUPPORTED;

Expand All @@ -322,30 +370,13 @@ const uint8_t* XrContext::getRequiredLUID() {
}

if (result == XR_SUCCESS && vkPhysicalDevice != VK_NULL_HANDLE) {
// Step 3: Extract LUID from the physical device
VkPhysicalDeviceIDProperties idProps{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES};
idProps.pNext = nullptr;
VkPhysicalDeviceProperties2 props2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
props2.pNext = &idProps;

auto pfnGetPhysicalDeviceProperties2 = (PFN_vkGetPhysicalDeviceProperties2)vkGetInstanceProcAddr((VkInstance)vkInstance, "vkGetPhysicalDeviceProperties2");
if (pfnGetPhysicalDeviceProperties2) {
pfnGetPhysicalDeviceProperties2(vkPhysicalDevice, &props2);
if (idProps.deviceLUIDValid) {
std::memcpy(requiredLuid, idProps.deviceLUID, VK_LUID_SIZE);
luidValid = true;
std::cout << "XrContext: Required LUID found and stored." << std::endl;
} else {
std::cout << "XrContext: Physical device LUID is not valid." << std::endl;
}
} else {
std::cerr << "XrContext: Failed to load vkGetPhysicalDeviceProperties2" << std::endl;
}
requiredPhysicalDevice = vkPhysicalDevice;
std::cout << "XrContext: Required Vulkan physical device obtained from OpenXR runtime." << std::endl;
} else {
std::cerr << "XrContext: Failed to get Vulkan graphics device from OpenXR (XrResult=" << result << ")" << std::endl;
}
}
return luidValid ? requiredLuid : nullptr;
return vk::PhysicalDevice(requiredPhysicalDevice);
}

vk::Extent2D XrContext::getRecommendedExtent() const {
Expand Down
12 changes: 9 additions & 3 deletions attachments/openxr_engine/xr_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ class XrContext {
#endif

// Core Handshake (Chapter 2)
const uint8_t* getRequiredLUID();
std::vector<const char*> getVulkanInstanceExtensions();
std::vector<const char*> getVulkanDeviceExtensions(vk::PhysicalDevice physicalDevice);
// The runtime hands back the exact VkPhysicalDevice it wants us to use — no LUID
// matching required. Returns VK_NULL_HANDLE if the query fails.
vk::PhysicalDevice getRequiredPhysicalDevice();

// Swapchain Management (Chapter 3 & 8)
vk::Extent2D getRecommendedExtent() const;
Expand Down Expand Up @@ -116,6 +120,8 @@ class XrContext {
static bool checkRuntimeAvailable();

private:
PFN_xrGetVulkanInstanceExtensionsKHR pfnGetVulkanInstanceExtensionsKHR = nullptr;
PFN_xrGetVulkanDeviceExtensionsKHR pfnGetVulkanDeviceExtensionsKHR = nullptr;
PFN_xrGetVulkanGraphicsRequirements2KHR pfnGetVulkanGraphicsRequirements2KHR = nullptr;
PFN_xrGetVulkanGraphicsDevice2KHR pfnGetVulkanGraphicsDevice2KHR = nullptr;

Expand All @@ -127,8 +133,8 @@ class XrContext {
bool sessionBegun = false;
XrReferenceSpaceType referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE;

uint8_t requiredLuid[VK_LUID_SIZE] = {0};
bool luidValid = false;
VkPhysicalDevice requiredPhysicalDevice = VK_NULL_HANDLE;
bool requiredPhysicalDeviceQueried = false;

#if defined(PLATFORM_ANDROID)
struct android_app* androidApp = nullptr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Before we can render a single pixel to an XR headset, we must first establish a
In this chapter, we are going to look at the three pillars of a successful spatial handshake:

1. **System Integration**: How to extend our engine's `VulkanContext` to support the mandatory OpenXR extensions, specifically `XR_KHR_vulkan_enable2`.
2. **Hardware Alignment**: Utilizing the Locally Unique Identifier (**LUID**) to ensure that both OpenXR and Vulkan are talking to the exact same physical GPU. This is critical for cross-process memory visibility and performance.
2. **Hardware Alignment**: Using the exact `VkPhysicalDevice` handle the OpenXR runtime hands back from `xrGetVulkanGraphicsDevice2KHR` to ensure both OpenXR and Vulkan are talking to the same physical GPU. The runtime enforces this choice, so there is no matching to do—just use the handle it gives you. This is critical for cross-process memory visibility and performance.
3. **Vulkan 1.3 Requirements**: Activating the modern features—like Timeline Semaphores, Dynamic Rendering, and Synchronization 2—that allow our spatial pipeline to operate with minimal latency.

== The Concept of the Handshake
Expand Down
Original file line number Diff line number Diff line change
@@ -1,73 +1,78 @@
:pp: {plus}{plus}
= Hardware Alignment (LUID)
= Hardware Alignment

In many modern computing environments, especially high-end gaming desktops, it is common to have multiple GPUs—such as an integrated GPU on the processor and a dedicated high-performance card. For spatial computing, it is absolutely critical that both our application and the OpenXR runtime are using the exact same physical hardware.

== Ecosystem Perspective

This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**.

Aligning your Vulkan physical device with the XR runtime's preferred hardware is a mandatory step to ensure high-performance resource sharing. Using the LUID (Locally Unique Identifier) ensures that both systems are talking to the same physical silicon.
Aligning your Vulkan physical device with the XR runtime's preferred hardware is a mandatory step to ensure high-performance resource sharing. Critically, this is not a negotiation: the OpenXR runtime *enforces* which GPU you must use, and it hands you that exact device directly. There is no matching or searching to do.

== The "PCIe Tax": Why Alignment is Mandatory

To understand why we care about the LUID, we must consider the cost of data movement. The PCIe bus is the highway between your CPU, system RAM, and your GPUs. While it is very fast, it is still orders of magnitude slower than the internal memory bus of a modern GPU (VRAM).
To understand why we care about hardware alignment, we must consider the cost of data movement. The PCIe bus is the highway between your CPU, system RAM, and your GPUs. While it is very fast, it is still orders of magnitude slower than the internal memory bus of a modern GPU (VRAM).

* **Display Discovery**: The runtime has negotiated with the operating system (e.g., via DXGI on Windows) to identify which physical GPU is electrically connected to the headset's display.
* **Avoiding Cross-GPU Copies**: If our engine renders a frame on GPU A, but the headset is connected to GPU B, the OS would be forced to copy that image across the **PCIe** bus. This adds several milliseconds of latency—potentially half of your frame budget for a 90Hz headset.
* **Internal VRAM Efficiency**: Moving an image within the same GPU is almost instantaneous. By pinning the application to the same GPU, the runtime ensures your frames stay within high-speed VRAM at all times.

== What is an LUID?

An **LUID** (Locally Unique Identifier) is a 64-bit value guaranteed to be unique on the local machine until the next reboot. Unlike a **UUID**, which is persistent across machines, the LUID is a transient hardware handle provided by the operating system.

In the context of the OpenXR-Vulkan handshake, the LUID serves as the hardware "fingerprint" of the GPU. OpenXR tells us: "I am currently talking to the GPU with this specific LUID," and we must search through our available `VkPhysicalDevice` handles until we find the one that matches.
Because getting this wrong silently costs performance (or breaks resource sharing outright), OpenXR does not leave device selection up to the application at all—it tells you exactly which `VkPhysicalDevice` to use.

== Querying the XR Device

Once we have initialized our `xr::Instance`, we can query the specific hardware identity that the runtime expects us to use. We do this by calling `xrGetVulkanGraphicsDevice2KHR`.
Once we have initialized our `xr::Instance`, we can query the specific `VkPhysicalDevice` that the runtime requires us to use. We do this by calling `xrGetVulkanGraphicsDevice2KHR` (or the older `xrGetVulkanGraphicsDeviceKHR`).

[source,cpp]
----
VkPhysicalDevice xrRequiredDevice;
xrGetVulkanGraphicsDevice2KHR(xrInstance, systemId, *instance, &xrRequiredDevice);

XrVulkanGraphicsDeviceGetInfoKHR getInfo{XR_TYPE_VULKAN_GRAPHICS_DEVICE_GET_INFO_KHR};
getInfo.systemId = systemId;
getInfo.vulkanInstance = instance;

xrGetVulkanGraphicsDevice2KHR(xrInstance, &getInfo, &xrRequiredDevice);
----

While we can get the raw handle directly from the runtime, matching via LUID is often more robust, especially when our engine's architecture abstracts physical device selection or when multiple Vulkan instances are present.
This handle is the answer. The runtime has already resolved display topology, driver adapter enumeration, and cross-process sharing requirements on your behalf—that is precisely what this call is for. Your job is simply to use `xrRequiredDevice` as your `VkPhysicalDevice`, not to re-derive it by some other means.


== Matching the LUID in Vulkan
== Using the Handle Directly

To find the LUID of a Vulkan physical device, we query the `VkPhysicalDeviceIDProperties` structure. In the `vulkan-hpp` RAII world, we can retrieve this by chaining structures in a `getProperties2` call.
In the `vulkan-hpp` RAII world, you don't need to enumerate physical devices at all in XR mode—you already have the handle the runtime requires. Wrap it directly:

[source,cpp]
----
// Iterating through physical devices to find a match using vulkan-hpp RAII
for (const auto& physicalDevice : instance.enumeratePhysicalDevices()) {
auto props2 = physicalDevice.getProperties2<vk::PhysicalDeviceProperties2, vk::PhysicalDeviceIDProperties>();
const auto& idProps = props2.get<vk::PhysicalDeviceIDProperties>();

if (idProps.deviceLUIDValid) {
// Compare the 8-byte LUID against the target from OpenXR
if (std::memcmp(idProps.deviceLUID, targetLUID, VK_LUID_SIZE) == 0) {
return physicalDevice;
}
// The runtime enforces this choice; we don't search for it, we're told it.
vk::PhysicalDevice requiredDevice(xrRequiredDevice);
----

If you still want a `vk::raii::PhysicalDevice` (for example, to reuse suitability-checking code that expects one), find the matching entry by comparing the raw handle—not any derived identifier:

[source,cpp]
----
for (auto& physicalDevice : instance.enumeratePhysicalDevices()) {
if (*physicalDevice == requiredDevice) {
return physicalDevice;
}
}
----

This is a simple handle comparison, not a hardware "fingerprint" match—the runtime already told us which device this is.

== Cross-Process Memory Visibility

The LUID isn't just for selection; it is the foundation of **Cross-Process Memory Visibility**. Because the XR runtime usually lives in a separate process from our engine, the images we render must be shared across process boundaries.
Using the runtime-provided device is the foundation of **Cross-Process Memory Visibility**. Because the XR runtime usually lives in a separate process from our engine, the images we render must be shared across process boundaries.

By aligning our hardware selection via the LUID, we guarantee that the "Wait-Acquire-Release" cycle can happen entirely on-device, without expensive CPU-side synchronization or system memory copies.
By using the exact `VkPhysicalDevice` the runtime requires, we guarantee that the "Wait-Acquire-Release" cycle can happen entirely on-device, without expensive CPU-side synchronization or system memory copies.

== Advanced: Multi-GPU Load Balancing and Device Groups

While an OpenXR session is typically tied to the single GPU connected to the display, Vulkan allows us to go further:

* **Vulkan Device Groups**: You can use `VK_KHR_device_group` to treat multiple GPUs as a single logical device. This allows you to use a secondary GPU for heavy compute tasks (like physics or scene decomposition) and aggregate the results on the primary GPU before handing the final frame to the XR compositor.
* **Handling Hardware Changes**: If the hardware configuration changes (e.g., an external GPU is disconnected), you can use Vulkan's hardware abstraction to detect these changes and prompt the user for a clean engine restart, ensuring that the hardware LUID handshake remains valid.
* **Handling Hardware Changes**: If the hardware configuration changes (e.g., an external GPU is disconnected), you can use Vulkan's hardware abstraction to detect these changes and prompt the user for a clean engine restart. On restart, simply re-query `xrGetVulkanGraphicsDevice2KHR`—the runtime will hand you whatever device is now correct.

TIP: For more information on hardware alignment, check out the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#VkPhysicalDeviceIDProperties[Vulkan Specification on Device IDs], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series].
TIP: For more information on hardware alignment, check out the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xrspec.html#xrGetVulkanGraphicsDevice2KHR[OpenXR Specification on xrGetVulkanGraphicsDevice2KHR], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series].

xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc[Next]
Loading
Loading