Develop
Develop
Select your platform

Core Concepts

OpenXR is a cross-vendor API that enables app developers to access XR functionality of an XR device through an XR runtime.
This documentation section discusses key concepts regarding native app development using OpenXR. The primary focus is on core OpenXR concepts, along with code snippets.
Important: Studying this conceptual documentation alongside the OpenXR Tutorial by the Khronos Group is highly recommended.
For a detailed look at the formal OpenXR specification, visit the OpenXR 1.0 Specification page.
You can access the detailed API reference documentation and a concise PDF reference guide for getting started.
The OpenXR™ Software Development Kit (SDK) Sources Project includes the hello_xr sample app which demonstrates core OpenXR functionality. To build and run the hello_xr app on Meta Quest devices, follow this guide.

System

In OpenXR, a system consists of related devices and hardware components in the runtime that work together to provide an XR experience.
Using the system concept, you can create a session, accept user input, display frames, and more, without needing to know the specific physical device or implement device-specific logic. The runtime determines which devices to activate or avoid enabling based on the user’s actual usage. Once an XR system is selected, the runtime decides which devices and hardware components are active.

Handles and Atoms

In OpenXR, handles represent runtime-owned objects. Apps manage the lifetime of these objects using create and destroy functions. Handles are hierarchical, with parent handles controlling the validity and lifetime of child handles.
For example, to create an XrActionSet handle, you call xrCreateActionSet and pass an XrInstance handle as a parameter. XrInstance is the parent handle to XrActionSet handles. Similarly, destroying an XrSession also destroys its associated XrSpace and XrSwapchain handles. The following diagram shows the handle type hierarchy for the most significant core (KHR) handle types, along with a non-core handle type given as an example.
OpenXR Handles Hierarchy
OpenXR extensions add new handles, structs, or functions to the OpenXR API. The following table lists the most important handle types in the core OpenXR specification.
Handle typeDescription
XrInstance
Enables apps to use the OpenXR runtime. These handles are the only ones without a parent.
XrSession
Enables interaction with a specific XR device, allows for user input, rendering, managing events, and more. Generally, it is a graphics interaction with a particular XrSystem using a particular graphics binding.
XrAction
Refers to individual actions when retrieving input data or sending haptic events. Apps receive input action states without directly receiving input from the hardware.
XrActionSet
Refers to groups of actions that are valid in a certain context (for example, all actions to interact with a menu UI). Action sets can be enabled or disabled.
XrSwapchain
Manages display of rendered images as a series of frame buffers. Images can be organized in multiple swapchains (queues of images to be displayed to the user).
XrSpace
Represents spaces that the app can reason about through xrLocateSpace.
Unlike handles, atoms do not represent objects. They are coded and fixed numbers representing predefined values in the runtime. Their meaning is only valid within an active instance. The two most important atoms are XrPath and XrSystemID. The XrPath atom refers to a string that corresponds to a fixed numerical value and represents a semantic path. For example, the path /interaction_profiles/oculus/touch_controller represents the Meta Quest Touch Controller interaction profile as a path. The XrSystemId atom represents a value that identifies the user’s system among those that support OpenXR. Both atoms are children of an XrInstance handle.

Passing Function Parameters through Structs

There are two ways of calling functions. The first one is in parameter form. However, many functions pass parameters through structs rather than directly. This allows new extensions to add new parameters to an existing function without changing the function signature. Passing function parameters through structs is useful when these structs store data about extensions or in struct-chaining, that is creating new structs and chaining them to a next pointer. Here is an example that illustrates the idea of struct-chaining.
An XrInstanceCreateInfo struct helps runtimes iterate through a struct pointer chain when creating an instance. It stores, among else, extension names and other instance-related information. This struct type is defined as:
typedef struct XrInstanceCreateInfo {
    XrStructureType          type;
    const void*              next;
    ...
    const char* const*       enabledExtensionNames;
} XrInstanceCreateInfo;
Where:
  • type is an enum value XrStructureType that defines the type of this struct, for example XR_TYPE_INSTANCE_CREATE_INFO which is used in storing info about creating an instance.
  • next is NULL or a pointer to the next struct in a struct-chain.
  • enabledExtensionNames is a pointer to a string array with the names of extensions to enable.
