1 Introduction

1.1 Goal of OpenXR

OpenXR aims to help solve the fragmentation of the XR ecosystem. Before the advent of OpenXR, software developers working with multiple hardware platforms had to write different code paths for each platform to address the different hardware. Each platform had its own, often proprietary, API, and deploying an existing application to a new platform required a lot of adaptation. Developing a new application for a new platform was even more challenging. Documentation for the OpenXR 1.0 Core Specification can be found https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html.

Despite of their unique features, the platforms had a great deal in common. For example, most headsets had a main view seen from two slightly different perspectives. Most had a way to track the user’s head and hands or hand controllers. Most had buttons, many had analogue controls like triggers or joysticks and many had haptic feedback.

XR Fragmentation

OpenXR provides a common interface to reduce XR fragmentation.

OpenXR aims to solve this problem by providing a common API to address XR hardware, in reading its inputs and outputting to its displays and haptic systems. Just as OpenGL and Vulkan provide a common API to access graphics hardware, so OpenXR allows you to write code that works with multiple XR platforms, with minimal adaptation.

1.2 Overview

We’ll start with the main concepts you’ll need to be familiar with around OpenXR.

OpenXR Concepts

Concept

Description

API

The OpenXR API is the set of commands, functions and structures that an OpenXR-compliant runtime is required to offer.

Application

The Application is your program, called an “app” for short.

Runtime

A Runtime is a specific implementation of the OpenXR functionality. It might be provided by a hardware vendor, as part of a device’s operating system; it might be supplied by a software vendor to enable OpenXR support with a specific range of hardware. The Loader finds the appropriate Runtime and loads it when OpenXR is initialized.

Loader

The OpenXR Loader is a special library that connects your application to whichever OpenXR Runtime you’re using. The Loader’s job is to find the Runtime and initialize it, then allow your application to access the Runtime’s version of the API. Some devices can have multiple Runtimes available, but only one can be active at any given time.

Layers

API layers are optional components that augment an OpenXR system. A Layer might help with debugging, or filter information between the application and the Runtime. API layers are selectively enabled when the OpenXR Instance is created.

Instance

The Instance is the foundational object that allows your application to communicate with a Runtime. You’ll ask OpenXR to create an Instance when initializing XR support in your application.

Graphics

OpenXR needs to connect to a graphics API in order to render the headset views. Which Graphics APIs are supported depends on the Runtime and the hardware.

Input/Output

OpenXR allows apps to query what inputs and outputs are available. These can then be bound to Actions, so the app knows what the user is doing.

Action

A semantically-defined input or output for the app, which can be bound to different hardware inputs or outputs using Bindings.

Binding

A mapping from hardware/Runtime-defined inputs and outputs to semantic Actions.

Pose

A position and orientation in 3D space.

OpenXR provides a clear and precise common language for developers and hardware vendors to use.

An OpenXR Runtime implements the OpenXR API. The runtime translates the OpenXR function calls into something that the vendor’s software/hardware can understand.

The OpenXR Loader finds and loads a suitable OpenXR runtime that is present on the system. The Loader will load in all of the OpenXR function pointers stated in the core specification for the application to use. If you are using an extension, such as XR_EXT_debug_utils, any functions associated with that extension will need to be loaded in with xrGetInstanceProcAddr. Some platforms like Android require extra work and information to initialize the loader. Documentation for the OpenXR Loader can be found https://registry.khronos.org/OpenXR/specs/1.0/loader.html.

API Layers are additional code layers that are inserted by the loader between the application and the runtime. Each of these API layers intercepts the OpenXR function calls from the layer above, does something with that function, and then calls the next layer down. Examples of API Layers would be: logging the OpenXR functions to the output or a file; creating trace files of the OpenXR calls for later replay; or for checking that the function calls made to OpenXR are compatible with the OpenXR specification. See Chapter 6.3.

OpenXR supports multiple graphics APIs via its extension functionality. OpenXR can extend its functionality to include debugging layers, vendor hardware and software support and graphics APIs. This idea of absolving the core specification of the graphics API functionality provides flexibility in choosing the graphics APIs now and in the future. OpenXR is targeted at developing XR experiences and isn’t concerned with the specifics of any graphics APIs. The extensible nature of OpenXR allows revisions of existing APIs and new graphics APIs to be integrated with ease. See Chapter 5.

