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.1/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.
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.
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.1/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:
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).
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.
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.
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.
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=924a94a2da0b5ef8e82154c623d88644
URL https://github.com/KhronosGroup/OpenXR-SDK-Source/archive/refs/tags/release-1.0.34.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.1/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_pollOnce(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:
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.
Version: v1.0.8