Using structs that include type and next members is essential in struct-chaining.
In Khronos’s hello_xr sample app, first XrInstanceCreateInfo is created and then the instance is created by calling xrCreateInstance (passing the struct and an m_instance instance handle):
XrInstanceCreateInfo createInfo{XR_TYPE_INSTANCE_CREATE_INFO};
createInfo.next = m_platformPlugin->GetInstanceCreateExtension();
...
createInfo.enabledExtensionNames = extensions.data();
...
CHECK_XRCMD(xrCreateInstance(&createInfo, &m_instance));
Where m_platformPlugin is basically a shared pointer to a nested struct of XrBaseInStructure type that provides extensions to XrInstanceCreateInfo (for calling xrCreateInstance). An XrBaseInStructure allows iterating through a read-only struct pointer chain and is defined as:
typedef struct XrBaseInStructure {
    XrStructureType                    type;
    const struct XrBaseInStructure*    next;
} XrBaseInStructure;
Similarly, an XrBaseOutStructure allows iterating through a struct pointer chain that returns data back to the app.
typedef struct XrBaseOutStructure {
    XrStructureType               type;
    struct XrBaseOutStructure*    next;
} XrBaseOutStructure;
Most chained structs are associated with extensions. To use a structure type defined by an extension in a next chain, the proper extension must have been previously enabled during xrCreateInstance. The runtime ignores all unrecognized structs in a next chain (including those that relate to extensions that are not enabled).
Note: Chained output structs may be modified by the runtime with the exception of the type and next members which will be unmodified.

Two-call Idiom

When an OpenXR function that accepts buffer size parameters returns multiple values, it does so by apps performing two calls to the same function. For example, here is an imaginary xrFunction that can be called in parameter form:
XrResult xrFunction(uint32_t capacityInput, uint32_t* countOutput, float* elements);
Where:
  • elements is a pointer to a buffer which stores floats or just nullptr when requesting the buffer size
  • capacityInput is the number of elements in the buffer or 0 when requesting the required buffer size
  • countOutput is a valid pointer to the actual number of elements found in the buffer (independent of the capacityInput and elements parameters)
The first call retrieves the buffer size (the required number of buffer elements) pointed to by elements. In parameter form, the first call to xrFunction must pass:
  • a valid countOutput pointer
  • a nullptr as elements
  • 0 as capacityInput
When capacityInput is 0, the function sets the value pointed to by countOutput to the required size in number of elements and returns XR_SUCCESS, so elements is ignored.
The app can then allocate a buffer large enough to fit a number of elements equal to the value that countOutput points to. So, the second call to xrFunction retrieves the data by passing:
  • a buffer at least as large as the value that countOutput points to
  • a pointer to the allocated buffer as elements
  • the length of the buffer as capacityInput
When capacityInput is:
  • Non-zero but less than required, the function sets the value pointed to by countOutput to the required capacity, and returns XR_ERROR_SIZE_INSUFFICIENT. The data in elements is then undefined.
  • Non-zero and the function returned successfully, the function sets the value pointed to by countOutput to the count of the elements that have been written to the elements buffer.
Note: If in struct form, countOutput can be the actual value, rather than a pointer. Here is how an alternative xrFunction function would look like:
XrResult xrFunction(XrBuffer* buffer);
Where XrBuffer is defined as:
struct XrBuffer {
    uint32_t          capacityInput;
    uint32_t          countOutput;
    float*            elements;
};
If elements is a struct that has type and next fields, you must set next to nullptr or a struct that defines type and next.
Here is an example of how the two-call idiom works in Khronos’s hello_xr sample app to enumerate supported blend modes by a system. A blend mode can be stored as an XrEnvironmentBlendMode enum. Calling xrEnumerateEnvironmentBlendModes twice retrieves information about environment blend modes for a desired system:
uint32_t count;
CHECK_XRCMD(xrEnumerateEnvironmentBlendModes(m_instance, m_systemId, type, 0, &count, nullptr));
...
std::vector<XrEnvironmentBlendMode> blendModes(count);
CHECK_XRCMD(xrEnumerateEnvironmentBlendModes(m_instance, m_systemId, type, count, &count, blendModes.data()));
Variable count stores the number of supported blend modes and vector blendModes stores the supported environment blend modes.
Code snippets in this document belong to the OpenXR specification and the hello_xr sample app which is developed by The Khronos Group Inc. and licensed under the Apache License, Version 2.0.
Did you find this page helpful?
Thumbs up icon
Thumbs down icon