OpenXR recognizes that there is a vast and ever-changing array of hardware and configurations in the XR space. With new headsets and controllers coming to the market, an abstraction of the input system was needed so that the same applications can target different and newer hardware with minimal change. This is the core reasoning behind the OpenXR Actions System.

1.3 Environment Setup

This section will help you set up your development environment. Here your choice of platform makes a difference, but afterwards, things will be much more consistent. You can change platform and graphics API at any time by clicking the tabs at the top of the page. Select the platform you want to develop for now, by clicking the appropriate tab above.

When building for Android, you can use Microsoft Windows, Linux or Apple macOS as the host platform to run Android studio.

Android Studio

Install Android Studio from this location: https://developer.android.com/studio.

Vulkan is included as part of the NDK provided by Google and is supported on Android 7.0 (Nougat), API level 24 or higher (see https://developer.android.com/ndk/guides/graphics).

You will need an Android device that supports at least Vulkan 1.0 for this tutorial.

1.4 Project Setup

This section explains how to set up your project ready for Chapter 2 and will make references to the /Chapter2 folder. It explains how to include the OpenXR headers, link the openxr_loader library, graphics API integration, and other boilerplate code and finally create a simple stub application which will be expanded on in later chapters.

1.4.1 CMake and Project Files

For a quick setup download this ``.zip`` archive:

AndroidBuildFolder.zip

First, create a workspace folder and copy the downloaded zip archive into that folder. Unzip the archive in place and rename the AndroidBuildFolder folder to Chapter2. You can delete the used zip archive as it’s no longer needed. Open Android Studio and then open the Chapter2 foler that was created.

Now follow instructions under the CMake and CMakeLists.txt headings of this sub-chapter and then go straight on to 1.4.2 Common Files.

For a detailed explantion of the Android build folder set up continue below:

Android Studio

Here, We’ll show how to hand build an Android Studio project that runs a C++ Native Activity. First, we will create a workspace folder and in that folder create a subdirectory called /Chapter2. Open Android Studio, select New Project and choose an Empty Views Activity (Android Studio 22+) or an Empty Activity (Android Studio up to version 21).

Android Studio - New Project - Empty View Activity.

Set the Name to ‘OpenXR Tutorial Chapter 2’, the Package name to org.khronos.openxrtutorialchapter2 and save location to that Chapter2 folder. The language can be ignored here as we are using C++, and we can set the Minimum SDK to API 24: Android 7.0 (Nougat) or higher. If a “Build Configuration Language” option is shown, set this to Groovy DSL (build.gradle). Click “Finish” to complete the set up.

Android Studio - New Project - options.

With the Android Studio project now set up, we need to modify some of the files and folders to support the C++ Native Activity.

In Android Studio, switch to the “Project” view in the “Project” tab (on the top right of the default Android Studio layout).

Under the app folder in Chapter2, you can delete the libs folder, and under the app/src you can also delete the androidTest and test folders. Finally under app/src/main, delete the java folder. Under the app/src/main/res, delete the layout, values-night and xml folders. Under the values folder, delete colors.xml and themes.xml.

Now sync the project by selecting “File > Sync Project with Gradle” on the menu.

Gradle Sync

CMake

Create a folder called cmake in the workspace directory. Download the linked file below and put it in cmake. This will be used by CMake to help build our project. Files with shader in the name will be used in later chapters.

glsl_shader.cmake

Create a text file called CMakeLists.txt in the Chapter2 directory. We will use this file to specify how our Native C++ code will be built. This file will be invoked by Android Studio’s Gradle build system.

CMakeLists.txt

Add the following to your new CMakeLists.txt file:

cmake_minimum_required(VERSION 3.22.1)
set(PROJECT_NAME OpenXRTutorialChapter2)
project("${PROJECT_NAME}")

Here we have declared our tutorial project. At least CMake 3.22.1 is needed to use all the features in the tutorial. Now add:

# Additional Directories for find_package() to search within.
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake")
# For FetchContent_Declare() and FetchContent_MakeAvailable()
include(FetchContent)

# openxr_loader - From github.com/KhronosGroup
set(BUILD_TESTS
    OFF
    CACHE INTERNAL "Build tests"
)
set(BUILD_API_LAYERS
    ON
    CACHE INTERNAL "Use OpenXR layers"
)
FetchContent_Declare(
    OpenXR
    URL_HASH MD5=81930f0ccecdca852906e1a22aee4a45
    URL https://github.com/KhronosGroup/OpenXR-SDK-Source/archive/refs/tags/release-1.0.28.zip
        SOURCE_DIR
        openxr
)
FetchContent_MakeAvailable(OpenXR)

After setting our CMake version, our own CMake variable PROJECT_NAME to OpenXRTutorialChapter2 and with that variable setting the project’s name, we append to the CMAKE_MODULE_PATH variable an additional path for find_package() to search within and we include FetchContent and use it to get the OpenXR-SDK-Source from Khronos’s GitHub page.

# Files
set(SOURCES
        "main.cpp"
        "../Common/GraphicsAPI.cpp"
        "../Common/GraphicsAPI_Vulkan.cpp"
        "../Common/OpenXRDebugUtils.cpp")
set(HEADERS
        "../Common/DebugOutput.h"
        "../Common/GraphicsAPI.h"
        "../Common/GraphicsAPI_Vulkan.h"
        "../Common/HelperFunctions.h"
        "../Common/OpenXRDebugUtils.h"
        "../Common/OpenXRHelper.h")

Here, we include all the files needed for our project. All files with ../Common/*.* are available to download from this tutorial website. Below are the links and discussion of their usage within this tutorial and with OpenXR. This tutorial includes all the graphics APIs header and cpp files; you only need to download the files pertaining to your graphics API choice.

Add the following to CMakeLists.txt:

add_library(${PROJECT_NAME} SHARED ${SOURCES} ${HEADERS})
target_include_directories(${PROJECT_NAME} PRIVATE 
    # In this repo
    ../Common/
    # From OpenXR repo
    "${openxr_SOURCE_DIR}/src/common"
    "${openxr_SOURCE_DIR}/external/include"
)

# export ANativeActivity_onCreate for java to call.
set_property(
    TARGET ${PROJECT_NAME}
    APPEND_STRING
    PROPERTY LINK_FLAGS " -u ANativeActivity_onCreate"
)

# native_app_glue
include(AndroidNdkModules)
android_ndk_import_module_native_app_glue()

target_link_libraries(${PROJECT_NAME} android native_app_glue openxr_loader)
target_compile_options(${PROJECT_NAME} PRIVATE -Wno-cast-calling-convention)

We have added a library with the ${SOURCES} and ${HEADERS} and have added the ../Common, "${openxr_SOURCE_DIR}/src/common" and "${openxr_SOURCE_DIR}/external/include" folders as include directories. We have set the LINK_FLAGS for our project with the flag -u ANativeActivity_onCreate() to support C++ native code. This is used by a static library called native_app_glue, which connects the Java Virtual Machine and our C++ code. Ultimately, it allows us to use the android_main() entry point. We add native_app_glue to our project by including AndroidNdkModules and calling:

android_ndk_import_module_native_app_glue().

Now, we link the android, native_app_glue and openxr_loader libraries to our OpenXRTutorialChapter2 library. Our libOpenXRTutorialChapter2 .so will be packaged inside our .apk along with any shared libraries that we have linked. We also add -Wno-cast-calling-convention to the compiler option to allow the casting of calling conversions for function pointers.

Now, add:

# VulkanNDK
find_library(vulkan-lib vulkan)
if (vulkan-lib)
    target_include_directories(${PROJECT_NAME} PUBLIC ${ANDROID_NDK}/sources/third_party/vulkan/src/include)
    target_link_libraries(${PROJECT_NAME} ${vulkan-lib})
    target_compile_definitions(${PROJECT_NAME} PUBLIC XR_TUTORIAL_USE_VULKAN)
endif()

Here we find the Vulkan library in the NDK. We include the directory to the Android Vulkan headers and link against the libvulkan.so library. We’ve added the XR_TUTORIAL_USE_VULKAN compiler definition to specify which graphics APIs should be supported and have their headers included in GraphicsAPI.h.

AndroidManifest.xml

Replace the file ‘app/src/main/AndroidManifest.xml’ with the following:

Download AndroidManifest.xml. You can open the file. You don’t need to edit it, but note:

  • We added a <uses-feature> to require OpenGL ES 3.2 and Vulkan 1.0.3 support.

  • Next, we added android.hardware.vr.headtracking to specify that the application works with 3DOF or 6DOF and on devices that are not all-in-ones. It’s set to false so as to allow greater compatibility across devices.

  • Finally, we updated the <intent-filter> to tell the application that it should take over rendering when active, rather than appearing in a window. We set:

<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />

Note: not all devices yet support this category. For example, for Meta Quest devices you will need

<category android:name="com.oculus.intent.category.VR" />

The code shows both the ‘Standard Khronos OpenXR’ and ‘Meta Quest-specific non-standard’ ways of setting the intent filter. If you’re building for another Android-based XR device which does not support all of the standard commands used here, you may need to look up the appropriate commands and modify the manifest.

Gradle

Now download app/build.gradle and replace the existing file app/build.gradle.

In the dependencies section we have added:

implementation 'org.khronos.openxr:openxr_loader_for_android:...'

This provides an AndroidManifest.xml that will be merged into our own, setting some required properties for the package and application. We are still required to add to our own AndroidManifest.xml file with relevant intent filters, such as org.khronos.openxr.intent.category.IMMERSIVE_HMD. It also provides the OpenXR headers and library binaries in a format that the Android Gradle Plugin will expose to CMake.

Now download build.gradle and replace the existing file in the Chapter2 folder.

1.4.2 Common Files

Create a folder called Common in the workspace directory. Download each of the linked files below and put them in Common:

Or, you can download the zip archive containing all the required files. Extract the archive to get the Common folder.

DebugOutput

DebugOutput is a class that redirects std::cout and std::cerr to the output window in your IDE.

DebugOutput uses __android_log_write() to log the message to Android Logcat.

HelperFunctions

This is a simple header file for boilerplate code for the various platforms. It includes various C/C++ standard header and the code that defines the macro DEBUG_BREAK, according to which platform we’re building for. This macro will stop the execution of your program when an error occurs, so you can see where it happened and fix it. We use this macro in the OpenXRMessageCallbackFunction() function, which is discussed in detail in Chapter 2.1. IsStringInVector() and BitwiseCheck() are just simple wrappers over commonly used code. IsStringInVector() checks if a const char * is in a std::vector<const char *> by using strcmp(), and BitwiseCheck() checks if a bit is set in a bitfield.

OpenXRDebugUtils

A header and cpp file pair that helps in setting up the DebugUtilsMessenger. XR_EXT_debug_utils is an OpenXR instance extension that can intercept calls made to OpenXR and provide extra information or report warnings and errors, if the usage of the API or the current state of OpenXR is not valid. As you go through this tutorial it is highly recommended to have this enabled to help with debugging. This is discussed in detail in Chapter 2.1, but in general, CreateOpenXRDebugUtilsMessenger() creates and DestroyOpenXRDebugUtilsMessenger() destroys an XrDebugUtilsMessengerEXT. OpenXRMessageCallbackFunction() is a callback function that is specified at object creation, which is called when OpenXR raises an issue. The header declares the functions and the cpp defines them.

OpenXRHelper

A header for including all the needed header files and helper functions. Looking inside this file, we can see:

// Define any XR_USE_PLATFORM_... / XR_USE_GRAPHICS_API_... before this header file.

// OpenXR Headers
#include <openxr/openxr.h>
#include <openxr/openxr_platform.h>

Here, we include the main OpenXR header file openxr.h and the OpenXR platform header file openxr_platform.h. For the OpenXR platform header file, note the comment about using the preceding XR_USE_PLATFORM_... and XR_USE_GRAPHICS_API_... macros. When enabled, we gain access to functionality that interacts with the chosen graphics API and/or platform. These macros are automatically set by GraphicsAPI.h

This header also defines the macro OPENXR_CHECK. Many OpenXR functions return an XrResult. This macro will check if the call has failed and will log a message to std::cerr. This can be modified to suit your needs. There are two additional functions GetXRErrorString() and OpenXRDebugBreak(), which are used to convert the XrResult to a string and as a breakpoint function respectively.

1.4.3 Main.cpp and the OpenXRTutorial Class

Now, create a text file called main.cpp in the Chapter2 folder. Open main.cpp and add the following:

#include <DebugOutput.h>

Next, we add the GraphicsAPI_....h header to include the GraphicsAPI code of your chosen graphics API. This will in turn include GraphicsAPI.h, HelperFunctions.h and OpenXRHelper.h.

#include <GraphicsAPI_Vulkan.h>

You can also include OpenXRDebugUtils.h to help with the set-up of XrDebugUtilsMessengerEXT.

#include <OpenXRDebugUtils.h>

Now we will define the main class OpenXRTutorial of the application. It’s just a stub class for now, with an empty Run() method. Add the following to main.cpp:

class OpenXRTutorial {
public:
        OpenXRTutorial(GraphicsAPI_Type apiType)
        {
        }
        ~OpenXRTutorial() = default;
        void Run()
        {
        }
private:
        void PollSystemEvents()
        {
        }
private:
        bool m_applicationRunning = true;
        bool m_sessionRunning = false;
};

Note here that for some platforms, we need additional functionality provided via the PollSystemEvents() method, so that our application can react to any relevant updates from the platform correctly.

The PollSystemEvents() method is outside the scope of OpenXR, but in general it will poll Android for system events, updates and uses the AndroidAppState, m_applicationRunning and m_sessionRunning members, which we describe later in this chapter.

We’ll add the main function for the application. It will look slightly different, depending on your chosen platform. We first create a ‘pseudo-main function’ called OpenXRTutorial_Main(), in which we create an instance of our OpenXRTutorial class, taking a GraphicsAPI_Type parameter, and call the Run() method. GraphicsAPI_Type can be changed to suit the graphics API that you have chosen.

void OpenXRTutorial_Main(GraphicsAPI_Type apiType) {
    DebugOutput debugOutput;  // This redirects std::cerr and std::cout to the IDE's output or Android Studio's logcat.
    XR_TUT_LOG("OpenXR Tutorial Chapter 2");
    OpenXRTutorial app(apiType);
    app.Run();
}

Then, we create the actual platform-specific main function (our entry point to the application), which will call OpenXRTutorial_Main() with our GraphicsAPI_Type parameter. This must be changed to match on your chosen graphics API, one of: D3D11, D3D12, OPENGL, OPENGL_ES, or VULKAN.

android_app *OpenXRTutorial::androidApp = nullptr;
OpenXRTutorial::AndroidAppState OpenXRTutorial::androidAppState = {};

void android_main(struct android_app *app) {
    // Allow interaction with JNI and the JVM on this thread.
    // https://developer.android.com/training/articles/perf-jni#threads
    JNIEnv *env;
    app->activity->vm->AttachCurrentThread(&env, nullptr);

    // https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#XR_KHR_loader_init
    // Load xrInitializeLoaderKHR() function pointer. On Android, the loader must be initialized with variables from android_app *.
    // Without this, there's is no loader and thus our function calls to OpenXR would fail.
    XrInstance m_xrInstance = XR_NULL_HANDLE;  // Dummy XrInstance variable for OPENXR_CHECK macro.
    PFN_xrInitializeLoaderKHR xrInitializeLoaderKHR = nullptr;
    OPENXR_CHECK(xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR", (PFN_xrVoidFunction *)&xrInitializeLoaderKHR), "Failed to get InstanceProcAddr for xrInitializeLoaderKHR.");
    if (!xrInitializeLoaderKHR) {
        return;
    }

    // Fill out an XrLoaderInitInfoAndroidKHR structure and initialize the loader for Android.
    XrLoaderInitInfoAndroidKHR loaderInitializeInfoAndroid{XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR};
    loaderInitializeInfoAndroid.applicationVM = app->activity->vm;
    loaderInitializeInfoAndroid.applicationContext = app->activity->clazz;
    OPENXR_CHECK(xrInitializeLoaderKHR((XrLoaderInitInfoBaseHeaderKHR *)&loaderInitializeInfoAndroid), "Failed to initialize Loader for Android.");

    // Set userData and Callback for PollSystemEvents().
    app->userData = &OpenXRTutorial::androidAppState;
    app->onAppCmd = OpenXRTutorial::AndroidAppHandleCmd;

    OpenXRTutorial::androidApp = app;

And we will initialize the app with your chosen API:

    OpenXRTutorial_Main(VULKAN);
}

Before we can use OpenXR for Android, we need to initialize the loader based the application’s context and virtual machine. We retrieve the function pointer to xrInitializeLoaderKHR, and with the XrLoaderInitInfoAndroidKHR filled out, call that function to initialize OpenXR for our use. At this point, we also attach the current thread to the Java Virtual Machine. We assign our AndroidAppState static member and our AndroidAppHandleCmd() static method to the android_app * and save it to a static member in the class.

Android requires a few extra additions to the OpenXRTutorial class. Namely, android_app *, AndroidAppState and AndroidAppHandleCmd, which are used in getting updates from the Android Operating System and keep our application functioning. Add the following code after the definition of Run() in your OpenXRTutorial class declaration:

public:
    // Stored pointer to the android_app structure from android_main().
    static android_app *androidApp;

    // Custom data structure that is used by PollSystemEvents().
    // Modified from https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/d6b6d7a10bdcf8d4fe806b4f415fde3dd5726878/src/tests/hello_xr/main.cpp#L133C1-L189C2
    struct AndroidAppState {
        ANativeWindow *nativeWindow = nullptr;
        bool resumed = false;
    };
    static AndroidAppState androidAppState;

    // Processes the next command from the Android OS. It updates AndroidAppState.
    static void AndroidAppHandleCmd(struct android_app *app, int32_t cmd) {
        AndroidAppState *appState = (AndroidAppState *)app->userData;

        switch (cmd) {
        // There is no APP_CMD_CREATE. The ANativeActivity creates the application thread from onCreate().
        // The application thread then calls android_main().
        case APP_CMD_START: {
            break;
        }
        case APP_CMD_RESUME: {
            appState->resumed = true;
            break;
        }
        case APP_CMD_PAUSE: {
            appState->resumed = false;
            break;
        }
        case APP_CMD_STOP: {
            break;
        }
        case APP_CMD_DESTROY: {
            appState->nativeWindow = nullptr;
            break;
        }
        case APP_CMD_INIT_WINDOW: {
            appState->nativeWindow = app->window;
            break;
        }
        case APP_CMD_TERM_WINDOW: {
            appState->nativeWindow = nullptr;
            break;
        }
        }
    }

And into PollSystemEvents() method copy:

// Checks whether Android has requested that application should by destroyed.
if (androidApp->destroyRequested != 0) {
    m_applicationRunning = false;
    return;
}
while (true) {
    // Poll and process the Android OS system events.
    struct android_poll_source *source = nullptr;
    int events = 0;
    // The timeout depends on whether the application is active.
    const int timeoutMilliseconds = (!androidAppState.resumed && !m_sessionRunning && androidApp->destroyRequested == 0) ? -1 : 0;
    if (ALooper_pollAll(timeoutMilliseconds, nullptr, &events, (void **)&source) >= 0) {
        if (source != nullptr) {
            source->process(androidApp, source);
        }
    } else {
        break;
    }
}

1.4.4 Build and Run

With all the source and build system files set up, we can now build our Android project. If while editing main.cpp or any other file you are seeing warnings like this: "This file does not belong to any project target...", right-click on the top-level folder of the project in the Project panel, select Mark Directory as >, and click the option Sources Root.

To build your project go to the upper right of Android Studio, and there you should find the toolbar below. Click the green hammer icon to build the project, if all is successful you should see “BUILD SUCCESSFUL in […]s” in the Build Output window. It is also recommended to sync the gradle files too.

Next to the green hammer icon is the Run/Debug configuration dropdown menu. If that isn’t populated, create a configuration called app.

Turn on and connect your Android device. Set up any requirements for USB debugging and adb. Your device should appear in the dropdown. Here, we are using a Meta Quest 2:

Build/Run Toolbar

To debug/run the application click the green bug icon (the plain green bug, not the one with an arrow on it).

1.5 Summary

In this chapter, you learned about the fundamental concepts of OpenXR and created a project you will use to build your OpenXR application. Now that we have this basic application up and running, we will start to set up OpenXR.

Below is a download link to a zip archive for this chapter containing all the C++ and CMake code for all platform and graphics APIs. Note that Chapter2 is renamed to Chapter1 is the archive and repository folder.

Chapter1.zip

Version: v0.0.0