diff --git a/3rdparty/osgXR/CHANGELOG.md b/3rdparty/osgXR/CHANGELOG.md
new file mode 100644
index 000000000..2213a592a
--- /dev/null
+++ b/3rdparty/osgXR/CHANGELOG.md
@@ -0,0 +1,184 @@
+Version 0.3.7
+-------------
+
+New features:
+ * Implement basic Windows OpenGL graphics binding.
+
+Windows (MSVC) build fixes:
+ * Session: #ifdef X11 specific workaround.
+ * CMake: Require osgUtil.
+ * Manager: Avoid undefined Mirror.
+ * Fix Win32 DLL imports/exports.
+ * Subaction: Use C++11 smart pointers for \_private to avoid undefined Private
+   implementation class.
+ * Fix build against old OpenGL headers.
+
+ABI changes (ABI version 5):
+ * Subaction: Use C++11 smart pointers for \_private.
+ * Switch Action, ActionSet & InteractionProfile pimpls to std::unique\_ptr<>.
+
+Version 0.3.6
+-------------
+
+Bug fixes:
+ * Work around Monado GL context assumptions for xrCreateSession &
+   xrCreateSwapchain calls.
+ * Permit new swapchain formats: GL\_RGB10\_A2 (for Monado on AMD) & GL\_RGBA8.
+
+Behaviour changes:
+ * Report list of swapchain formats on failure to choose one.
+
+Behind the scenes:
+ * Minor whitespace cleanups in src/XRState.cpp.
+
+Version 0.3.5
+-------------
+
+Bug fixes:
+ * Ensure OpenXR::Action is valid before creating an action state object.
+ * Don't suggest empty InteractionProfile bindings to OpenXR.
+
+Behaviour changes:
+ * Manager (XRState) no longer keeps counted references to ActionSets or
+   InteractionProfiles. The app should manage the lifetime of these objects
+   itself, and can now safely discard and recreate them.
+ * All created Actions are now provided to OpenXR even if unreferenced by any
+   InteractionProfile suggested bindings.
+ * Action setup is now treated as a separate initialisation stage. As such if
+   no action sets or no interaction profiles have been created, action setup
+   will now take place on the next update() after they are created.
+
+New/expanded APIs:
+ * Manager::syncActionSet() - To inform osgXR that actions, action sets, or
+   interaction profiles have been altered, so it can take action to apply them
+   as soon as possible.
+ * ActionPose::Location::operator !=, ActionPose::Location::operator == - For
+   comparing pose location objects for equality.
+
+Behind the scenes:
+ * Some minor refactoring in src/Action.cpp.
+
+Version 0.3.4
+-------------
+
+Bug fixes:
+* Fix draw pass accounting and slave cam VR mode.
+
+Behaviour changes:
+* Automatically fall back from SceneView mode if the view configuration isn't
+  stereo.
+
+New/expanded APIs:
+* New action APIs for exposing OpenXR actions (both input and haptics), action
+  sets, interaction profiles, and subactions:
+  * osgXR/Action: New Action, ActionBoolean, ActionFloat, ActionVector2f,
+    ActionPose and ActionVibration classes to represent different kinds of
+    XrAction.
+  * osgXR/ActionSet: New ActionSet class to group actions that can be activated
+    and deactivated together.
+  * osgXR/InteractionProfile: New InteractionProfile class to allows default
+    action bindings for interaction profiles to be suggested.
+  * osgXR/Subaction: New Subaction class to represent a subaction path (or top
+    level user path) which groups physical interactions, allowing single
+    actions that apply to both hands to be handled separately.
+
+Behind the scenes:
+* Add internal action management classes in OpenXR namespace.
+* Wrap XrSpace in an OpenXR::Space class.
+* Wrap XrPath in an OpenXR::Path class.
+* Extend inline code documentation.
+* Code cleanups.
+
+Version 0.3.3 (formerly 0.4.0)
+------------------------------
+
+New/expanded APIs:
+* Settings::getVisibilityMask(), Settings::getVisibilityMask() - for setting
+  whether osgXR should create visibility masks (when supported by the OpenXR
+  runtime).
+* Manager::hasVisibilityMaskExtension() - for finding whether the visibility
+  mask extension is supported by the OpenXR runtime.
+* Manager::setVisibilityMaskNodeMasks() - for setting the left and right eye
+  NodeMasks to use for visibility masks.
+
+Behind the scenes:
+* Implement creation, caching, and updating of visibility mask geometry for each
+  OpenXR view.
+* Implement rendering of visibility masks to the depth buffer to reduce fragment
+  overhead when GPU bound.
+
+Version 0.3.2
+-------------
+
+Bug fixes:
+* Fix a couple of bugs around session recreation (used when VR or swapchain mode
+  changes).
+
+Behaviour changes:
+* Pick depth buffer format based on GraphicsContext traits depth bits.
+* Enable depth info submission at session state to allow it to be dynamically
+  switched without hitting a SteamVR hang during instance destruction.
+
+Behind the scenes:
+* Fix a few inconsequential compiler warnings with -Wall.
+* Minor cosmetic cleanups (whitespace, explicit include).
+
+Version 0.3.1
+-------------
+
+Behind the scenes:
+* Fix possible crash in XRState::isRunning() if session is delayed coming up.
+
+Version 0.3.0
+-------------
+
+API changes:
+* Manager::checkAndResetStateChanged() - to detect when VR state may have
+  changed, requiring app caches to be invalidated or synchronised with the new
+  state.
+
+Version 0.2.1
+-------------
+
+Behind the scenes:
+* Make frame view location accessors thread safe so multiple cull threads can be
+  used.
+
+Version 0.2.0
+-------------
+
+API changes:
+* Cleanups (moving dynamic bits out of Settings).
+* Manager::update() - should be called regularly to allow for incremental
+  bringup and OpenXR event handling.
+* Manager::setEnabled() - for triggering bringing up/down of VR.
+
+New/expanded APIs:
+* Manager::destroyAndWait() and Manager::isDestroying() - for clean shutdown
+  before program exit.
+* Manager::syncSettings() - to trigger appropriate reinitialisation to handle
+  any changed settings.
+* Manager::getStateString() - to get a user readable description of the current
+  VR state.
+* Manager::onRunning() - virtual callback when VR has started (consider setting
+  up desktop mirrors).
+* Manager::onStopped() - virtual callback when VR has stopped (consider
+  removing desktop mirrors).
+* Manager::onFocus() - virtual callback when VR app is running in focus
+  (consider resuming if paused).
+* Manager::onUnfocus() - virtual callback when VR app has lost focus (consider
+  pausing the experience).
+
+Behind the scenes highlights:
+* Make OpenXR bringup incremental, reversible and restartable.
+* Improved handling of SteamVR's messing with GL context and threading.
+* Separated event handling.
+
+Version 0.1.0
+-------------
+
+This represents early development with the API still in heavy flux.
+
+It supported:
+* A Manager class with virtual callbacks for configuring views.
+* Desktop mirrors of the VR views.
diff --git a/3rdparty/osgXR/CMakeLists.txt b/3rdparty/osgXR/CMakeLists.txt
new file mode 100644
index 000000000..87862f2e3
--- /dev/null
+++ b/3rdparty/osgXR/CMakeLists.txt
@@ -0,0 +1,79 @@
+# Top level CMakeLists.txt
+cmake_minimum_required(VERSION 3.11)
+
+set(osgXR_MAJOR_VERSION 0)
+set(osgXR_MINOR_VERSION 3)
+set(osgXR_PATCH_VERSION 7)
+set(osgXR_SOVERSION 5)
+
+set(osgXR_VERSION "${osgXR_MAJOR_VERSION}.${osgXR_MINOR_VERSION}.${osgXR_PATCH_VERSION}")
+
+project(osgXR
+        VERSION     ${osgXR_VERSION}
+        DESCRIPTION "OpenXR integration for OpenSceneGraph applications"
+)
+
+if(CMAKE_PROJECT_NAME STREQUAL osgXR)
+    # Normal top level project build, install package components
+
+    include(CMakePackageConfigHelpers)
+    include(GNUInstallDirs)
+
+    # Build options
+    option(BUILD_SHARED_LIBS "Whether to build as a shared library" ON)
+    option(BUILD_OSGXR_EXAMPLES "Enable to build osgXR examples" OFF)
+
+    # Source files in src/
+    add_subdirectory(src)
+
+    if(BUILD_OSGXR_EXAMPLES)
+        add_subdirectory(examples)
+    endif()
+
+    set(INSTALL_INCDIR "${CMAKE_INSTALL_INCLUDEDIR}")
+
+    # Preprocess pkgconfig file
+    configure_file(osgXR.pc.in osgXR.pc @ONLY)
+
+    # Preprocess package config
+    configure_package_config_file(Config.cmake.in osgXRConfig.cmake
+        INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/osgXR"
+        PATH_VARS           INSTALL_INCDIR
+    )
+    # Build package version file
+    write_basic_package_version_file(osgXRConfigVersion.cmake
+        VERSION "${PROJECT_VERSION}"
+        COMPATIBILITY SameMinorVersion
+    )
+
+    # Install library and headers
+    install(TARGETS                   osgXR
+            EXPORT                    osgXRTargets
+            LIBRARY DESTINATION       "${CMAKE_INSTALL_LIBDIR}"
+            PUBLIC_HEADER DESTINATION "${INSTALL_INCDIR}/osgXR"
+            INCLUDES      DESTINATION "${INSTALL_INCDIR}"
+    )
+    # Install pkgconfig file
+    install(FILES       "${CMAKE_CURRENT_BINARY_DIR}/osgXR.pc"
+            DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig"
+    )
+    # Install export targets
+    install(EXPORT      osgXRTargets
+            FILE        osgXRTargets.cmake
+            NAMESPACE   osgXR::
+            DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/osgXR"
+    )
+    # Install package config and version files
+    install(FILES       "${CMAKE_CURRENT_BINARY_DIR}/osgXRConfig.cmake"
+                        "${CMAKE_CURRENT_BINARY_DIR}/osgXRConfigVersion.cmake"
+            DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/osgXR"
+    )
+
+else()
+    # Subproject build
+
+    # Just build src/ as a static library
+    set(osgXR_LIBRARY_TYPE STATIC)
+    add_subdirectory(src)
+
+endif()
diff --git a/3rdparty/osgXR/Config.cmake.in b/3rdparty/osgXR/Config.cmake.in
new file mode 100644
index 000000000..679fb6551
--- /dev/null
+++ b/3rdparty/osgXR/Config.cmake.in
@@ -0,0 +1,14 @@
+include(CMakeFindDependencyMacro)
+
+find_dependency(OpenGL)
+find_dependency(OpenSceneGraph COMPONENTS osgViewer)
+find_dependency(OpenXR)
+
+@PACKAGE_INIT@
+
+set_and_check(osgXR_INCLUDE_DIR @PACKAGE_INSTALL_INCDIR@)
+set(osgXR_LIBRARY osgXR::osgXR)
+
+include("${CMAKE_CURRENT_LIST_DIR}/osgXRTargets.cmake")
+
+check_required_components(osgXR)
diff --git a/3rdparty/osgXR/LICENSE.txt b/3rdparty/osgXR/LICENSE.txt
new file mode 100644
index 000000000..4362b4915
--- /dev/null
+++ b/3rdparty/osgXR/LICENSE.txt
@@ -0,0 +1,502 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/3rdparty/osgXR/README.md b/3rdparty/osgXR/README.md
new file mode 100644
index 000000000..61b802ba3
--- /dev/null
+++ b/3rdparty/osgXR/README.md
@@ -0,0 +1,73 @@
+osgXR: Virtual Reality with OpenXR and OpenSceneGraph
+=====================================================
+
+This library is to allow Virtual Reality support to be added to
+[OpenSceneGraph](http://www.openscenegraph.org/) applications, using the
+[OpenXR](https://www.khronos.org/OpenXR/) standard.
+
+Status:
+ * Still in development, contributions welcome. Plenty of work to do.
+ * APIs to support OpenXR VR display output in OpenSceneGraph apps.
+ * APIs to support OpenXR's action based input and haptic output for controller
+   interaction.
+ * OpenGL graphics bindings for Linux (X11/Xlib) and Windows (note that WMR
+   only supports DirectX bindings).
+
+License: LGPL 2.1
+
+Dependencies: OpenSceneGraph, OpenXR
+
+Links:
+ * Matrix room: [#osgxr:hoganfam.uk](https://matrix.to/#/#osgxr:hoganfam.uk?via=hoganfam.uk)
+ * [OpenXR specifications](https://www.khronos.org/registry/OpenXR/#apispecs)
+
+
+Installation
+------------
+
+Something like this:
+```shell
+mkdir build
+cd build
+cmake ..
+make
+make install
+```
+
+
+Getting Started
+---------------
+
+To import osgXR into a CMake based project, you can use the included CMake
+module, adding something like this to your CMakeLists.txt:
+```cmake
+find_package(osgXR 0.3.5 REQUIRED)
+
+target_link_libraries(target
+        ..
+        osgXR::osgXR
+)
+```
+
+osgXR can also optionally be built as a subproject. Consider using ``git
+subtree`` to import osgXR, then use something like this:
+```cmake
+add_subdirectory(osgXR)
+
+target_link_libraries(target
+        ..
+        osgXR
+)
+```
+
+If you have installed osgXR outside of the system prefix (CMake's default prefix
+on UNIX systems is ``/usr/local``), you may need to tell CMake where to find it
+when you configure the project. You can do this by defining ``osgXR_DIR`` when
+invoking cmake, e.g. with the argument ``-DosgXR_DIR=$PREFIX/lib/cmake/osgXR``
+where ``$PREFIX`` is osgXR's install prefix (``CMAKE_INSTALL_PREFIX``).
+
+
+The Public API
+--------------
+
+See the [API documentation](docs/API.md) for details of the API.
diff --git a/3rdparty/osgXR/docs/API.md b/3rdparty/osgXR/docs/API.md
new file mode 100644
index 000000000..74b9e2b27
--- /dev/null
+++ b/3rdparty/osgXR/docs/API.md
@@ -0,0 +1,102 @@
+osgXR API Documentation
+=======================
+
+The osgXR API is currently considered unstable, however it is versioned. Only
+matching minor version numbers should be expected to be source and binary
+compatible with one another.
+
+The osgXR headers can be found in [include/osgXR/](../include/osgXR/).
+
+The initial version of this library exposed ``<osgXR/OpenXRDisplay>`` for
+configuring a view for VR, and ``<osgXR/osgXR>`` with a convenience wrapper
+``osgXR::setupViewerDefaults`` to set up VR automatically based on environment
+variables. This worked for most simple OpenSceneGraph examples, however for real
+projects something more capable is needed, so it is likely these will be removed
+in a future version (they are not currently working due to the new
+``XRState::update()`` based state machine).
+
+It is instead recommended to extend the ``osgXR::Manager`` class from
+``<osgXR/Manager>`` and implement the callbacks.
+
+## <[osgXR/Action](../include/osgXR/Action)>
+
+This header provides the ``osgXR::Action`` base class, and the
+``osgXR::ActionBoolean``, ``osgXR::ActionFloat``, ``osgXR::ActionVector2f``,
+``osgXR::ActionPose`` and ``osgXR::ActionVibration`` classes which an
+application can use to define OpenXR actions, read input state, and send haptic
+output.
+
+## <[osgXR/ActionSet](../include/osgXR/ActionSet)>
+
+This header provides the ``osgXR::ActionSet`` class which an application uses
+to group actions into groups which can be separately activated and deactivated.
+
+## <[osgXR/InteractionProfile](../include/osgXR/InteractionProfile)>
+
+This header provides the ``osgXR::InteractionProfile`` class which an
+application uses to suggest bindings between OpenXR actions and the physical
+interactions of a given OpenXR controller profile.
+
+## <[osgXR/Manager](../include/osgXR/Manager)>
+
+This header provides the ``osgXR::Manager`` class which an application can
+extend to implement a VR manager class. Virtual callbacks tell the application
+which camera views are required to implement VR, and an ``update()`` function
+gives osgXR a chance to incrementally bring up or tear down VR.
+
+## <[osgXR/Mirror](../include/osgXR/Mirror)>
+
+This header provides the ``osgXR::Mirror`` class which an application can use to
+register a desktop mirror of the VR view.
+
+## <[osgXR/MirrorSettings](../include/osgXR/MirrorSettings)>
+
+This header provides the ``osgXR::MirrorSettings`` class which encapsulates
+configuration data for desktop mirrors of VR views.
+
+## <[osgXR/OpenXRDisplay](../include/osgXR/OpenXRDisplay)>
+
+This header provides the ``osgXR::OpenXRDisplay`` ViewConfig class. It is
+largely replaced by ``osgXR::Manager``.
+
+## <[osgXR/Settings](../include/osgXR/Settings)>
+
+This header provides the ``osgXR::Settings`` class which encapsulates all the VR
+configuration data.
+
+## <[osgXR/Subaction](../include/osgXR/Subaction)>
+
+This header provides the ``osgXR::Subaction`` class which an application can
+use to represent OpenXR subaction paths (top level user paths), which allow a
+single OpenXR action to represent the same action on both hands. It can be
+passed to other action related classes to filter actions by hand, and it can be
+extended by the application to implement a callback for InteractionProfile
+changes.
+
+## <[osgXR/View](../include/osgXR/View)>
+
+This header provides the ``osgXR::View`` class which represents a view of the
+world which ``osgXR::Manager`` will request to be configured by the application.
+
+## <[osgXR/osgXR](../include/osgXR/osgXR)>
+
+This header provides convenience functions for quick and easy integration of VR
+capabilities into a simple OpenSceneGraph application or example. It is
+recommended ``osgXR::Manager`` be used instead for most projects.
+
+### osgXR::setupViewerDefaults()
+
+This sets up VR on the provided viewer based on the content of the following
+environment variables:
+ * ``OSGXR=1``                  enables VR.
+ * ``OSGXR_MODE=SLAVE_CAMERAS`` forces the use of separate slave cameras per view.
+ * ``OSGXR_MODE=SCENE_VIEW``    forces the use of OpenSceneGraph's SceneView stereo (default).
+ * ``OSGXR_SWAPCHAIN=MULTIPLE`` forces the use of separate swapchains per view.
+ * ``OSGXR_SWAPCHAIN=SINGLE``   forces the use of a single swapchain containing all views.
+ * ``OSGXR_UNITS_PER_METER=10`` allows the scale of the environment to be controlled.
+ * ``OSGXR_VALIDATION_LAYER=1`` enables the OpenXR validation layer (off by default).
+ * ``OSGXR_DEPTH_INFO=1``       enables passing of depth information to OpenXR (off by default).
+ * ``OSGXR_MIRROR=NONE``        use a blank screen as the default mirror.
+ * ``OSGXR_MIRROR=LEFT``        use OpenXR view 0 (left) as the default mirror.
+ * ``OSGXR_MIRROR=RIGHT``       use OpenXR view 1 (right) as the default mirror.
+ * ``OSGXR_MIRROR=LEFT_RIGHT``  use both left and right views side by side as the default mirror.
diff --git a/3rdparty/osgXR/examples/CMakeLists.txt b/3rdparty/osgXR/examples/CMakeLists.txt
new file mode 100644
index 000000000..4ad17295f
--- /dev/null
+++ b/3rdparty/osgXR/examples/CMakeLists.txt
@@ -0,0 +1,29 @@
+cmake_minimum_required(VERSION 3.11)
+project(osgXR::examples)
+
+find_package(OpenGL REQUIRED)
+find_package(OpenSceneGraph REQUIRED COMPONENTS osgDB osgViewer)
+
+if(CMAKE_PROJECT_NAME STREQUAL osgXR)
+    # If we're building from osgXR source tree, take a shortcut
+    set(osgXR_INCLUDE_DIR "../include")
+    set(osgXR_LIBRARY osgXR)
+else()
+    # Otherwise, we'd normally use osgXR::osgXR, but osgXR_LIBRARY will do here
+    find_package(osgXR REQUIRED)
+endif()
+
+add_executable(osgteapot osgteapot.cpp)
+
+target_include_directories(osgteapot
+    PRIVATE
+        ${OPENGL_INCLUDE_DIR}
+        ${OPENSCENEGRAPH_INCLUDE_DIRS}
+)
+
+target_link_libraries(osgteapot
+    PUBLIC
+        ${OPENGL_LIBRARIES}
+        ${OPENSCENEGRAPH_LIBRARIES}
+        ${osgXR_LIBRARY}
+)
diff --git a/3rdparty/osgXR/examples/osgteapot.cpp b/3rdparty/osgXR/examples/osgteapot.cpp
new file mode 100644
index 000000000..5bc071a40
--- /dev/null
+++ b/3rdparty/osgXR/examples/osgteapot.cpp
@@ -0,0 +1,365 @@
+/* OpenSceneGraph example, osgteapot.
+*
+*  Permission is hereby granted, free of charge, to any person obtaining a copy
+*  of this software and associated documentation files (the "Software"), to deal
+*  in the Software without restriction, including without limitation the rights
+*  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+*  copies of the Software, and to permit persons to whom the Software is
+*  furnished to do so, subject to the following conditions:
+*
+*  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+*  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+*  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+*  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+*  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+*  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+*  THE SOFTWARE.
+*/
+
+#include <osg/Geode>
+#include <osg/TexGen>
+#include <osg/Texture2D>
+
+#include <osgDB/ReadFile>
+
+#include <osgViewer/Viewer>
+
+#include <osgXR/osgXR>
+
+
+// The classic OpenGL teapot... taken form glut-3.7/lib/glut/glut_teapot.c
+
+/* Copyright (c) Mark J. Kilgard, 1994. */
+
+/**
+(c) Copyright 1993, Silicon Graphics, Inc.
+
+ALL RIGHTS RESERVED
+
+Permission to use, copy, modify, and distribute this software
+for any purpose and without fee is hereby granted, provided
+that the above copyright notice appear in all copies and that
+both the copyright notice and this permission notice appear in
+supporting documentation, and that the name of Silicon
+Graphics, Inc. not be used in advertising or publicity
+pertaining to distribution of the software without specific,
+written prior permission.
+
+THE MATERIAL EMBODIED ON THIS SOFTWARE IS PROVIDED TO YOU
+"AS-IS" AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR
+OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY WARRANTY OF
+MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  IN NO
+EVENT SHALL SILICON GRAPHICS, INC.  BE LIABLE TO YOU OR ANYONE
+ELSE FOR ANY DIRECT, SPECIAL, INCIDENTAL, INDIRECT OR
+CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER,
+INCLUDING WITHOUT LIMITATION, LOSS OF PROFIT, LOSS OF USE,
+SAVINGS OR REVENUE, OR THE CLAIMS OF THIRD PARTIES, WHETHER OR
+NOT SILICON GRAPHICS, INC.  HAS BEEN ADVISED OF THE POSSIBILITY
+OF SUCH LOSS, HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ARISING OUT OF OR IN CONNECTION WITH THE POSSESSION, USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+US Government Users Restricted Rights
+
+Use, duplication, or disclosure by the Government is subject to
+restrictions set forth in FAR 52.227.19(c)(2) or subparagraph
+(c)(1)(ii) of the Rights in Technical Data and Computer
+Software clause at DFARS 252.227-7013 and/or in similar or
+successor clauses in the FAR or the DOD or NASA FAR
+Supplement.  Unpublished-- rights reserved under the copyright
+laws of the United States.  Contractor/manufacturer is Silicon
+Graphics, Inc., 2011 N.  Shoreline Blvd., Mountain View, CA
+94039-7311.
+
+OpenGL(TM) is a trademark of Silicon Graphics, Inc.
+*/
+
+
+/* Rim, body, lid, and bottom data must be reflected in x and
+   y; handle and spout data across the y axis only.  */
+
+static int patchdata[][16] =
+{
+    /* rim */
+  {102, 103, 104, 105, 4, 5, 6, 7, 8, 9, 10, 11,
+    12, 13, 14, 15},
+    /* body */
+  {12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
+    24, 25, 26, 27},
+  {24, 25, 26, 27, 29, 30, 31, 32, 33, 34, 35, 36,
+    37, 38, 39, 40},
+    /* lid */
+  {96, 96, 96, 96, 97, 98, 99, 100, 101, 101, 101,
+    101, 0, 1, 2, 3,},
+  {0, 1, 2, 3, 106, 107, 108, 109, 110, 111, 112,
+    113, 114, 115, 116, 117},
+    /* bottom */
+  {118, 118, 118, 118, 124, 122, 119, 121, 123, 126,
+    125, 120, 40, 39, 38, 37},
+    /* handle */
+  {41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52,
+    53, 54, 55, 56},
+  {53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
+    28, 65, 66, 67},
+    /* spout */
+  {68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
+    80, 81, 82, 83},
+  {80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91,
+    92, 93, 94, 95}
+};
+/* *INDENT-OFF* */
+
+static float cpdata[][3] =
+{
+    {0.2, 0, 2.7}, {0.2, -0.112, 2.7}, {0.112, -0.2, 2.7}, {0,
+    -0.2, 2.7}, {1.3375, 0, 2.53125}, {1.3375, -0.749, 2.53125},
+    {0.749, -1.3375, 2.53125}, {0, -1.3375, 2.53125}, {1.4375,
+    0, 2.53125}, {1.4375, -0.805, 2.53125}, {0.805, -1.4375,
+    2.53125}, {0, -1.4375, 2.53125}, {1.5, 0, 2.4}, {1.5, -0.84,
+    2.4}, {0.84, -1.5, 2.4}, {0, -1.5, 2.4}, {1.75, 0, 1.875},
+    {1.75, -0.98, 1.875}, {0.98, -1.75, 1.875}, {0, -1.75,
+    1.875}, {2, 0, 1.35}, {2, -1.12, 1.35}, {1.12, -2, 1.35},
+    {0, -2, 1.35}, {2, 0, 0.9}, {2, -1.12, 0.9}, {1.12, -2,
+    0.9}, {0, -2, 0.9}, {-2, 0, 0.9}, {2, 0, 0.45}, {2, -1.12,
+    0.45}, {1.12, -2, 0.45}, {0, -2, 0.45}, {1.5, 0, 0.225},
+    {1.5, -0.84, 0.225}, {0.84, -1.5, 0.225}, {0, -1.5, 0.225},
+    {1.5, 0, 0.15}, {1.5, -0.84, 0.15}, {0.84, -1.5, 0.15}, {0,
+    -1.5, 0.15}, {-1.6, 0, 2.025}, {-1.6, -0.3, 2.025}, {-1.5,
+    -0.3, 2.25}, {-1.5, 0, 2.25}, {-2.3, 0, 2.025}, {-2.3, -0.3,
+    2.025}, {-2.5, -0.3, 2.25}, {-2.5, 0, 2.25}, {-2.7, 0,
+    2.025}, {-2.7, -0.3, 2.025}, {-3, -0.3, 2.25}, {-3, 0,
+    2.25}, {-2.7, 0, 1.8}, {-2.7, -0.3, 1.8}, {-3, -0.3, 1.8},
+    {-3, 0, 1.8}, {-2.7, 0, 1.575}, {-2.7, -0.3, 1.575}, {-3,
+    -0.3, 1.35}, {-3, 0, 1.35}, {-2.5, 0, 1.125}, {-2.5, -0.3,
+    1.125}, {-2.65, -0.3, 0.9375}, {-2.65, 0, 0.9375}, {-2,
+    -0.3, 0.9}, {-1.9, -0.3, 0.6}, {-1.9, 0, 0.6}, {1.7, 0,
+    1.425}, {1.7, -0.66, 1.425}, {1.7, -0.66, 0.6}, {1.7, 0,
+    0.6}, {2.6, 0, 1.425}, {2.6, -0.66, 1.425}, {3.1, -0.66,
+    0.825}, {3.1, 0, 0.825}, {2.3, 0, 2.1}, {2.3, -0.25, 2.1},
+    {2.4, -0.25, 2.025}, {2.4, 0, 2.025}, {2.7, 0, 2.4}, {2.7,
+    -0.25, 2.4}, {3.3, -0.25, 2.4}, {3.3, 0, 2.4}, {2.8, 0,
+    2.475}, {2.8, -0.25, 2.475}, {3.525, -0.25, 2.49375},
+    {3.525, 0, 2.49375}, {2.9, 0, 2.475}, {2.9, -0.15, 2.475},
+    {3.45, -0.15, 2.5125}, {3.45, 0, 2.5125}, {2.8, 0, 2.4},
+    {2.8, -0.15, 2.4}, {3.2, -0.15, 2.4}, {3.2, 0, 2.4}, {0, 0,
+    3.15}, {0.8, 0, 3.15}, {0.8, -0.45, 3.15}, {0.45, -0.8,
+    3.15}, {0, -0.8, 3.15}, {0, 0, 2.85}, {1.4, 0, 2.4}, {1.4,
+    -0.784, 2.4}, {0.784, -1.4, 2.4}, {0, -1.4, 2.4}, {0.4, 0,
+    2.55}, {0.4, -0.224, 2.55}, {0.224, -0.4, 2.55}, {0, -0.4,
+    2.55}, {1.3, 0, 2.55}, {1.3, -0.728, 2.55}, {0.728, -1.3,
+    2.55}, {0, -1.3, 2.55}, {1.3, 0, 2.4}, {1.3, -0.728, 2.4},
+    {0.728, -1.3, 2.4}, {0, -1.3, 2.4}, {0, 0, 0}, {1.425,
+    -0.798, 0}, {1.5, 0, 0.075}, {1.425, 0, 0}, {0.798, -1.425,
+    0}, {0, -1.5, 0.075}, {0, -1.425, 0}, {1.5, -0.84, 0.075},
+    {0.84, -1.5, 0.075}
+};
+
+static float tex[2][2][2] =
+{
+  { {0, 0},
+    {1, 0}},
+  { {0, 1},
+    {1, 1}}
+};
+
+/* *INDENT-ON* */
+
+static void
+teapot(GLint grid, GLenum type)
+{
+  float p[4][4][3], q[4][4][3], r[4][4][3], s[4][4][3];
+  long i, j, k, l;
+
+  glPushAttrib(GL_ENABLE_BIT | GL_EVAL_BIT);
+  glEnable(GL_AUTO_NORMAL);
+  glEnable(GL_NORMALIZE);
+  glEnable(GL_MAP2_VERTEX_3);
+  glEnable(GL_MAP2_TEXTURE_COORD_2);
+  for (i = 0; i < 10; i++) {
+    for (j = 0; j < 4; j++) {
+      for (k = 0; k < 4; k++) {
+        for (l = 0; l < 3; l++) {
+          p[j][k][l] = cpdata[patchdata[i][j * 4 + k]][l];
+          q[j][k][l] = cpdata[patchdata[i][j * 4 + (3 - k)]][l];
+          if (l == 1)
+            q[j][k][l] *= -1.0;
+          if (i < 6) {
+            r[j][k][l] =
+              cpdata[patchdata[i][j * 4 + (3 - k)]][l];
+            if (l == 0)
+              r[j][k][l] *= -1.0;
+            s[j][k][l] = cpdata[patchdata[i][j * 4 + k]][l];
+            if (l == 0)
+              s[j][k][l] *= -1.0;
+            if (l == 1)
+              s[j][k][l] *= -1.0;
+          }
+        }
+      }
+    }
+    glMap2f(GL_MAP2_TEXTURE_COORD_2, 0, 1, 2, 2, 0, 1, 4, 2,
+      &tex[0][0][0]);
+    glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4,
+      &p[0][0][0]);
+    glMapGrid2f(grid, 0.0, 1.0, grid, 0.0, 1.0);
+    glEvalMesh2(type, 0, grid, 0, grid);
+    glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4,
+      &q[0][0][0]);
+    glEvalMesh2(type, 0, grid, 0, grid);
+    if (i < 6) {
+      glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4,
+        &r[0][0][0]);
+      glEvalMesh2(type, 0, grid, 0, grid);
+      glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4,
+        &s[0][0][0]);
+      glEvalMesh2(type, 0, grid, 0, grid);
+    }
+  }
+  glPopAttrib();
+}
+
+
+// Now the OSG wrapper for the above OpenGL code, the most complicated bit is computing
+// the bounding box for the above example, normally you'll find this the easy bit.
+class Teapot : public osg::Drawable
+{
+    public:
+        Teapot() {}
+
+        /** Copy constructor using CopyOp to manage deep vs shallow copy.*/
+        Teapot(const Teapot& teapot,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY):
+            osg::Drawable(teapot,copyop) {}
+
+        META_Object(myTeapotApp,Teapot)
+
+
+        // the draw immediate mode method is where the OSG wraps up the drawing of
+        // of OpenGL primitives.
+        virtual void drawImplementation(osg::RenderInfo&) const
+        {
+            // teapot(..) doesn't use vertex arrays at all so we don't need to toggle their state
+            // if we did we'd need to something like following call
+            // state.disableAllVertexArrays(), see src/osg/Geometry.cpp for the low down.
+
+            // just call the OpenGL code.
+            teapot(14,GL_FILL);
+        }
+
+
+        // we need to set up the bounding box of the data too, so that the scene graph knows where this
+        // objects is, for both positioning the camera at start up, and most importantly for culling.
+        virtual osg::BoundingBox computeBoundingBox() const
+        {
+            osg::BoundingBox bbox;
+
+            // follow is some truly horrible code required to calculate the
+            // bounding box of the teapot.  Have used the original code above to do
+            // help compute it.
+            float p[4][4][3], q[4][4][3], r[4][4][3], s[4][4][3];
+            long i, j, k, l;
+
+            for (i = 0; i < 10; i++) {
+              for (j = 0; j < 4; j++) {
+                for (k = 0; k < 4; k++) {
+
+                  for (l = 0; l < 3; l++) {
+                    p[j][k][l] = cpdata[patchdata[i][j * 4 + k]][l];
+                    q[j][k][l] = cpdata[patchdata[i][j * 4 + (3 - k)]][l];
+                    if (l == 1)
+                      q[j][k][l] *= -1.0;
+
+                    if (i < 6) {
+                      r[j][k][l] =
+                        cpdata[patchdata[i][j * 4 + (3 - k)]][l];
+                      if (l == 0)
+                        r[j][k][l] *= -1.0;
+                      s[j][k][l] = cpdata[patchdata[i][j * 4 + k]][l];
+                      if (l == 0)
+                        s[j][k][l] *= -1.0;
+                      if (l == 1)
+                        s[j][k][l] *= -1.0;
+                    }
+                  }
+
+                  bbox.expandBy(osg::Vec3(p[j][k][0],p[j][k][1],p[j][k][2]));
+                  bbox.expandBy(osg::Vec3(q[j][k][0],q[j][k][1],q[j][k][2]));
+
+                  if (i < 6)
+                  {
+                    bbox.expandBy(osg::Vec3(r[j][k][0],r[j][k][1],r[j][k][2]));
+                    bbox.expandBy(osg::Vec3(s[j][k][0],s[j][k][1],s[j][k][2]));
+                  }
+
+
+                }
+              }
+            }
+
+            return bbox;
+        }
+
+    protected:
+
+        virtual ~Teapot() {}
+
+};
+
+
+osg::Geode* createTeapot()
+{
+    osg::Geode* geode = new osg::Geode();
+
+    // add the teapot to the geode.
+    geode->addDrawable( new Teapot );
+
+    // add a reflection map to the teapot.
+    osg::ref_ptr<osg::Image> image = osgDB::readRefImageFile("Images/reflect.rgb");
+    if (image)
+    {
+        osg::Texture2D* texture = new osg::Texture2D;
+        texture->setImage(image);
+
+        osg::TexGen* texgen = new osg::TexGen;
+        texgen->setMode(osg::TexGen::SPHERE_MAP);
+
+        osg::StateSet* stateset = new osg::StateSet;
+        stateset->setTextureAttributeAndModes(0,texture,osg::StateAttribute::ON);
+        stateset->setTextureAttributeAndModes(0,texgen,osg::StateAttribute::ON);
+
+        geode->setStateSet(stateset);
+    }
+
+    return geode;
+}
+
+int main(int , char **)
+{
+#if 1
+
+    // create viewer on heap as a test, this looks to be causing problems
+    // on init on some platforms, and seg fault on exit when multi-threading on linux.
+    // Normal stack based version below works fine though...
+
+    // construct the viewer.
+    osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer;
+
+    // add model to viewer.
+    viewer->setSceneData( createTeapot() );
+
+    osgXR::setupViewerDefaults(viewer, "osgteaport", 1);
+
+    return viewer->run();
+
+#else
+
+    // construct the viewer.
+    osgViewer::Viewer viewer;
+
+    // add model to viewer.
+    viewer.setSceneData( createTeapot() );
+
+    // create the windows and run the threads.
+    return viewer.run();
+#endif
+
+}
diff --git a/3rdparty/osgXR/include/osgXR/Action b/3rdparty/osgXR/include/osgXR/Action
new file mode 100644
index 000000000..108a07b91
--- /dev/null
+++ b/3rdparty/osgXR/include/osgXR/Action
@@ -0,0 +1,471 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_Action
+#define OSGXR_Action 1
+
+#include <osgXR/Export>
+
+#include <osg/Quat>
+#include <osg/Referenced>
+#include <osg/Vec2f>
+#include <osg/Vec3f>
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace osgXR {
+
+class ActionSet;
+class Subaction;
+
+/**
+ * Represents an OpenXR action.
+ * OpenXR actions are inputs & outputs which are abstracted from the physical
+ * input sources. The OpenXR runtime is responsible for binding them to sources,
+ * using suggested bindings in interaction profiles.
+ *
+ * These Action objects can persist across multiple VR sessions, and changes can
+ * be made at any time, however some changes won't take effect while a session
+ * is running.
+ */
+class OSGXR_EXPORT Action : public osg::Referenced
+{
+    public:
+
+        class Private;
+
+    protected:
+
+        /// Constructor (internal).
+        Action(Private *priv);
+
+    public:
+
+        /// Destructor.
+        ~Action();
+
+        /**
+         * Add a subaction that may be later queried.
+         * Any subaction that is intended to be queried must be added to the
+         * action first.
+         * @param subaction Subaction that may be later queried.
+         */
+        void addSubaction(Subaction *subaction);
+
+        // Accessors
+
+        /**
+         * Set the action's name and localized name.
+         * @param name          New name for OpenXR action.
+         * @param localizedName A localized version of @p name.
+         */
+        void setName(const std::string &name,
+                     const std::string &localizedName);
+
+        /**
+         * Set the action's name.
+         * @param name New name for OpenXR action.
+         */
+        void setName(const std::string &name);
+        /// Get the action's name.
+        const std::string &getName() const;
+
+        /**
+         * Set the action's localized name.
+         * @param localizedName The localized name for the action.
+         */
+        void setLocalizedName(const std::string &localizedName);
+        /// Get the action's localized name.
+        const std::string &getLocalizedName() const;
+
+        /**
+         * Get a list of currently bound source paths for this action.
+         * @param sourcePaths[out] Vector of source paths to write into.
+         */
+        void getBoundSources(std::vector<std::string> &sourcePaths) const;
+
+        typedef enum {
+            // Must match XR_INPUT_SOURCE_LOCALIZED_NAME_*
+            /// Include user path (e.g. "Left Hand").
+            USER_PATH_BIT = 1,
+            /// Include interaction profile (e.g. "Vive Controller").
+            INTERACTION_PROFILE_BIT = 2,
+            /// Include input component (e.g. "Trigger").
+            COMPONENT_BIT = 4,
+        } LocalizedNameFlags;
+
+        /**
+         * Get a list of currently bound source localized names for this action.
+         * @param whichComponents  Which components to include.
+         * @param names[out] Vector of names to write into.
+         */
+        void getBoundSourcesLocalizedNames(uint32_t whichComponents,
+                                           std::vector<std::string> &names) const;
+
+    private:
+
+        std::unique_ptr<Private> _private;
+
+        // Copying not permitted
+        Action(const Action &copy);
+};
+
+/// An action that can only have boolean values.
+class OSGXR_EXPORT ActionBoolean : public Action
+{
+    public:
+
+        /**
+         * Construct a boolean action.
+         * @param actionSet The action set the action should belong to.
+         */
+        ActionBoolean(ActionSet *actionSet);
+
+        /**
+         * Construct a boolean action.
+         * @param actionSet The action set the action should belong to.
+         * @param name      The name of the OpenXR action, also used as the
+         *                  localized name.
+         */
+        ActionBoolean(ActionSet *actionSet,
+                      const std::string &name);
+
+        /**
+         * Construct a boolean action.
+         * @param actionSet     The action set the action should belong to.
+         * @param name          The name of the OpenXR action.
+         * @param localizedName The localized name for the action.
+         */
+        ActionBoolean(ActionSet *actionSet,
+                      const std::string &name,
+                      const std::string &localizedName);
+
+        /**
+         * Get the current value of the action as a bool.
+         * @param subaction The subaction to filter sources from, which must
+         *                  have been specified to Action::addSubaction().
+         * @return The current value of the action.
+         */
+        bool getValue(Subaction *subaction = nullptr);
+};
+
+/// An action that can have floating point values.
+class OSGXR_EXPORT ActionFloat : public Action
+{
+    public:
+
+        /**
+         * Construct a floating-point action.
+         * @param actionSet The action set the action should belong to.
+         */
+        ActionFloat(ActionSet *actionSet);
+
+        /**
+         * Construct a floating-point action.
+         * @param actionSet The action set the action should belong to.
+         * @param name      The name of the OpenXR action, also used as the
+         *                  localized name.
+         */
+        ActionFloat(ActionSet *actionSet,
+                    const std::string &name);
+
+        /**
+         * Construct a floating-point action.
+         * @param actionSet     The action set the action should belong to.
+         * @param name          The name of the OpenXR action.
+         * @param localizedName The localized name for the action.
+         */
+        ActionFloat(ActionSet *actionSet,
+                    const std::string &name,
+                    const std::string &localizedName);
+
+        /**
+         * Get the current value of the action as a float.
+         * @param subaction The subaction to filter sources from, which must
+         *                  have been specified to Action::addSubaction().
+         * @return The current value of the action.
+         */
+        float getValue(Subaction *subaction = nullptr);
+};
+
+/// An action that can have 2 dimentional floating point vector values.
+class OSGXR_EXPORT ActionVector2f : public Action
+{
+    public:
+
+        /**
+         * Construct a 2d floating-point vector action.
+         * @param actionSet The action set the action should belong to.
+         */
+        ActionVector2f(ActionSet *actionSet);
+
+        /**
+         * Construct a 2d floating-point vector action.
+         * @param actionSet The action set the action should belong to.
+         * @param name      The name of the OpenXR action, also used as the
+         *                  localized name.
+         */
+        ActionVector2f(ActionSet *actionSet,
+                       const std::string &name);
+
+        /**
+         * Construct a 2d floating-point vector action.
+         * @param actionSet     The action set the action should belong to.
+         * @param name          The name of the OpenXR action.
+         * @param localizedName The localized name for the action.
+         */
+        ActionVector2f(ActionSet *actionSet,
+                       const std::string &name,
+                       const std::string &localizedName);
+
+        /**
+         * Get the current value of the action as an OSG vector.
+         * @param subaction The subaction to filter sources from, which must
+         *                  have been specified to Action::addSubaction().
+         * @return The current value of the action.
+         */
+        osg::Vec2f getValue(Subaction *subaction = nullptr);
+};
+
+/// An action that can have pose (position and orientation) values.
+class OSGXR_EXPORT ActionPose : public Action
+{
+    public:
+
+        /**
+         * Construct a pose action.
+         * @param actionSet The action set the action should belong to.
+         */
+        ActionPose(ActionSet *actionSet);
+
+        /**
+         * Construct a pose action.
+         * @param actionSet The action set the action should belong to.
+         * @param name      The name of the OpenXR action, also used as the
+         *                  localized name.
+         */
+        ActionPose(ActionSet *actionSet,
+                   const std::string &name);
+
+        /**
+         * Construct a pose action.
+         * @param actionSet     The action set the action should belong to.
+         * @param name          The name of the OpenXR action.
+         * @param localizedName The localized name for the action.
+         */
+        ActionPose(ActionSet *actionSet,
+                   const std::string &name,
+                   const std::string &localizedName);
+
+        /**
+         * Represents a pose action's position and orientation.
+         * This represents a pose action's position and orientation, along with
+         * flags to indicate whether each of these are valid and whether they're
+         * currently tracked (as opposed to estimated based on recent tracking).
+         */
+        class OSGXR_EXPORT Location
+        {
+            public:
+
+                typedef enum {
+                    // Must match XR_SPACE_LOCATION_* */
+                    ORIENTATION_VALID_BIT   = 0x1,
+                    POSITION_VALID_BIT      = 0x2,
+                    ORIENTATION_TRACKED_BIT = 0x4,
+                    POSITION_TRACKED_BIT    = 0x8,
+                } Flags;
+
+                // Constructors
+
+                /// Construct a pose action location.
+                Location();
+                /// Construct a pose action location.
+                Location(Flags flags,
+                         const osg::Quat &orientation,
+                         const osg::Vec3f &position);
+
+                // Accessors
+
+                /**
+                 * Find whether the orientation is valid.
+                 * If not, the orientation is undefined.
+                 * @return Whether the orientation is valid.
+                 */
+                bool isOrientationValid() const
+                {
+                    return _flags & ORIENTATION_VALID_BIT;
+                }
+
+                /**
+                 * Find whether the position is valid.
+                 * If not, the position is undefined.
+                 * @return Whether the position is valid.
+                 */
+                bool isPositionValid() const
+                {
+                    return _flags & POSITION_VALID_BIT;
+                }
+
+                /**
+                 * Find whether the orientation is being tracked.
+                 * If not, the orientation may only be an estimate.
+                 * @return Whether the orientation is being tracked.
+                 */
+                bool isOrientationTracked() const
+                {
+                    return _flags & ORIENTATION_TRACKED_BIT;
+                }
+
+                /**
+                 * Find whether the position is being tracked.
+                 * If not, the position may only be an estimate.
+                 * @return Whether the position is being tracked.
+                 */
+                bool isPositionTracked() const
+                {
+                    return _flags & POSITION_TRACKED_BIT;
+                }
+
+                /// Get the flags which indicate validity and tracking.
+                Flags getFlags() const
+                {
+                    return _flags;
+                }
+
+                /**
+                 * Get the pose action's orientation as a quaternion.
+                 * Get the pose action's orientation relative to the default
+                 * reference space as an OSG quaternion.
+                 *
+                 * The orientation is undefined if isOrientationValid() returns
+                 * false.
+                 *
+                 * The orientation may only be an estimate if
+                 * isOrientationTracked() returns false.
+                 */
+                const osg::Quat &getOrientation() const
+                {
+                    return _orientation;
+                }
+
+                /**
+                 * Get the pose action's position as a 3D vector.
+                 * Get the pose action's position relative to the default
+                 * reference space as an OSG 3D vector.
+                 *
+                 * The position is undefined if isPositionValid() returns false.
+                 *
+                 * The position may only be an estimate if isPositionTracked()
+                 * returns false.
+                 */
+                const osg::Vec3f &getPosition() const
+                {
+                    return _position;
+                }
+
+                // Comparison operators
+
+                bool operator != (const Location &other) const
+                {
+                    return _flags != other._flags ||
+                           (isOrientationValid() && _orientation != other._orientation) ||
+                           (isPositionValid() && _position != other._position);
+                }
+
+                bool operator == (const Location &other) const
+                {
+                    return !operator != (other);
+                }
+
+            protected:
+
+                Flags _flags;
+                osg::Quat _orientation;
+                osg::Vec3f _position;
+        };
+
+        /**
+         * Get the current pose of the action as a Location object.
+         * @param subaction The subaction to filter sources from, which must
+         *                  have been specified to Action::addSubaction().
+         * @return The current pose of the action.
+         */
+        Location getValue(Subaction *subaction = nullptr);
+};
+
+/// An output action for vibration.
+class OSGXR_EXPORT ActionVibration : public Action
+{
+    public:
+
+        /**
+         * Construct a vibration output action.
+         * @param actionSet The action set the action should belong to.
+         */
+        ActionVibration(ActionSet *actionSet);
+
+        /**
+         * Construct a vibration output action.
+         * @param actionSet The action set the action should belong to.
+         * @param name      The name of the OpenXR action, also used as the
+         *                  localized name.
+         */
+        ActionVibration(ActionSet *actionSet,
+                        const std::string &name);
+
+        /**
+         * Construct a vibration output action.
+         * @param actionSet     The action set the action should belong to.
+         * @param name          The name of the OpenXR action.
+         * @param localizedName The localized name for the action.
+         */
+        ActionVibration(ActionSet *actionSet,
+                        const std::string &name,
+                        const std::string &localizedName);
+
+        enum {
+            /// Indicates a minimum supported durection for a haptic device.
+            DURATION_MIN = -1,
+            /// Indicates an optimal frequency for a haptic pulse.
+            FREQUENCY_UNSPECIFIED = 0,
+        };
+
+        /**
+         * Apply haptic feedback.
+         * @param duration_ns Duration of vibration in nanoseconds.
+         * @param frequency   Frequency of vibration in Hz.
+         * @param amplitude   Amplitude of vibration between 0.0 and 1.0.
+         * @return true on success, false otherwise.
+         */
+        bool applyHapticFeedback(int64_t duration_ns, float frequency,
+                                 float amplitude);
+
+        /**
+         * Apply haptic feedback.
+         * @param subaction   The subaction to apply haptics to, which must
+         *                    have been specified to Action::addSubaction().
+         * @param duration_ns Duration of vibration in nanoseconds.
+         * @param frequency   Frequency of vibration in Hz.
+         * @param amplitude   Amplitude of vibration between 0.0 and 1.0.
+         * @return true on success, false otherwise.
+         */
+        bool applyHapticFeedback(Subaction *subaction,
+                                 int64_t duration_ns, float frequency,
+                                 float amplitude);
+
+        /**
+         * Stop any in-progress haptic feedback.
+         * @param subaction   The subaction to apply haptics to, which must
+         *                    have been specified to Action::addSubaction().
+         * @return true on success, false otherwise.
+         */
+        bool stopHapticFeedback(Subaction *subaction = nullptr);
+};
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/include/osgXR/ActionSet b/3rdparty/osgXR/include/osgXR/ActionSet
new file mode 100644
index 000000000..63bb9eb51
--- /dev/null
+++ b/3rdparty/osgXR/include/osgXR/ActionSet
@@ -0,0 +1,138 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_ActionSet
+#define OSGXR_ActionSet 1
+
+#include <osgXR/Export>
+
+#include <osg/Referenced>
+
+#include <cstdint>
+#include <memory>
+#include <string>
+
+namespace osgXR {
+
+class Manager;
+class Subaction;
+
+/**
+ * Represents a group of OpenXR actions.
+ * Action sets are attached to the OpenXR session, and can be dynamically
+ * activated and deactivated.
+ */
+class OSGXR_EXPORT ActionSet : public osg::Referenced
+{
+    public:
+
+        /**
+         * Construct an action set.
+         * @param manager The VR manager object to add the action set to.
+         */
+        ActionSet(Manager *manager);
+
+        /**
+         * Construct an action set.
+         * @param manager The VR manager object to add the action set to.
+         * @param name    The name of the OpenXR action set, also used as the
+         *                localized name.
+         */
+        ActionSet(Manager *manager,
+                  const std::string &name);
+
+        /**
+         * Construct an action set.
+         * @param manager       The VR manager object to add the action set to.
+         * @param name          The name of the OpenXR action set.
+         * @param localizedName The localized name for the action set.
+         */
+        ActionSet(Manager *manager,
+                  const std::string &name,
+                  const std::string &localizedName);
+
+        /// Destructor.
+        ~ActionSet();
+
+        // Accessors
+
+        /**
+         * Set the action set's name and localized name.
+         * @param name          New name for OpenXR action set.
+         * @param localizedName A localized version of @p name.
+         */
+        void setName(const std::string &name,
+                     const std::string &localizedName);
+
+        /**
+         * Set the action set's name.
+         * @param name New name for OpenXR action set.
+         */
+        void setName(const std::string &name);
+        /// Get the action's name.
+        const std::string &getName() const;
+
+        /**
+         * Set the action set's localized name.
+         * @param localizedName The localized name for the action set.
+         */
+        void setLocalizedName(const std::string &localizedName);
+        /// Get the action set's localized name.
+        const std::string &getLocalizedName() const;
+
+        /**
+         * Set the priority of the action set.
+         * @param priority New priority of the action set. Larger priority
+         *                 action sets take precedence over smaller priority
+         *                 action sets.
+         */
+        void setPriority(uint32_t priority);
+        /// Get the priority of the action set.
+        uint32_t getPriority() const;
+
+        // Activation of the action set
+
+        /**
+         * Activate the action set within a subaction.
+         * Set the action set as active so that its actions (filtered by
+         * subaction) are synchronised each frame. If @p subaction is nullptr,
+         * all subactions in the set will be synchronised, otherwise multiple
+         * subactions can be activated by multiple calls.
+         * @param subaction The subaction to activate this action set within.
+         *                  May be nullptr (default) in which case all
+         *                  subactions are activated.
+         */
+        void activate(Subaction *subaction = nullptr);
+
+        /**
+         * Deactivate the action set within a subaction.
+         * Set the action set as inactive so that its actions (filtered by
+         * subaction) are no longer synchronised each frame. If @p subaction is
+         * nullptr, any full activation is removed, otherwise multiple
+         * subactions can be deactivated by multiple calls.
+         * @param subaction The subaction to deactivate this action set within.
+         *                  May be nullptr (default) in which case all
+         *                  subactions activations are removed.
+         */
+        void deactivate(Subaction *subaction = nullptr);
+
+        /**
+         * Find whether the action set is activated for any subactions.
+         * @return Whether any subactions are activated for this action set.
+         */
+        bool isActive();
+
+        class Private;
+
+    private:
+
+        std::unique_ptr<Private> _private;
+
+        // Copying not permitted
+        ActionSet(const ActionSet &copy);
+};
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/include/osgXR/Export b/3rdparty/osgXR/include/osgXR/Export
new file mode 100644
index 000000000..ab58b4c28
--- /dev/null
+++ b/3rdparty/osgXR/include/osgXR/Export
@@ -0,0 +1,22 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_Export
+#define OSGXR_Export 1
+
+#include <osgXR/Config>
+
+#if defined(_MSC_VER) || defined(__CYGWIN__) || defined(__MINGW32__)
+    #if defined(OSGXR_STATIC_LIBRARY)
+        #define OSGXR_EXPORT
+    #elif defined(OSGXR_LIBRARY)
+        #define OSGXR_EXPORT __declspec(dllexport)
+    #else
+        #define OSGXR_EXPORT __declspec(dllimport)
+    #endif
+#else
+    #define OSGXR_EXPORT
+#endif
+
+#endif
diff --git a/3rdparty/osgXR/include/osgXR/InteractionProfile b/3rdparty/osgXR/include/osgXR/InteractionProfile
new file mode 100644
index 000000000..71b9d4e71
--- /dev/null
+++ b/3rdparty/osgXR/include/osgXR/InteractionProfile
@@ -0,0 +1,74 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_InteractionProfile
+#define OSGXR_InteractionProfile 1
+
+#include <osgXR/Export>
+
+#include <osg/Referenced>
+
+#include <memory>
+#include <string>
+
+namespace osgXR {
+
+class Action;
+class Manager;
+
+/**
+ * Represents a group of suggested bindings for a specific interaction profile.
+ * This class allow the application to suggest bindings for actions to specific
+ * input paths for a given interaction profile. If the OpenXR runtime recognises
+ * the profile it may use the suggested bindings to bind actions to whichever
+ * input devices the user may have, even without a specific binding to that
+ * device.
+ */
+class OSGXR_EXPORT InteractionProfile : public osg::Referenced
+{
+    public:
+
+        /**
+         * Construct an interaction profile.
+         * The OpenXR interaction profile path is constructed as
+         * "/interaction_profiles/@p vendor /@p type ".
+         * @param manager The VR manager object to add the action set to.
+         * @param vendor  Vendor segment of OpenXR interaction profile path.
+         * @param type    Type segment of OpenXR interaction profile path.
+         */
+        InteractionProfile(Manager *manager,
+                           const std::string &vendor,
+                           const std::string &type);
+
+        /// Destructor
+        ~InteractionProfile();
+
+        // Accessors
+
+        /// Get the vendor segment of the OpenXR interaction profile path.
+        const std::string &getVendor() const;
+
+        /// Get the type segment of the OpenXR interaction profile path.
+        const std::string &getType() const;
+
+        /**
+         * Suggest a binding for an action.
+         * @param action  The action to bind.
+         * @param binding The OpenXR path to bind the action to.
+         */
+        void suggestBinding(Action *action, const std::string &binding);
+
+        class Private;
+
+    private:
+
+        std::unique_ptr<Private> _private;
+
+        // Copying not permitted
+        InteractionProfile(const InteractionProfile &copy);
+};
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/include/osgXR/Manager b/3rdparty/osgXR/include/osgXR/Manager
new file mode 100644
index 000000000..bbd53819a
--- /dev/null
+++ b/3rdparty/osgXR/include/osgXR/Manager
@@ -0,0 +1,237 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_Manager
+#define OSGXR_Manager 1
+
+#include <osg/Camera>
+#include <osg/Node>
+#include <osg/ref_ptr>
+
+#include <osgViewer/View>
+#include <osgViewer/ViewerBase>
+
+#include <osgXR/Export>
+#include <osgXR/Mirror>
+#include <osgXR/Settings>
+#include <osgXR/View>
+
+#include <list>
+
+namespace osgXR {
+
+// Internal state class
+class XRState;
+
+/**
+ * Public VR state manager class.
+ * Applications can extend this class to allow tighter integration with osgXR.
+ */
+class OSGXR_EXPORT Manager : public osgViewer::ViewConfig
+{
+    public:
+
+        Manager();
+        virtual ~Manager();
+
+        /// Use if viewer is a CompositeViewer.
+        void setViewer(osgViewer::ViewerBase *viewer)
+        {
+            _viewer = viewer;
+        }
+
+        /// Set the NodeMasks to use for visibility masks.
+        void setVisibilityMaskNodeMasks(osg::Node::NodeMask left,
+                                        osg::Node::NodeMask right) const;
+
+        void configure(osgViewer::View& view) const override;
+
+        /**
+         * Perform a regular update.
+         * This will poll for OpenXR events, and handle any pending VR start /
+         * stop operations (possibly invoking the Manager's view callbacks).
+         * Some of these operations require threading on the viewer to be
+         * temporarily stopped, but in all cases it is started again.
+         */
+        virtual void update();
+
+        /// Find whether state has changed since last call, and reset.
+        bool checkAndResetStateChanged();
+
+        /// Find whether VR seems to be present.
+        bool getPresent() const;
+
+        /**
+         * Get whether VR is currently set to be enabled.
+         * When enabled, osgXR will try to keep VR running.
+         * @return Whether VR is enabled
+         */
+        bool getEnabled() const;
+        /**
+         * Set whether VR is currently set to be enabled.
+         * When enabled, osgXR will try to keep VR running.
+         * @param enabled Whether VR is enabled.
+         */
+        void setEnabled(bool enabled);
+
+        /**
+         * Start destroying the VR state and wait for safe shutdown.
+         */
+        void destroyAndWait();
+
+        /**
+         * Find whether this manager is in the process of being destroyed.
+         */
+        bool isDestroying() const;
+
+        /**
+         * Get whether a VR session is currently running.
+         * @return Whether a VR session is currently running.
+         */
+        bool isRunning() const;
+
+        /// Arrange reinit as needed for new settings.
+        void syncSettings();
+
+        /// Arrange reinit as needed of action setup.
+        void syncActionSetup();
+
+        /*
+         * OpenXR information.
+         */
+
+        /**
+         * Find whether OpenXR's validation layer is supported.
+         * This looks to see whether the OpenXR validation API layer (i.e.
+         * XR_APILAYER_LUNARG_core_validation) is available.
+         */
+        bool hasValidationLayer() const;
+
+        /**
+         * Find whether OpenXR supports the submission of depth information.
+         * This looks to see whether the OpenXR instance extension for
+         * submitting depth information to help the runtime perform better
+         * reprojection (i.e. XR_KHR_composition_layer_depth) is available.
+         */
+        bool hasDepthInfoExtension() const;
+
+        /**
+         * Find whether OpenXR supports the visibility mask extension.
+         * This looks to see whether the OpenXR instance extension for getting
+         * visibility masks is available, which can be used to reduce fragment
+         * load.
+         */
+        bool hasVisibilityMaskExtension() const;
+
+        /// Find the name of the OpenXR runtime.
+        const char *getRuntimeName() const;
+
+        /// Find the name of the OpenXR system in use.
+        const char *getSystemName() const;
+
+        /// Get a string describing the state (for user consumption).
+        const char *getStateString() const;
+
+        /*
+         * For implementation by derived classes.
+         */
+
+        /**
+         * Callback telling the app to configure a new view.
+         * This callback allows osgXR to tell the app to configure a new view of
+         * the world. The application should notify osgXR of the addition and
+         * removal of slave cameras which osgXR should hook into using the
+         * osgXR::View parameter.
+         * The implementation may stop threading, and it will be started again
+         * before update() returns.
+         * @param xrView The new osgXR::View with a public API to allow the
+         *               application to retrieve what it needs in relation to
+         *               the view and to inform osgXR of changes.
+         */
+        virtual void doCreateView(View *xrView) = 0;
+
+        /**
+         * Callback telling the app to destroy an existing view.
+         * This callback allows osgXR to tell the app to remove an existing view
+         * of the world that it had requested via doCreateView(). The
+         * application should notify osgXR of the removal of any slave cameras
+         * which it has already informed osgXR about.
+         * The implementation may stop threading, and it will be started again
+         * before update() returns.
+         */
+        virtual void doDestroyView(View *xrView) = 0;
+
+        /**
+         * Callback telling the app that the VR session is now running.
+         * This happens after the OpenXR session has started running, and views
+         * have been configured (see doCreateView()). The app should start
+         * rendering the VR views, and may choose to reconfigure the desktop
+         * window to make a VR mirror visible.
+         */
+        virtual void onRunning();
+
+        /**
+         * Callback telling the app that the VR session has now stopped.
+         * This happens after the OpenXR session has stopped, and views have
+         * been removed (see doDestroyView()). The app should stop rendering the
+         * VR views, and may choose to reconfigure the desktop window so as to
+         * no longer show a VR mirror.
+         */
+        virtual void onStopped();
+
+        /**
+         * Callback telling the app that the VR session is in focus.
+         * This happens when the VR session enters focus and can get VR input
+         * from the user. The app may choose to resume the experience if it was
+         * previously paused due to onUnfocus().
+         */
+        virtual void onFocus();
+
+        /**
+         * Callback telling the app that the VR session is no longer in focus.
+         * This happens when the VR session leaves focus and can no longer get
+         * VR input from the user. The VR runtime may be presenting a modal
+         * pop-up on top of the application's rendered frames. The app may
+         * choose to pause the experience.
+         */
+        virtual void onUnfocus();
+
+
+        /// Add a custom mirror to the queue of mirrors to configure.
+        void addMirror(Mirror *mirror);
+
+        /// Set up a camera to render a VR mirror.
+        void setupMirrorCamera(osg::Camera *camera);
+
+        /*
+         * Internal
+         */
+
+        inline Settings *_getSettings()
+        {
+            return _settings.get();
+        }
+
+        inline XRState *_getXrState()
+        {
+            return _state;
+        }
+
+        void _setupMirrors();
+
+    protected:
+
+        osg::ref_ptr<osgViewer::ViewerBase> _viewer;
+        osg::ref_ptr<Settings> _settings;
+        bool _destroying;
+
+    private:
+
+        std::list<osg::ref_ptr<Mirror> > _mirrorQueue;
+        osg::ref_ptr<XRState> _state;
+};
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/include/osgXR/Mirror b/3rdparty/osgXR/include/osgXR/Mirror
new file mode 100644
index 000000000..ce0724e22
--- /dev/null
+++ b/3rdparty/osgXR/include/osgXR/Mirror
@@ -0,0 +1,50 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_Mirror
+#define OSGXR_Mirror 1
+
+#include <osg/Camera>
+#include <osg/Referenced>
+#include <osg/ref_ptr>
+
+#include <osgXR/Export>
+#include <osgXR/MirrorSettings>
+
+namespace osgXR {
+
+class Manager;
+
+/**
+ * Public VR mirror class.
+ */
+class OSGXR_EXPORT Mirror : public osg::Referenced
+{
+    public:
+
+        Mirror(Manager *manager, osg::Camera *camera);
+        virtual ~Mirror();
+
+        /*
+         * internal
+         */
+
+        // Called when enough is known about OpenXR system
+        void _init();
+
+    private:
+
+        void setupQuad(unsigned int viewIndex,
+                       float x, float w);
+
+        osg::observer_ptr<Manager> _manager;
+        osg::observer_ptr<osg::Camera> _camera;
+
+        MirrorSettings _mirrorSettings;
+};
+
+}
+
+#endif
+
diff --git a/3rdparty/osgXR/include/osgXR/MirrorSettings b/3rdparty/osgXR/include/osgXR/MirrorSettings
new file mode 100644
index 000000000..1ad315e30
--- /dev/null
+++ b/3rdparty/osgXR/include/osgXR/MirrorSettings
@@ -0,0 +1,72 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_MirrorSettings
+#define OSGXR_MirrorSettings 1
+
+#include <osgXR/Export>
+
+namespace osgXR {
+
+class OSGXR_EXPORT MirrorSettings
+{
+    public:
+
+        MirrorSettings();
+
+        /// Equality operator.
+        bool operator == (const MirrorSettings &other) const
+        {
+            return _mirrorMode == other._mirrorMode &&
+                   (_mirrorMode != MIRROR_SINGLE ||
+                    _mirrorViewIndex == other._mirrorViewIndex);
+        }
+
+        /// Inequality operator.
+        bool operator != (const MirrorSettings &other) const
+        {
+            return _mirrorMode != other._mirrorMode ||
+                   (_mirrorMode == MIRROR_SINGLE &&
+                    _mirrorViewIndex != other._mirrorViewIndex);
+        }
+
+        /// Type of VR mirror to show.
+        typedef enum MirrorMode
+        {
+            /// Choose automatically.
+            MIRROR_AUTOMATIC,
+            /// Render nothing to the mirror.
+            MIRROR_NONE,
+            /// Render a single view fullscreen to the mirror.
+            MIRROR_SINGLE,
+            /// Render left & right views side by side.
+            MIRROR_LEFT_RIGHT,
+        } MirrorMode;
+        /// Set the mirror mode to use.
+        void setMirror(MirrorMode mode, int viewIndex = -1)
+        {
+            _mirrorMode = mode;
+            _mirrorViewIndex = viewIndex;
+        }
+        /// Get the mirror mode to use.
+        MirrorMode getMirrorMode() const
+        {
+            return _mirrorMode;
+        }
+        /// Get the mirror view index.
+        int getMirrorViewIndex() const
+        {
+            return _mirrorViewIndex;
+        }
+
+    protected:
+
+        // Mirror mode
+        MirrorMode _mirrorMode;
+        int _mirrorViewIndex;
+};
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/include/osgXR/OpenXRDisplay b/3rdparty/osgXR/include/osgXR/OpenXRDisplay
new file mode 100644
index 000000000..6639e92c6
--- /dev/null
+++ b/3rdparty/osgXR/include/osgXR/OpenXRDisplay
@@ -0,0 +1,49 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OpenXRDisplay
+#define OSGXR_OpenXRDisplay 1
+
+#include <osgXR/Export>
+#include <osgXR/Settings>
+
+#include <osg/Referenced>
+#include <osg/ref_ptr>
+#include <osgViewer/View>
+
+#include <cinttypes>
+#include <string>
+
+namespace osgXR {
+
+class XRState;
+
+/** a camera for each OpenXR view.*/
+class OSGXR_EXPORT OpenXRDisplay : public osgViewer::ViewConfig
+{
+    public:
+
+        OpenXRDisplay();
+        OpenXRDisplay(Settings *settings);
+
+        OpenXRDisplay(const OpenXRDisplay& rhs,
+                      const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY);
+        virtual ~OpenXRDisplay();
+
+        META_Object(osgXR, OpenXRDisplay);
+
+        void configure(osgViewer::View& view) const override;
+
+    protected:
+
+        osg::ref_ptr<Settings> _settings;
+
+        // Internal OpenXR state object
+        // FIXME this should probably belong elsewhere
+        mutable osg::ref_ptr<XRState> _state;
+};
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/include/osgXR/Settings b/3rdparty/osgXR/include/osgXR/Settings
new file mode 100644
index 000000000..a413492f9
--- /dev/null
+++ b/3rdparty/osgXR/include/osgXR/Settings
@@ -0,0 +1,345 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_Settings
+#define OSGXR_Settings 1
+
+#include <osg/Referenced>
+
+#include <osgXR/Export>
+#include <osgXR/MirrorSettings>
+
+#include <string>
+
+namespace osgXR {
+
+/// Encapsulates osgXR / OpenXR settings data.
+class OSGXR_EXPORT Settings : public osg::Referenced
+{
+    public:
+
+        /*
+         * Instance management.
+         */
+
+        Settings();
+        virtual ~Settings();
+
+        /// Get the default/global instance of Settings.
+        static Settings *instance();
+
+        /*
+         * OpenXR application information.
+         */
+
+        /**
+         * Set the application's name and version to expose to OpenXR.
+         * These will be used to create an OpenXR instance.
+         * @param appName    Name of the application.
+         * @param appVersion 32-bit version number of the application.
+         */
+        void setApp(const std::string &appName, uint32_t appVersion)
+        {
+            _appName = appName;
+            _appVersion = appVersion;
+        }
+
+        /**
+         * Set the application's name to expose to OpenXR.
+         * This will be used to create an OpenXR instance.
+         * @param appName    Name of the application.
+         */
+        void setAppName(const std::string &appName)
+        {
+            _appName = appName;
+        }
+        /// Get the application's name to expose to OpenXR.
+        const std::string &getAppName() const
+        {
+            return _appName;
+        }
+
+        /**
+         * Set the application's version to expose to OpenXR.
+         * This will be used to create an OpenXR instance.
+         * @param appVersion 32-bit version number of the application.
+         */
+        void setAppVersion(uint32_t appVersion)
+        {
+            _appVersion = appVersion;
+        }
+        /// Get the application's 32-bit version number to expose to OpenXR.
+        uint32_t getAppVersion() const
+        {
+            return _appVersion;
+        }
+
+
+        /*
+         * osgXR configuration settings.
+         */
+
+        /**
+         * Set whether to try enabling OpenXR's validation layer.
+         * This controls whether the OpenXR validation API layer (i.e.
+         * XR_APILAYER_LUNARG_core_validation) will be enabled when creating an
+         * OpenXR instance.
+         * By default this is disabled.
+         * @param validationLayer Whether to try enabling the validation layer.
+         */
+        void setValidationLayer(bool validationLayer)
+        {
+            _validationLayer = validationLayer;
+        }
+        /// Get whether to try enabling OpenXR's validation layer.
+        bool getValidationLayer() const
+        {
+            return _validationLayer;
+        }
+
+        /**
+         * Set whether to enable submission of depth information to OpenXR.
+         * This controls whether the OpenXR instance depth information extension
+         * (i.e XR_KHR_composition_layer_depth) will be used to submit depth
+         * information to OpenXR to allow improved reprojection.
+         * This is currently disabled by default.
+         * @param depthInfo Whether to enable submission of depth information.
+         */
+        void setDepthInfo(bool depthInfo)
+        {
+            _depthInfo = depthInfo;
+        }
+        /// Get whether to enable submission of depth information to OpenXR.
+        bool getDepthInfo() const
+        {
+            return _depthInfo;
+        }
+
+        /**
+         * Set whether to create visibility masks.
+         * This controls whether the OpenXR instance visibility mask extension
+         * (i.e. XR_KHR_visibility_mask) will be used to create and update
+         * visibility masks for each VR view in order to mask hidden fragments.
+         * This is enabled by default.
+         * @param visibilityMask Whether to create visibility masks.
+         */
+        void setVisibilityMask(bool visibilityMask)
+        {
+            _visibilityMask = visibilityMask;
+        }
+        /// Get whether to create visibility masks.
+        bool getVisibilityMask() const
+        {
+            return _visibilityMask;
+        }
+
+        /// OpenXR system orm factors.
+        typedef enum FormFactor
+        {
+            /// A display mounted to the user's head.
+            HEAD_MOUNTED_DISPLAY,
+            /// A display held in the user's hands.
+            HANDHELD_DISPLAY,
+        } FormFactor;
+        /**
+         * Set which OpenXR form factor to use.
+         * This controls which OpenXR form factor to try to use. The default is
+         * HEAD_MOUNTED_DISPLAY.
+         * @param formFactor Form factor to use.
+         */
+        void setFormFactor(FormFactor formFactor)
+        {
+            _formFactor = formFactor;
+        }
+        /// Get which OpenXR form factor to use.
+        FormFactor getFormFactor() const
+        {
+            return _formFactor;
+        }
+
+        /// Modes for blending layers onto the user's view of the real world.
+        typedef enum BlendMode
+        {
+            // Matches XrEnvironmentBlendMode
+            /// Display layers with no view of physical world behind.
+            OPAQUE = 1,
+            /// Additively blend layers with view of physical world behind.
+            ADDITIVE = 2,
+            /// Alpha blend layers with view of physical world behind.
+            ALPHA_BLEND = 3,
+        } BlendMode;
+        /**
+         * Specify a preferred environment blend mode.
+         * The chosen environment blend mode is permitted for use, and will be
+         * chosen in preference to any other supported environment blend modes
+         * specified by allowEnvBlendMode() if supported by OpenXR.
+         * @param mode Environment blend mode to prefer.
+         */
+        void preferEnvBlendMode(BlendMode mode)
+        {
+            uint32_t mask = (1u << (unsigned int)mode);
+            _preferredEnvBlendModeMask |= mask;
+            _allowedEnvBlendModeMask |= mask;
+        }
+        /**
+         * Specify a permitted environment blend mode.
+         * The chosen environment blend mode is permitted for use, and may be
+         * chosen if supported by OpenXR when none of the preferred environment
+         * blend modes specified by preferEnvBlenMode() are supported by OpenXR.
+         * @param mode Environment blend mode to prefer.
+         */
+        void allowEnvBlendMode(BlendMode mode)
+        {
+            uint32_t mask = (1u << (unsigned int)mode);
+            _allowedEnvBlendModeMask |= mask;
+        }
+        /// Get the bitmask of preferred environment blend modes.
+        uint32_t getPreferredEnvBlendModeMask() const
+        {
+            return _preferredEnvBlendModeMask;
+        }
+        /// Set the bitmask of preferred environment blend modes.
+        void setPreferredEnvBlendModeMask(uint32_t preferredEnvBlendModeMask)
+        {
+            _preferredEnvBlendModeMask = preferredEnvBlendModeMask;
+        }
+        /// Get the bitmask of permitted environment blend modes.
+        uint32_t getAllowedEnvBlendModeMask() const
+        {
+            return _allowedEnvBlendModeMask;
+        }
+        /// Set the bitmask of allowed environment blend modes.
+        void setAllowedEnvBlendModeMask(uint32_t allowedEnvBlendModeMask)
+        {
+            _allowedEnvBlendModeMask = allowedEnvBlendModeMask;
+        }
+
+        /// Techniques for rendering multiple views.
+        typedef enum VRMode
+        {
+            /// Choose automatically.
+            VRMODE_AUTOMATIC,
+            /** Create a slave camera for each view.
+             * Either separate swapchains, or single with multiple viewports.
+             */
+            VRMODE_SLAVE_CAMERAS,
+            /** Use the OSG SceneView stereo functionality.
+             * No extra slave cameras.
+             * Only supports SWAPCHAIN_SINGLE with stereo.
+             */
+            VRMODE_SCENE_VIEW,
+        } VRMode;
+        /// Set the rendering technique to use.
+        void setVRMode(VRMode mode)
+        {
+            _vrMode = mode;
+        }
+        /// Get the rendering technique to use.
+        VRMode getVRMode() const
+        {
+            return _vrMode;
+        }
+
+        /// Techniques for managing swapchains.
+        typedef enum SwapchainMode
+        {
+            /// Choose automatically.
+            SWAPCHAIN_AUTOMATIC,
+            /// Create a 2D swapchain per view.
+            SWAPCHAIN_MULTIPLE,
+            /** Create a single 2D swapchain with a viewport per view.
+             * Stack them horizontally.
+             */
+            SWAPCHAIN_SINGLE,
+        } SwapchainMode;
+        /// Set the swapchain management technique to use.
+        void setSwapchainMode(SwapchainMode mode)
+        {
+            _swapchainMode = mode;
+        }
+        /// Get the swapchain management technique to use.
+        SwapchainMode getSwapchainMode() const
+        {
+            return _swapchainMode;
+        }
+
+        /// Get mirror settings.
+        MirrorSettings &getMirrorSettings()
+        {
+            return _mirrorSettings;
+        }
+        /// Get mirror settings.
+        const MirrorSettings &getMirrorSettings() const
+        {
+            return _mirrorSettings;
+        }
+
+        /**
+         * Set the number of virtual world units to fit per real world meter.
+         * This controls the size of the user relative to the virtual world, by
+         * scaling down the size of the world.
+         * @param unitsPerMeter The number of units per real world meter.
+         */
+        void setUnitsPerMeter(float unitsPerMeter)
+        {
+            _unitsPerMeter = unitsPerMeter;
+        }
+        /// Get the number of virtual world units to fit per real world meter.
+        float getUnitsPerMeter() const
+        {
+            return _unitsPerMeter;
+        }
+
+        // Internal APIs
+
+        typedef enum {
+            DIFF_NONE             = 0,
+            DIFF_APP_INFO         = (1u << 0),
+            DIFF_VALIDATION_LAYER = (1u << 1),
+            DIFF_DEPTH_INFO       = (1u << 2),
+            DIFF_VISIBILITY_MASK  = (1u << 3),
+            DIFF_FORM_FACTOR      = (1u << 4),
+            DIFF_BLEND_MODE       = (1u << 5),
+            DIFF_VR_MODE          = (1u << 6),
+            DIFF_SWAPCHAIN_MODE   = (1u << 7),
+            DIFF_MIRROR           = (1u << 8),
+            DIFF_SCALE            = (1u << 9),
+        } _ChangeMask;
+
+        unsigned int _diff(const Settings &other) const;
+
+    private:
+
+        /*
+         * Internal data.
+         */
+
+        // For XrInstance creation
+        std::string _appName;
+        uint32_t _appVersion;
+        bool _validationLayer;
+        bool _depthInfo;
+        bool _visibilityMask;
+
+        // To get XrSystem
+        FormFactor _formFactor;
+
+        // For choosing environment blend mode
+        uint32_t _preferredEnvBlendModeMask;
+        uint32_t _allowedEnvBlendModeMask;
+
+        // VR/swapchain modes to use
+        VRMode _vrMode;
+        SwapchainMode _swapchainMode;
+
+        // Mirror settings
+        MirrorSettings _mirrorSettings;
+
+        // How big the world
+        float _unitsPerMeter;
+};
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/include/osgXR/Subaction b/3rdparty/osgXR/include/osgXR/Subaction
new file mode 100644
index 000000000..fc9d9e00a
--- /dev/null
+++ b/3rdparty/osgXR/include/osgXR/Subaction
@@ -0,0 +1,75 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_Subaction
+#define OSGXR_Subaction 1
+
+#include <osgXR/Export>
+
+#include <osg/Referenced>
+#include <osg/ref_ptr>
+
+#include <memory>
+#include <string>
+
+namespace osgXR {
+
+class InteractionProfile;
+class Manager;
+
+/**
+ * Represents an OpenXR subaction path (a.k.a top level user path).
+ * This represents an OpenXR subaction path, also referred to in the OpenXR spec
+ * as a top level user path. These are the physical groupings of inputs, for
+ * example "/user/head" or "/user/hand/left". Actions and action sets can be
+ * filtered by subactions, so that the same action (e.g. "shoot") can be read
+ * separately for different hands.
+ */
+class OSGXR_EXPORT Subaction : public osg::Referenced
+{
+    public:
+
+        /**
+         * Construct a subaction for a path.
+         * @param manager The VR manager object to add the action set to.
+         * @param path    The subaction path, e.g. "/user/hand/left".
+         */
+        Subaction(Manager *manager,
+                  const std::string &path);
+
+        /// Destructor
+        virtual ~Subaction();
+
+        // Accessors
+
+        /// Get the subaction's path.
+        const std::string &getPath() const;
+
+        /// Find the interaction profile bound to the subaction.
+        InteractionProfile *getCurrentProfile();
+
+        class Private;
+
+    protected:
+
+        // Change handlers
+
+        /**
+         * Notification of change of interaction profile for subaction.
+         * This is called when the subaction's current interaction profile is
+         * changed. Derived classes can implement this to their own ends.
+         * @param newProfile The interaction profile object that is now
+         *                   current for the subaction indicated by @p
+         *                   subaction. May be nullptr.
+         */
+        virtual void onProfileChanged(InteractionProfile *newProfile);
+
+    private:
+
+        std::shared_ptr<Private> _private;
+};
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/include/osgXR/View b/3rdparty/osgXR/include/osgXR/View
new file mode 100644
index 000000000..1e227c1b2
--- /dev/null
+++ b/3rdparty/osgXR/include/osgXR/View
@@ -0,0 +1,79 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_View
+#define OSGXR_View 1
+
+#include <osgXR/Export>
+
+#include <osg/Camera>
+#include <osg/ref_ptr>
+
+#include <osgViewer/GraphicsWindow>
+#include <osgViewer/View>
+
+namespace osgXR {
+
+/**
+ * Representation of a view from an osgXR app point of view.
+ * This represents a render view that osgXR expects the application to set up.
+ * This may not directly correspond to OpenXR views, for example if using stereo
+ * SceneView mode there will be a single view set up for stereo rendering.
+ */
+class OSGXR_EXPORT View : public osg::Referenced
+{
+    public:
+
+        /*
+         * Application -> osgXR notifications.
+         */
+
+        /**
+         * Notify osgXR that a new slave camera has been added to the view.
+         * This tells osgXR that a new slave camera has been added to the view
+         * which it should hook into so that it renders to the appropriate
+         * texture and submits for XR display.
+         */
+        virtual void addSlave(osg::Camera *slaveCamera) = 0;
+
+        /**
+         * Notify osgXR that a slave camera is being removed from the view.
+         * This tells osgXR when a slave camera previously notified with
+         * addSlave() is being removed.
+         */
+        virtual void removeSlave(osg::Camera *slaveCamera) = 0;
+
+        /*
+         * Accessors.
+         */
+
+        /// Get the OSG GraphicsWindow associated with this osgXR view.
+        inline const osgViewer::GraphicsWindow *getWindow() const
+        {
+            return _window;
+        }
+
+        /// Get the OSG View associated with this osgXR view.
+        inline const osgViewer::View *getView() const
+        {
+            return _osgView;
+        }
+
+    protected:
+
+        /*
+         * Internal
+         */
+
+        View(osgViewer::GraphicsWindow *window, osgViewer::View *osgView);
+        virtual ~View();
+
+        osg::ref_ptr<osgViewer::GraphicsWindow> _window;
+        osg::ref_ptr<osgViewer::View> _osgView;
+
+};
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/include/osgXR/osgXR b/3rdparty/osgXR/include/osgXR/osgXR
new file mode 100644
index 000000000..8baa33db3
--- /dev/null
+++ b/3rdparty/osgXR/include/osgXR/osgXR
@@ -0,0 +1,22 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_osgXR
+#define OSGXR_osgXR 1
+
+#include <osgXR/Export>
+
+#include <osgViewer/Viewer>
+
+#include <string>
+
+namespace osgXR {
+
+void OSGXR_EXPORT setupViewerDefaults(osgViewer::Viewer *viewer,
+                                      const std::string &appName,
+                                      uint32_t appVersion);
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/osgXR.pc.in b/3rdparty/osgXR/osgXR.pc.in
new file mode 100644
index 000000000..59105e39d
--- /dev/null
+++ b/3rdparty/osgXR/osgXR.pc.in
@@ -0,0 +1,12 @@
+prefix=@CMAKE_INSTALL_PREFIX@
+exec_prefix=@CMAKE_INSTALL_PREFIX@
+libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@
+includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@
+
+Name: @PROJECT_NAME@
+Description: @PROJECT_DESCRIPTION@
+Version: @PROJECT_VERSION@
+
+Requires: openscenegraph OpenXR
+Libs: -L${libdir} -lmylib
+Cflags: -I${includedir}
diff --git a/3rdparty/osgXR/src/Action.cpp b/3rdparty/osgXR/src/Action.cpp
new file mode 100644
index 000000000..4ccaf537c
--- /dev/null
+++ b/3rdparty/osgXR/src/Action.cpp
@@ -0,0 +1,507 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "Action.h"
+#include "ActionSet.h"
+
+#include "OpenXR/Action.h"
+#include "OpenXR/Session.h"
+#include "OpenXR/Space.h"
+
+#include <map>
+
+using namespace osgXR;
+
+// Internal API
+
+Action::Private::Private(ActionSet *actionSet) :
+    _actionSet(actionSet),
+    _updated(true)
+{
+    ActionSet::Private::get(_actionSet)->registerAction(this);
+}
+
+Action::Private::~Private()
+{
+    ActionSet::Private::get(_actionSet)->unregisterAction(this);
+}
+
+void Action::Private::setName(const std::string &name)
+{
+    _updated = true;
+    _name = name;
+}
+
+const std::string &Action::Private::getName() const
+{
+    return _name;
+}
+
+void Action::Private::setLocalizedName(const std::string &localizedName)
+{
+    _updated = true;
+    _localizedName = localizedName;
+}
+
+const std::string &Action::Private::getLocalizedName() const
+{
+    return _localizedName;
+}
+
+void Action::Private::addSubaction(std::shared_ptr<Subaction::Private> subaction)
+{
+    _updated = true;
+    _subactions.insert(subaction);
+}
+
+void Action::Private::cleanupInstance()
+{
+    _updated = true;
+    _action = nullptr;
+}
+
+void Action::Private::getBoundSources(std::vector<std::string> &sourcePaths) const
+{
+    OpenXR::Session *session = ActionSet::Private::get(_actionSet)->getSession();
+    if (_action.valid() && session)
+    {
+        std::vector<XrPath> paths;
+        if (session->getActionBoundSources(_action, paths))
+        {
+            // Convert XrPath's into std::string's
+            OpenXR::Instance *instance = session->getInstance();
+            sourcePaths.resize(paths.size());
+            for (unsigned int i = 0; i < paths.size(); ++i)
+                sourcePaths[i] = OpenXR::Path(instance, paths[i]).toString();
+
+            // Success!
+            return;
+        }
+    }
+
+    // Failure, clear output
+    sourcePaths.resize(0);
+}
+
+void Action::Private::getBoundSourcesLocalizedNames(XrInputSourceLocalizedNameFlags whichComponents,
+                                                    std::vector<std::string> &names) const
+{
+    OpenXR::Session *session = ActionSet::Private::get(_actionSet)->getSession();
+    if (_action.valid() && session)
+    {
+        std::vector<XrPath> paths;
+        if (session->getActionBoundSources(_action, paths))
+        {
+            // Convert XrPath's into localized names
+            names.resize(paths.size());
+            for (unsigned int i = 0; i < paths.size(); ++i)
+                names[i] = session->getInputSourceLocalizedName(paths[i],
+                                                                      whichComponents);
+
+            // Success!
+            return;
+        }
+    }
+
+    // Failure, clear output
+    names.resize(0);
+}
+
+namespace osgXR {
+
+template <typename T>
+class ActionPrivateCommon : public Action::Private
+{
+    public:
+
+        typedef typename T::State State;
+
+        ActionPrivateCommon(ActionSet *actionSet) :
+            Private(actionSet)
+        {
+        }
+
+        void cleanupSession() override
+        {
+            _states.clear();
+        }
+
+        OpenXR::Action *setup(OpenXR::Instance *instance) override
+        {
+            OpenXR::ActionSet *actionSet = ActionSet::Private::get(_actionSet)->setup(instance);
+            if (!actionSet)
+            {
+                // Can't continue without an action set
+                _action = nullptr;
+                _updated = true;
+            }
+            else if (_updated || actionSet != _action->getActionSet())
+            {
+                _action = new T(actionSet, _name, _localizedName);
+                for (auto &subaction: _subactions)
+                    _action->addSubaction(subaction->setup(instance));
+                _updated = false;
+            }
+            return _action;
+        }
+
+        State *getState(Subaction::Private *subaction = nullptr)
+        {
+            auto it = _states.find(subaction);
+            if (it != _states.end())
+                return (*it).second.get();
+
+            OpenXR::Session *session = ActionSet::Private::get(_actionSet)->getSession();
+            if (session)
+            {
+                OpenXR::Path subactionPath;
+                if (subaction)
+                    subactionPath = subaction->setup(session->getInstance());
+                OpenXR::Action *action = setup(session->getInstance());
+                if (action)
+                {
+                    osg::ref_ptr<State> ret = static_cast<T*>(_action.get())->createState(session,
+                                                                                          subactionPath);
+                    _states[subaction] = ret;
+                    return ret.get();
+                }
+            }
+            return nullptr;
+        }
+
+    protected:
+
+        std::map<Subaction::Private *, osg::ref_ptr<State>> _states;
+};
+
+template <typename T>
+class ActionPrivateSimple : public ActionPrivateCommon<T>
+{
+    public:
+
+        typedef typename T::State State;
+
+        ActionPrivateSimple(ActionSet *actionSet) :
+            ActionPrivateCommon<T>(actionSet)
+        {
+        }
+
+        auto getValue(Subaction::Private *subaction)
+        {
+            State *state = this->getState(subaction);
+            if (state && state->update() && state->isActive())
+                return state->getCurrentState();
+            else
+                return T::State::Info::defaultValue();
+        }
+};
+
+typedef ActionPrivateSimple<OpenXR::ActionBoolean>  ActionPrivateBoolean;
+typedef ActionPrivateSimple<OpenXR::ActionFloat>    ActionPrivateFloat;
+typedef ActionPrivateSimple<OpenXR::ActionVector2f> ActionPrivateVector2f;
+
+class ActionPrivatePose : public ActionPrivateCommon<OpenXR::ActionPose>
+{
+    public:
+
+        typedef OpenXR::ActionPose::State State;
+
+        ActionPrivatePose(ActionSet *actionSet) :
+            ActionPrivateCommon(actionSet)
+        {
+        }
+
+        OpenXR::Space *getSpace(Subaction::Private *subaction)
+        {
+            State *state = getState(subaction);
+            if (state && state->update() && state->isActive())
+                return state->getSpace();
+            else
+                return nullptr;
+        }
+
+        bool locate(Subaction::Private *subaction,
+                    ActionPose::Location &location)
+        {
+            OpenXR::Space *space = getSpace(subaction);
+            OpenXR::Session *session = ActionSet::Private::get(_actionSet)->getSession();
+            if (session && space)
+            {
+                OpenXR::Space::Location loc;
+                bool ret = space->locate(session->getLocalSpace(), session->getLastDisplayTime(),
+                                         loc);
+                location = ActionPose::Location((ActionPose::Location::Flags)loc.getFlags(),
+                                                loc.getOrientation(),
+                                                loc.getPosition());
+                return ret;
+            }
+            else
+            {
+                location = ActionPose::Location();
+                return false;
+            }
+        }
+};
+
+class ActionPrivateVibration : public ActionPrivateCommon<OpenXR::ActionVibration>
+{
+    public:
+
+        typedef OpenXR::ActionVibration::State State;
+
+        ActionPrivateVibration(ActionSet *actionSet) :
+            ActionPrivateCommon(actionSet)
+        {
+        }
+
+        bool applyHapticFeedback(Subaction::Private *subaction,
+                                 int64_t duration_ns, float frequency,
+                                 float amplitude)
+        {
+            State *state = getState(subaction);
+            if (!state)
+                return false;
+            return state->applyHapticFeedback(duration_ns, frequency,
+                                               amplitude);
+        }
+
+        bool stopHapticFeedback(Subaction::Private *subaction)
+        {
+            State *state = getState(subaction);
+            if (!state)
+                return false;
+            return state->stopHapticFeedback();
+        }
+};
+
+}
+
+// Public API
+
+Action::Action(Private *priv) :
+    _private(priv)
+{
+}
+
+Action::~Action()
+{
+}
+
+void Action::addSubaction(Subaction *subaction)
+{
+    _private->addSubaction(Subaction::Private::get(subaction));
+}
+
+void Action::setName(const std::string &name,
+                     const std::string &localizedName)
+{
+    _private->setName(name);
+    _private->setLocalizedName(localizedName);
+}
+
+void Action::setName(const std::string &name)
+{
+    _private->setName(name);
+}
+
+const std::string &Action::getName() const
+{
+    return _private->getName();
+}
+
+void Action::setLocalizedName(const std::string &localizedName)
+{
+    _private->setLocalizedName(localizedName);
+}
+
+const std::string &Action::getLocalizedName() const
+{
+    return _private->getLocalizedName();
+}
+
+void Action::getBoundSources(std::vector<std::string> &sourcePaths) const
+{
+    _private->getBoundSources(sourcePaths);
+}
+
+void Action::getBoundSourcesLocalizedNames(uint32_t whichComponents,
+                                           std::vector<std::string> &names) const
+{
+    _private->getBoundSourcesLocalizedNames(whichComponents, names);
+}
+
+// ActionBoolean
+
+ActionBoolean::ActionBoolean(ActionSet *actionSet) :
+    Action(new ActionPrivateBoolean(actionSet))
+{
+}
+
+ActionBoolean::ActionBoolean(ActionSet *actionSet,
+                             const std::string &name) :
+    Action(new ActionPrivateBoolean(actionSet))
+{
+    setName(name, name);
+}
+
+ActionBoolean::ActionBoolean(ActionSet *actionSet,
+                             const std::string &name,
+                             const std::string &localizedName) :
+    Action(new ActionPrivateBoolean(actionSet))
+{
+    setName(name, localizedName);
+}
+
+bool ActionBoolean::getValue(Subaction *subaction)
+{
+    auto privSubaction = Subaction::Private::get(subaction);
+    return static_cast<ActionPrivateBoolean *>(Private::get(this))->getValue(privSubaction.get());
+}
+
+// ActionFloat
+
+ActionFloat::ActionFloat(ActionSet *actionSet) :
+    Action(new ActionPrivateFloat(actionSet))
+{
+}
+
+ActionFloat::ActionFloat(ActionSet *actionSet,
+                         const std::string &name) :
+    Action(new ActionPrivateFloat(actionSet))
+{
+    setName(name, name);
+}
+
+ActionFloat::ActionFloat(ActionSet *actionSet,
+                         const std::string &name,
+                         const std::string &localizedName) :
+    Action(new ActionPrivateFloat(actionSet))
+{
+    setName(name, localizedName);
+}
+
+float ActionFloat::getValue(Subaction *subaction)
+{
+    auto privSubaction = Subaction::Private::get(subaction);
+    return static_cast<ActionPrivateFloat *>(Private::get(this))->getValue(privSubaction.get());
+}
+
+// ActionVector2f
+
+ActionVector2f::ActionVector2f(ActionSet *actionSet) :
+    Action(new ActionPrivateVector2f(actionSet))
+{
+}
+
+ActionVector2f::ActionVector2f(ActionSet *actionSet,
+                               const std::string &name) :
+    Action(new ActionPrivateVector2f(actionSet))
+{
+    setName(name, name);
+}
+
+ActionVector2f::ActionVector2f(ActionSet *actionSet,
+                               const std::string &name,
+                               const std::string &localizedName) :
+    Action(new ActionPrivateVector2f(actionSet))
+{
+    setName(name, localizedName);
+}
+
+osg::Vec2f ActionVector2f::getValue(Subaction *subaction)
+{
+    auto privSubaction = Subaction::Private::get(subaction);
+    return static_cast<ActionPrivateVector2f *>(Private::get(this))->getValue(privSubaction.get());
+}
+
+// ActionPose
+
+ActionPose::ActionPose(ActionSet *actionSet) :
+    Action(new ActionPrivatePose(actionSet))
+{
+}
+
+ActionPose::ActionPose(ActionSet *actionSet,
+                       const std::string &name) :
+    Action(new ActionPrivatePose(actionSet))
+{
+    setName(name, name);
+}
+
+ActionPose::ActionPose(ActionSet *actionSet,
+                       const std::string &name,
+                       const std::string &localizedName) :
+    Action(new ActionPrivatePose(actionSet))
+{
+    setName(name, localizedName);
+}
+
+ActionPose::Location ActionPose::getValue(Subaction *subaction)
+{
+    Location location;
+    auto privSubaction = Subaction::Private::get(subaction);
+    static_cast<ActionPrivatePose *>(Private::get(this))->locate(privSubaction.get(),
+                                                                 location);
+    return location;
+}
+
+ActionPose::Location::Location() :
+    _flags((Flags)0)
+{
+}
+
+ActionPose::Location::Location(Flags flags,
+                               const osg::Quat &orientation,
+                               const osg::Vec3f &position) :
+    _flags(flags),
+    _orientation(orientation),
+    _position(position)
+{
+}
+
+// ActionVibration
+
+ActionVibration::ActionVibration(ActionSet *actionSet) :
+    Action(new ActionPrivateVibration(actionSet))
+{
+}
+
+ActionVibration::ActionVibration(ActionSet *actionSet,
+                       const std::string &name) :
+    Action(new ActionPrivateVibration(actionSet))
+{
+    setName(name, name);
+}
+
+ActionVibration::ActionVibration(ActionSet *actionSet,
+                       const std::string &name,
+                       const std::string &localizedName) :
+    Action(new ActionPrivateVibration(actionSet))
+{
+    setName(name, localizedName);
+}
+
+bool ActionVibration::applyHapticFeedback(int64_t duration_ns, float frequency,
+                                          float amplitude)
+{
+    auto priv = static_cast<ActionPrivateVibration *>(Private::get(this));
+    return priv->applyHapticFeedback(nullptr, duration_ns, frequency,
+                                     amplitude);
+}
+
+bool ActionVibration::applyHapticFeedback(Subaction *subaction,
+                                          int64_t duration_ns, float frequency,
+                                          float amplitude)
+{
+    auto privSubaction = Subaction::Private::get(subaction);
+    auto priv = static_cast<ActionPrivateVibration *>(Private::get(this));
+    return priv->applyHapticFeedback(privSubaction.get(), duration_ns, frequency,
+                                     amplitude);
+}
+
+bool ActionVibration::stopHapticFeedback(Subaction *subaction)
+{
+    auto privSubaction = Subaction::Private::get(subaction);
+    auto priv = static_cast<ActionPrivateVibration *>(Private::get(this));
+    return priv->stopHapticFeedback(privSubaction.get());
+}
diff --git a/3rdparty/osgXR/src/Action.h b/3rdparty/osgXR/src/Action.h
new file mode 100644
index 000000000..1da217938
--- /dev/null
+++ b/3rdparty/osgXR/src/Action.h
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_ACTION
+#define OSGXR_ACTION 1
+
+#include <osgXR/Action>
+
+#include "Subaction.h"
+
+#include <osg/ref_ptr>
+
+#include <memory>
+#include <set>
+#include <string>
+
+namespace osgXR {
+
+namespace OpenXR {
+    class Action;
+    class Instance;
+};
+
+class Action::Private
+{
+    public:
+
+        static Private *get(Action *pub)
+        {
+            return pub->_private.get();
+        }
+
+        Private(ActionSet *actionSet);
+        virtual ~Private();
+
+        void setName(const std::string &name);
+        const std::string &getName() const;
+
+        void setLocalizedName(const std::string &localizedName);
+        const std::string &getLocalizedName() const;
+
+        void addSubaction(std::shared_ptr<Subaction::Private> subaction);
+
+        bool getUpdated() const
+        {
+            return _updated;
+        }
+
+        /// Setup action with an OpenXR instance
+        virtual OpenXR::Action *setup(OpenXR::Instance *instance) = 0;
+        /// Clean up action before an OpenXR session is destroyed
+        virtual void cleanupSession() = 0;
+        /// Clean up action before an OpenXR instance is destroyed
+        void cleanupInstance();
+
+        /**
+         * Get a list of currently bound source paths for this action.
+         * @param sourcePaths[out] Vector of source paths to write into.
+         */
+        void getBoundSources(std::vector<std::string> &sourcePaths) const;
+
+        /**
+         * Get a list of currently bound source localized names for this action.
+         * @param whichComponents  Which components to include.
+         * @param names[out] Vector of names to write into.
+         */
+        void getBoundSourcesLocalizedNames(XrInputSourceLocalizedNameFlags whichComponents,
+                                           std::vector<std::string> &names) const;
+
+    protected:
+
+        std::string _name;
+        std::string _localizedName;
+
+        osg::ref_ptr<ActionSet> _actionSet;
+        std::set<std::shared_ptr<Subaction::Private>> _subactions;
+
+        bool _updated;
+        osg::ref_ptr<OpenXR::Action> _action;
+};
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/ActionSet.cpp b/3rdparty/osgXR/src/ActionSet.cpp
new file mode 100644
index 000000000..56a982a1a
--- /dev/null
+++ b/3rdparty/osgXR/src/ActionSet.cpp
@@ -0,0 +1,242 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "ActionSet.h"
+#include "Action.h"
+
+#include "OpenXR/ActionSet.h"
+
+#include <osgXR/Manager>
+
+#include "XRState.h"
+
+using namespace osgXR;
+
+// Internal API
+
+ActionSet::Private::Private(XRState *state) :
+    _state(state),
+    _priority(0),
+    _updated(true)
+{
+    state->addActionSet(this);
+}
+
+ActionSet::Private::~Private()
+{
+    XRState *state = _state.get();
+    if (state)
+        state->removeActionSet(this);
+}
+
+void ActionSet::Private::setName(const std::string &name)
+{
+    _updated = true;
+    _name = name;
+}
+
+const std::string &ActionSet::Private::getName() const
+{
+    return _name;
+}
+
+void ActionSet::Private::setLocalizedName(const std::string &localizedName)
+{
+    _updated = true;
+    _localizedName = localizedName;
+}
+
+const std::string &ActionSet::Private::getLocalizedName() const
+{
+    return _localizedName;
+}
+
+void ActionSet::Private::setPriority(uint32_t priority)
+{
+    _updated = true;
+    _priority = priority;
+}
+
+uint32_t ActionSet::Private::getPriority() const
+{
+    return _priority;
+}
+
+bool ActionSet::Private::getUpdated() const
+{
+    if (_updated)
+        return true;
+    for (Action::Private *action: _actions)
+        if (action->getUpdated())
+            return true;
+    return false;
+}
+
+void ActionSet::Private::activate(std::shared_ptr<Subaction::Private> subaction)
+{
+    _activeSubactions.insert(subaction);
+
+    if (_actionSet.valid() && _session.valid())
+    {
+        OpenXR::Path path;
+        if (subaction)
+            path = subaction->setup(_session->getInstance());
+        _session->activateActionSet(_actionSet, path);
+    }
+}
+
+void ActionSet::Private::deactivate(std::shared_ptr<Subaction::Private> subaction)
+{
+    _activeSubactions.erase(subaction);
+
+    if (_actionSet.valid() && _session.valid())
+    {
+        OpenXR::Path path;
+        if (subaction)
+            path = subaction->setup(_session->getInstance());
+        _session->deactivateActionSet(_actionSet, path);
+    }
+}
+
+bool ActionSet::Private::isActive()
+{
+    return !_activeSubactions.empty();
+}
+
+void ActionSet::Private::registerAction(Action::Private *action)
+{
+    _actions.insert(action);
+}
+
+void ActionSet::Private::unregisterAction(Action::Private *action)
+{
+    _actions.erase(action);
+}
+
+OpenXR::ActionSet *ActionSet::Private::setup(OpenXR::Instance *instance)
+{
+    if (_updated)
+    {
+        _actionSet = new OpenXR::ActionSet(instance, _name, _localizedName,
+                                           _priority);
+        _updated = false;
+    }
+    return _actionSet;
+}
+
+bool ActionSet::Private::setup(OpenXR::Session *session)
+{
+    _session = session;
+    if (_actionSet.valid())
+    {
+        session->addActionSet(_actionSet);
+        // Init all the actions
+        for (Action::Private *action: _actions)
+        {
+            OpenXR::Action *xrAction = action->setup(session->getInstance());
+            if (xrAction)
+                xrAction->init();
+        }
+        for (auto &subaction: _activeSubactions)
+        {
+            OpenXR::Path path;
+            if (subaction)
+                path = subaction->setup(session->getInstance());
+            session->activateActionSet(_actionSet, path);
+        }
+        return true;
+    }
+    return false;
+}
+
+void ActionSet::Private::cleanupSession()
+{
+    for (auto *action: _actions)
+        action->cleanupSession();
+}
+
+void ActionSet::Private::cleanupInstance()
+{
+    _updated = true;
+    _actionSet = nullptr;
+    for (auto *action: _actions)
+        action->cleanupInstance();
+}
+
+// Public API
+
+ActionSet::ActionSet(Manager *manager) :
+    _private(new Private(manager->_getXrState()))
+{
+}
+
+ActionSet::ActionSet(Manager *manager,
+                     const std::string &name) :
+    _private(new Private(manager->_getXrState()))
+{
+    setName(name, name);
+}
+
+ActionSet::ActionSet(Manager *manager,
+                     const std::string &name,
+                     const std::string &localizedName) :
+    _private(new Private(manager->_getXrState()))
+{
+    setName(name, localizedName);
+}
+
+ActionSet::~ActionSet()
+{
+}
+
+void ActionSet::setName(const std::string &name,
+                        const std::string &localizedName)
+{
+    _private->setName(name);
+    _private->setLocalizedName(localizedName);
+}
+
+void ActionSet::setName(const std::string &name)
+{
+    _private->setName(name);
+}
+
+const std::string &ActionSet::getName() const
+{
+    return _private->getName();
+}
+
+void ActionSet::setLocalizedName(const std::string &localizedName)
+{
+    _private->setLocalizedName(localizedName);
+}
+
+const std::string &ActionSet::getLocalizedName() const
+{
+    return _private->getLocalizedName();
+}
+
+void ActionSet::setPriority(uint32_t priority)
+{
+    _private->setPriority(priority);
+}
+
+uint32_t ActionSet::getPriority() const
+{
+    return _private->getPriority();
+}
+
+void ActionSet::activate(Subaction *subaction)
+{
+    _private->activate(Subaction::Private::get(subaction));
+}
+
+void ActionSet::deactivate(Subaction *subaction)
+{
+    _private->deactivate(Subaction::Private::get(subaction));
+}
+
+bool ActionSet::isActive()
+{
+    return _private->isActive();
+}
diff --git a/3rdparty/osgXR/src/ActionSet.h b/3rdparty/osgXR/src/ActionSet.h
new file mode 100644
index 000000000..776836323
--- /dev/null
+++ b/3rdparty/osgXR/src/ActionSet.h
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_ACTION_SET
+#define OSGXR_ACTION_SET 1
+
+#include <osgXR/ActionSet>
+#include <osgXR/Action>
+
+#include "OpenXR/Path.h"
+
+#include "Subaction.h"
+
+#include <osg/observer_ptr>
+#include <osg/ref_ptr>
+
+#include <memory>
+#include <string>
+#include <set>
+
+namespace osgXR {
+
+class Action;
+class XRState;
+
+namespace OpenXR {
+    class ActionSet;
+    class Instance;
+    class Session;
+};
+
+class ActionSet::Private
+{
+    public:
+
+        static Private *get(ActionSet *pub)
+        {
+            return pub->_private.get();
+        }
+
+        Private(XRState *state);
+        ~Private();
+
+        void setName(const std::string &name);
+        const std::string &getName() const;
+
+        void setLocalizedName(const std::string &localizedName);
+        const std::string &getLocalizedName() const;
+
+        void setPriority(uint32_t priority);
+        uint32_t getPriority() const;
+
+        bool getUpdated() const;
+
+        void activate(std::shared_ptr<Subaction::Private> subaction = nullptr);
+        void deactivate(std::shared_ptr<Subaction::Private> subaction = nullptr);
+        bool isActive();
+
+        void registerAction(Action::Private *action);
+        void unregisterAction(Action::Private *action);
+
+        /// Setup action set with an OpenXR instance
+        OpenXR::ActionSet *setup(OpenXR::Instance *instance);
+        /// Setup action set with an OpenXR session
+        bool setup(OpenXR::Session *session);
+        /// Clean up action before an OpenXR session is destroyed
+        void cleanupSession();
+        /// Clean up action before an OpenXR instance is destroyed
+        void cleanupInstance();
+
+        OpenXR::Session *getSession()
+        {
+            return _session.get();
+        }
+
+    protected:
+
+        osg::observer_ptr<XRState> _state;
+        std::string _name;
+        std::string _localizedName;
+        uint32_t _priority;
+        std::set<std::shared_ptr<Subaction::Private>> _activeSubactions;
+
+        std::set<Action::Private *> _actions;
+
+        bool _updated;
+        osg::ref_ptr<OpenXR::ActionSet> _actionSet;
+        osg::observer_ptr<OpenXR::Session> _session;
+};
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/CMakeLists.txt b/3rdparty/osgXR/src/CMakeLists.txt
new file mode 100644
index 000000000..f434a7ec7
--- /dev/null
+++ b/3rdparty/osgXR/src/CMakeLists.txt
@@ -0,0 +1,132 @@
+# Dependencies
+find_package(OpenGL REQUIRED)
+find_package(OpenSceneGraph REQUIRED COMPONENTS osgViewer osgUtil)
+find_package(OpenXR REQUIRED)
+
+# Public header files
+set(osgXR_HEADERS
+    include/osgXR/Action
+    include/osgXR/ActionSet
+    include/osgXR/Export
+    include/osgXR/InteractionProfile
+    include/osgXR/Manager
+    include/osgXR/Mirror
+    include/osgXR/MirrorSettings
+    include/osgXR/OpenXRDisplay
+    include/osgXR/Settings
+    include/osgXR/Subaction
+    include/osgXR/View
+    include/osgXR/osgXR
+)
+
+# Source files
+set(osgXR_SRCS
+    OpenXR/Action.cpp
+    OpenXR/ActionSet.cpp
+    OpenXR/Compositor.cpp
+    OpenXR/EventHandler.cpp
+    OpenXR/GraphicsBinding.cpp
+    OpenXR/Instance.cpp
+    OpenXR/InteractionProfile.cpp
+    OpenXR/Path.cpp
+    OpenXR/Session.cpp
+    OpenXR/Space.cpp
+    OpenXR/Swapchain.cpp
+    OpenXR/SwapchainGroup.cpp
+    OpenXR/System.cpp
+    XRFramebuffer.cpp
+    XRState.cpp
+    XRRealizeOperation.cpp
+    Action.cpp
+    ActionSet.cpp
+    FrameStore.cpp
+    InteractionProfile.cpp
+    Manager.cpp
+    Mirror.cpp
+    MirrorSettings.cpp
+    OpenXRDisplay.cpp
+    Settings.cpp
+    Subaction.cpp
+    View.cpp
+    osgXR.cpp
+    projection.cpp
+)
+
+# Win32 graphics binding
+if(WIN32)
+    list(APPEND osgXR_SRCS
+        OpenXR/GraphicsBindingWin32.cpp
+    )
+    add_compile_definitions(OSGXR_USE_WIN32)
+endif()
+
+# X11 graphics binding
+find_package(X11)
+if(X11_FOUND)
+    list(APPEND osgXR_SRCS
+        OpenXR/GraphicsBindingX11.cpp
+    )
+    add_compile_definitions(OSGXR_USE_X11)
+endif()
+
+
+# Build osgXR as a library
+add_library(osgXR ${osgXR_LIBRARY_TYPE} ${osgXR_SRCS})
+
+get_target_property(osgXR_TYPE osgXR TYPE)
+if(osgXR_TYPE STREQUAL STATIC_LIBRARY)
+    # Needed to switch OSGXR_EXPORT off on Windows
+    set(OSGXR_STATIC_LIBRARY 1)
+endif()
+# Needed to switch OSGXR_EXPORT to dllexport on Windows
+add_compile_definitions(OSGXR_LIBRARY)
+
+# Generate a "generated/Version.h" header
+set(osgXR_VERSION_HEADER "${PROJECT_BINARY_DIR}/include/generated/Version.h")
+configure_file("${CMAKE_CURRENT_SOURCE_DIR}/Version.h.in"
+               "${osgXR_VERSION_HEADER}")
+
+# Generate  "osgXR/Config" header
+set(osgXR_CONFIG_HEADER "${PROJECT_BINARY_DIR}/include/osgXR/Config")
+configure_file("${CMAKE_CURRENT_SOURCE_DIR}/Config.in"
+               "${osgXR_CONFIG_HEADER}")
+list(APPEND osgXR_HEADERS "${osgXR_CONFIG_HEADER}")
+
+# Ensure required C++ standards are available
+target_compile_features(osgXR
+                        # smart pointers
+                        PUBLIC  cxx_std_11
+                        # std::optional
+                        PRIVATE cxx_std_17)
+
+target_include_directories(osgXR
+    PRIVATE
+        ${PROJECT_BINARY_DIR}/include
+        ${PROJECT_SOURCE_DIR}/include
+        ${OPENGL_INCLUDE_DIR}
+        ${OPENSCENEGRAPH_INCLUDE_DIRS}
+        ${OpenXR_INCLUDE_DIR}
+    PUBLIC
+        "$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/include>"
+        "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
+)
+
+target_link_libraries(osgXR
+    PRIVATE
+        ${OPENGL_LIBRARIES}
+    PUBLIC
+        ${OPENSCENEGRAPH_LIBRARIES}
+        OpenXR::openxr_loader
+)
+
+set_target_properties(osgXR
+    PROPERTIES
+        VERSION         ${PROJECT_VERSION}
+        SOVERSION       ${osgXR_SOVERSION}
+        PUBLIC_HEADER   "${osgXR_HEADERS}"
+        INTERFACE_osgXR_MAJOR_VERSION   ${osgXR_MAJOR_VERSION}
+        INTERFACE_osgXR_MINOR_VERSION   ${osgXR_MINOR_VERSION}
+)
+set_property(TARGET osgXR APPEND PROPERTY
+    COMPATIBLE_INTERFACE_STRING     osgXR_MAJOR_VERSION osgXR_MINOR_VERSION
+)
diff --git a/3rdparty/osgXR/src/Config.in b/3rdparty/osgXR/src/Config.in
new file mode 100644
index 000000000..2cdac032c
--- /dev/null
+++ b/3rdparty/osgXR/src/Config.in
@@ -0,0 +1,10 @@
+// -*-c++-*-
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_Config
+#define OSGXR_Config 1
+
+#cmakedefine OSGXR_STATIC_LIBRARY
+
+#endif
diff --git a/3rdparty/osgXR/src/FrameStampedVector.h b/3rdparty/osgXR/src/FrameStampedVector.h
new file mode 100644
index 000000000..715d788a1
--- /dev/null
+++ b/3rdparty/osgXR/src/FrameStampedVector.h
@@ -0,0 +1,86 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_FRAME_STAMPED_VECTOR
+#define OSGXR_FRAME_STAMPED_VECTOR 1
+
+#include <osg/FrameStamp>
+
+#include <optional>
+#include <utility>
+#include <vector>
+
+namespace osgXR {
+
+/**
+ * Manages frame stamping of vector items.
+ * Contains a vector of the chosen type, with each item stamped with a
+ * FrameStamp. Items can be retrieved by index or FrameStamp.
+ */
+template <typename T>
+class FrameStampedVector
+{
+    public:
+
+        typedef T Item;
+        typedef unsigned int FrameNumber;
+        typedef const osg::FrameStamp *Stamp;
+        typedef std::pair<Item, FrameNumber> StampedItem;
+
+        void reserve(unsigned int len)
+        {
+            _vec.reserve(len);
+        }
+
+        void resize(unsigned int len, Item item = Item())
+        {
+            _vec.resize(len, StampedItem(item, ~0));
+        }
+
+        unsigned int size() const
+        {
+            return _vec.size();
+        }
+
+        void push_back(const Item &item)
+        {
+            _vec.push_back(StampedItem(item, ~0));
+        }
+
+        // operator [] provides an Item if indexed directly
+        const Item &operator [] (unsigned int index) const
+        {
+            return _vec[index].first;
+        }
+
+        // operator [] provides an optional Item if indexed by stamp
+        std::optional<const Item> operator [] (Stamp stamp) const
+        {
+            int index = findStamp(stamp);
+            if (index < 0)
+                return std::nullopt;
+            return _vec[index].first;
+        }
+
+        int findStamp(Stamp stamp) const
+        {
+            unsigned int frameNumber = stamp->getFrameNumber();
+            for (unsigned int i = 0; i < _vec.size(); ++i)
+                if (_vec[i].second == frameNumber)
+                    return i;
+            return -1;
+        }
+
+        void setStamp(unsigned int index, Stamp stamp)
+        {
+            _vec[index].second = stamp->getFrameNumber();
+        }
+
+    protected:
+
+        std::vector<StampedItem> _vec;
+};
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/src/FrameStore.cpp b/3rdparty/osgXR/src/FrameStore.cpp
new file mode 100644
index 000000000..d7276f3fe
--- /dev/null
+++ b/3rdparty/osgXR/src/FrameStore.cpp
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "FrameStore.h"
+
+#include <osg/FrameStamp>
+
+#include <cassert>
+
+using namespace osgXR;
+
+FrameStore::FrameStore()
+{
+}
+
+osg::ref_ptr<FrameStore::Frame> FrameStore::getFrame(FrameStore::Stamp stamp)
+{
+    OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_mutex);
+
+    int index = lookupFrame(stamp);
+    if (index < 0)
+        return nullptr;
+
+    return _store[index];
+}
+
+osg::ref_ptr<FrameStore::Frame> FrameStore::getFrame(FrameStore::Stamp stamp,
+                                                     OpenXR::Session *session)
+{
+    OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_mutex);
+
+    int index = lookupFrame(stamp);
+    if (index < 0)
+    {
+        index = blankFrame();
+        // there surely shouldn't be more than 2 frames in parallel
+        assert(index >= 0);
+        if (index < 0)
+            return nullptr;
+
+        osg::ref_ptr<OpenXR::Session::Frame> frame = session->waitFrame();
+        if (frame.valid())
+        {
+            frame->setOsgFrameNumber(stamp->getFrameNumber());
+            _store[index] = frame;
+        }
+        return frame;
+    }
+
+    return _store[index];
+}
+
+bool FrameStore::endFrame(FrameStore::Stamp stamp)
+{
+    OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_mutex);
+
+    int index = lookupFrame(stamp);
+    if (index < 0)
+        return false;
+
+    _store[index]->end();
+    _store[index] = nullptr;
+
+    return true;
+}
+
+int FrameStore::lookupFrame(FrameStore::Stamp stamp) const
+{
+    unsigned int frameNumber = stamp->getFrameNumber();
+    for (unsigned int i = 0; i < maxFrames; ++i)
+    {
+        if (_store[i].valid() &&
+            _store[i]->getOsgFrameNumber() == frameNumber)
+        {
+            return i;
+        }
+    }
+    return -1;
+}
+
+int FrameStore::blankFrame() const
+{
+    for (unsigned int i = 0; i < maxFrames; ++i)
+        if (!_store[i].valid())
+            return i;
+    return -1;
+}
diff --git a/3rdparty/osgXR/src/FrameStore.h b/3rdparty/osgXR/src/FrameStore.h
new file mode 100644
index 000000000..4267cd6ab
--- /dev/null
+++ b/3rdparty/osgXR/src/FrameStore.h
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_FRAME_STORE
+#define OSGXR_FRAME_STORE 1
+
+#include "OpenXR/Session.h"
+
+#include <osg/ref_ptr>
+#include <OpenThreads/Mutex>
+
+#include <vector>
+
+namespace osg {
+    class FrameStamp;
+}
+
+namespace osgXR {
+
+/**
+ * Manages concurrent frames.
+ * A FrameStore stores any concurrent OpenXR frames and allows them to be
+ * created and retrieved in a thread-safe way based on an osg::FrameStamp.
+ */
+class FrameStore
+{
+    public:
+
+        typedef OpenXR::Session::Frame Frame;
+        typedef const osg::FrameStamp *Stamp;
+
+        FrameStore();
+
+        /// Get a frame by FrameStamp.
+        osg::ref_ptr<Frame> getFrame(Stamp stamp);
+
+        /// Get or wait for a frame by FrameStamp.
+        osg::ref_ptr<Frame> getFrame(Stamp stamp, OpenXR::Session *session);
+
+        /**
+         * End a frame by FrameStamp.
+         * @return true on success, false otherwise.
+         */
+        bool endFrame(Stamp stamp);
+
+    protected:
+
+        // These return cache index or -1
+        int lookupFrame(Stamp stamp) const;
+        int blankFrame() const;
+
+        // 2 allows work to start on next frame before the prior one has ended
+        static constexpr unsigned int maxFrames = 2;
+        // Protected by _mutex
+        osg::ref_ptr<Frame> _store[maxFrames];
+
+        // For access to _store
+        OpenThreads::Mutex _mutex;
+};
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/src/InteractionProfile.cpp b/3rdparty/osgXR/src/InteractionProfile.cpp
new file mode 100644
index 000000000..a067b2ea8
--- /dev/null
+++ b/3rdparty/osgXR/src/InteractionProfile.cpp
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "Action.h"
+#include "InteractionProfile.h"
+
+#include "OpenXR/InteractionProfile.h"
+#include "OpenXR/Path.h"
+#include "OpenXR/Session.h"
+
+#include <osgXR/Manager>
+
+#include "XRState.h"
+
+using namespace osgXR;
+
+// Internal API
+
+InteractionProfile::Private::Private(InteractionProfile *pub,
+                                     XRState *state,
+                                     const std::string &vendor,
+                                     const std::string &type) :
+    _pub(pub),
+    _state(state),
+    _vendor(vendor),
+    _type(type),
+    _updated(true)
+{
+    state->addInteractionProfile(this);
+}
+
+InteractionProfile::Private::~Private()
+{
+    XRState *state = _state.get();
+    if (state)
+        state->removeInteractionProfile(this);
+}
+
+void InteractionProfile::Private::suggestBinding(Action *action,
+                                                 const std::string &binding)
+{
+    _bindings.push_back({action, binding});
+    _updated = true;
+}
+
+bool InteractionProfile::Private::setup(OpenXR::Instance *instance)
+{
+    // Recreate every time, as actions may have been altered and recreated
+    _profile = new OpenXR::InteractionProfile(instance, _vendor.c_str(),
+                                              _type.c_str());
+
+    for (Binding &binding: _bindings)
+    {
+        // ensure action is set up
+        OpenXR::Action *action = Action::Private::get(binding.action)->setup(instance);
+        if (action)
+            _profile->addBinding(action, binding.binding);
+    }
+
+    bool ret = _profile->suggestBindings();
+    if (ret)
+        _updated = false;
+    return ret;
+}
+
+void InteractionProfile::Private::cleanupInstance()
+{
+    _profile = nullptr;
+}
+
+OpenXR::Path InteractionProfile::Private::getPath() const
+{
+    if (_profile.valid())
+        return _profile->getPath();
+    else
+        return OpenXR::Path();
+}
+
+// Public API
+
+InteractionProfile::InteractionProfile(Manager *manager,
+                                       const std::string &vendor,
+                                       const std::string &type) :
+    _private(new Private(this, manager->_getXrState(), vendor, type))
+{
+}
+
+InteractionProfile::~InteractionProfile()
+{
+}
+
+const std::string &InteractionProfile::getVendor() const
+{
+    return _private->getVendor();
+}
+
+const std::string &InteractionProfile::getType() const
+{
+    return _private->getType();
+}
+
+void InteractionProfile::suggestBinding(Action *action,
+                                        const std::string &binding)
+{
+    _private->suggestBinding(action, binding);
+}
diff --git a/3rdparty/osgXR/src/InteractionProfile.h b/3rdparty/osgXR/src/InteractionProfile.h
new file mode 100644
index 000000000..59485de65
--- /dev/null
+++ b/3rdparty/osgXR/src/InteractionProfile.h
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_INTERACTION_PROFILE
+#define OSGXR_INTERACTION_PROFILE 1
+
+#include <osgXR/InteractionProfile>
+
+#include <osg/observer_ptr>
+#include <osg/ref_ptr>
+
+#include <list>
+#include <string>
+
+namespace osgXR {
+
+class XRState;
+
+namespace OpenXR {
+    class InteractionProfile;
+    class Path;
+    class Session;
+};
+
+class InteractionProfile::Private
+{
+    public:
+
+        static Private *get(InteractionProfile *pub)
+        {
+            return pub->_private.get();
+        }
+
+        Private(InteractionProfile *pub,
+                XRState *newState,
+                const std::string &newVendor,
+                const std::string &newType);
+        ~Private();
+
+        void suggestBinding(Action *action, const std::string &binding);
+
+        bool getUpdated() const
+        {
+            return _updated;
+        }
+
+        /// Setup bindings with an OpenXR instance
+        bool setup(OpenXR::Instance *instance);
+        /// Clean up bindings before an OpenXR instance is destroyed
+        void cleanupInstance();
+
+        // Accessors
+
+        /// Get the public object.
+        InteractionProfile *getPublic()
+        {
+            return _pub;
+        }
+
+        /// Get the vendor segment of the OpenXR interaction profile path.
+        const std::string &getVendor() const
+        {
+            return _vendor;
+        }
+
+        /// Get the type segment of the OpenXR interaction profile path.
+        const std::string &getType() const
+        {
+            return _type;
+        }
+
+        OpenXR::Path getPath() const;
+
+    private:
+
+        InteractionProfile *_pub;
+        osg::observer_ptr<XRState> _state;
+        std::string _vendor;
+        std::string _type;
+
+        struct Binding {
+            osg::ref_ptr<Action> action;
+            std::string binding;
+        };
+        std::list<Binding> _bindings;
+
+        bool _updated;
+        osg::ref_ptr<OpenXR::InteractionProfile> _profile;
+};
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/Manager.cpp b/3rdparty/osgXR/src/Manager.cpp
new file mode 100644
index 000000000..39ce0d764
--- /dev/null
+++ b/3rdparty/osgXR/src/Manager.cpp
@@ -0,0 +1,191 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include <osgXR/Manager>
+#include <osgXR/Mirror>
+
+#include "XRState.h"
+#include "XRRealizeOperation.h"
+
+using namespace osgXR;
+
+Manager::Manager() :
+    _settings(Settings::instance()),
+    _destroying(false),
+    _state(new XRState(_settings, const_cast<Manager *>(this)))
+{
+}
+
+Manager::~Manager()
+{
+}
+
+void Manager::setVisibilityMaskNodeMasks(osg::Node::NodeMask left,
+                                         osg::Node::NodeMask right) const
+{
+    _state->setVisibilityMaskNodeMasks(left, right);
+}
+
+void Manager::configure(osgViewer::View &view) const
+{
+    osgViewer::ViewerBase *viewer = _viewer;
+    if (!viewer)
+        viewer = dynamic_cast<osgViewer::ViewerBase *>(&view);
+    if (!viewer)
+        return;
+
+    _state->setViewer(viewer);
+
+    // Its rather inconvenient that ViewConfig expects a const configure()
+    // Just cheat and cast away the constness here
+    osg::ref_ptr<XRRealizeOperation> realizeOp = new XRRealizeOperation(_state, &view);
+    viewer->setRealizeOperation(realizeOp);
+    if (viewer->isRealized())
+    {
+        osgViewer::ViewerBase::Contexts contexts;
+        viewer->getContexts(contexts, true);
+        if (contexts.size() > 0)
+            (*realizeOp)(contexts[0]);
+    }
+}
+
+void Manager::update()
+{
+    _state->update();
+}
+
+bool Manager::checkAndResetStateChanged()
+{
+    return _state->checkAndResetStateChanged();
+}
+
+bool Manager::getPresent() const
+{
+    return _state->getUpState() >= XRState::VRSTATE_SYSTEM;
+}
+
+bool Manager::getEnabled() const
+{
+    return _state->getUpState() == XRState::VRSTATE_ACTIONS;
+}
+
+void Manager::setEnabled(bool enabled)
+{
+    // Avoid needlessly discarding of the instance
+    // SteamVR 1.15 and 1.16 have issues with xrDestroySession() hanging
+    if (enabled)
+    {
+        _destroying = false;
+        _state->setProbing(true);
+    }
+    else if (_destroying)
+    {
+        _state->setProbing(false);
+    }
+
+    _state->setDestState(enabled ? XRState::VRSTATE_ACTIONS
+                                 : _state->getProbingState());
+}
+
+void Manager::destroyAndWait()
+{
+    _destroying = true;
+    setEnabled(false);
+    while (_state->isStateUpdateNeeded())
+        _state->update();
+}
+
+bool Manager::isDestroying() const
+{
+    return _destroying;
+}
+
+bool Manager::isRunning() const
+{
+    return _state->isRunning();
+}
+
+void Manager::syncSettings()
+{
+    _state->syncSettings();
+}
+
+void Manager::syncActionSetup()
+{
+    _state->syncActionSetup();
+}
+
+bool Manager::hasValidationLayer() const
+{
+    return _state->hasValidationLayer();
+}
+
+bool Manager::hasDepthInfoExtension() const
+{
+    return _state->hasDepthInfoExtension();
+}
+
+bool Manager::hasVisibilityMaskExtension() const
+{
+    return _state->hasVisibilityMaskExtension();
+}
+
+const char *Manager::getRuntimeName() const
+{
+    return _state->getRuntimeName();
+}
+
+const char *Manager::getSystemName() const
+{
+    return _state->getSystemName();
+}
+
+const char *Manager::getStateString() const
+{
+    return _state->getStateString();
+}
+
+void Manager::onRunning()
+{
+}
+
+void Manager::onStopped()
+{
+}
+
+void Manager::onFocus()
+{
+}
+
+void Manager::onUnfocus()
+{
+}
+
+void Manager::addMirror(Mirror *mirror)
+{
+    if (!_state->valid())
+    {
+        // handle this later, _state may not be created yet
+        _mirrorQueue.push_back(mirror);
+    }
+    else
+    {
+        // init the mirror right away
+        mirror->_init();
+    }
+}
+
+void Manager::setupMirrorCamera(osg::Camera *camera)
+{
+    addMirror(new Mirror(this, camera));
+}
+
+void Manager::_setupMirrors()
+{
+    // init each mirror in the queue
+    while (!_mirrorQueue.empty())
+    {
+        _mirrorQueue.front()->_init();
+        _mirrorQueue.pop_front();
+    }
+}
diff --git a/3rdparty/osgXR/src/Mirror.cpp b/3rdparty/osgXR/src/Mirror.cpp
new file mode 100644
index 000000000..a7249dfe0
--- /dev/null
+++ b/3rdparty/osgXR/src/Mirror.cpp
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include <osgXR/Manager>
+#include <osgXR/Mirror>
+
+#include "XRState.h"
+
+#include <osg/PolygonMode>
+
+using namespace osgXR;
+
+Mirror::Mirror(Manager *manager, osg::Camera *camera) :
+    _manager(manager),
+    _camera(camera),
+    _mirrorSettings(manager->_getSettings()->getMirrorSettings())
+{
+}
+
+Mirror::~Mirror()
+{
+}
+
+void Mirror::_init()
+{
+    _camera->setAllowEventFocus(false);
+    _camera->setViewMatrix(osg::Matrix::identity());
+    _camera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1, 0, 1));
+
+    // Find the mirror settings
+    MirrorSettings *mirrorSettings = &_mirrorSettings;
+    // but fall back to the manager's mirror settings
+    if (mirrorSettings->getMirrorMode() == MirrorSettings::MIRROR_AUTOMATIC)
+        mirrorSettings = &_manager->_getSettings()->getMirrorSettings();
+    switch (mirrorSettings->getMirrorMode())
+    {
+    case MirrorSettings::MIRROR_NONE:
+        // Draw nothing, but still clear the viewport
+        _camera->setClearMask(GL_COLOR_BUFFER_BIT);
+        break;
+    case MirrorSettings::MIRROR_AUTOMATIC:
+        // Fall-through: Default to MIRROR_SINGLE
+    case MirrorSettings::MIRROR_SINGLE:
+        {
+            int viewIndex = mirrorSettings->getMirrorViewIndex();
+            if (viewIndex < 0)
+                viewIndex = 0;
+            setupQuad(viewIndex, 0.0f, 1.0f);
+        }
+        break;
+    case MirrorSettings::MIRROR_LEFT_RIGHT:
+        for (unsigned int viewIndex = 0; viewIndex < 2; ++viewIndex)
+            setupQuad(viewIndex, 0.5f * viewIndex, 0.5f);
+        break;
+    }
+}
+
+namespace {
+
+class MirrorPreDrawCallback : public osg::Camera::DrawCallback
+{
+    public:
+
+        MirrorPreDrawCallback(osg::ref_ptr<XRState> xrState,
+                              osg::ref_ptr<osg::StateSet> stateSet,
+                              unsigned int viewIndex) :
+            _xrState(xrState),
+            _stateSet(stateSet),
+            _viewIndex(viewIndex)
+        {
+        }
+
+        void operator()(osg::RenderInfo& renderInfo) const override
+        {
+            const osg::FrameStamp *stamp = renderInfo.getState()->getFrameStamp();
+            _stateSet->setTextureAttributeAndModes(0,
+                                _xrState->getViewTexture(_viewIndex, stamp));
+        }
+
+    protected:
+
+        osg::observer_ptr<XRState> _xrState;
+        osg::ref_ptr<osg::StateSet> _stateSet;
+        unsigned int _viewIndex;
+};
+
+class MirrorPostDrawCallback : public osg::Camera::DrawCallback
+{
+    public:
+
+        MirrorPostDrawCallback(osg::ref_ptr<osg::StateSet> stateSet) :
+            _stateSet(stateSet)
+        {
+        }
+
+        void operator()(osg::RenderInfo& renderInfo) const override
+        {
+            _stateSet->removeTextureAttribute(0, osg::StateAttribute::Type::TEXTURE);
+        }
+
+    protected:
+
+        osg::ref_ptr<osg::StateSet> _stateSet;
+};
+
+}
+
+void Mirror::setupQuad(unsigned int viewIndex,
+                       float x, float w)
+{
+    XRState *xrState = _manager->_getXrState();
+
+    if (viewIndex >= xrState->getViewCount())
+        return;
+
+    // Build an always-visible quad to draw the view texture on
+    osg::ref_ptr<osg::Geode> quad = new osg::Geode;
+    quad->setCullingActive(false);
+
+    XRState::TextureRect rect = xrState->getViewTextureRect(viewIndex);
+    quad->addDrawable(osg::createTexturedQuadGeometry(
+                                  osg::Vec3(x,    0.0f,  0.0f),
+                                  osg::Vec3(w,    0.0f,  0.0f),
+                                  osg::Vec3(0.0f, 1.0f,  0.0f),
+                                  rect.x, rect.y,
+                                  rect.x + rect.width, rect.y + rect.height));
+
+    osg::ref_ptr<osg::StateSet> state = quad->getOrCreateStateSet();
+    int forceOff = osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED;
+    state->setMode(GL_LIGHTING, forceOff);
+    state->setMode(GL_DEPTH_TEST, forceOff);
+
+    _camera->addChild(quad);
+
+    // Set a callback so we can switch the texture to the active swapchain image
+    _camera->addPreDrawCallback(new MirrorPreDrawCallback(_manager->_getXrState(),
+                                                          state, viewIndex));
+    _camera->addPostDrawCallback(new MirrorPostDrawCallback(state));
+}
diff --git a/3rdparty/osgXR/src/MirrorSettings.cpp b/3rdparty/osgXR/src/MirrorSettings.cpp
new file mode 100644
index 000000000..9d718045f
--- /dev/null
+++ b/3rdparty/osgXR/src/MirrorSettings.cpp
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include <osgXR/MirrorSettings>
+
+using namespace osgXR;
+
+MirrorSettings::MirrorSettings() :
+    _mirrorMode(MIRROR_AUTOMATIC),
+    _mirrorViewIndex(-1)
+{
+}
diff --git a/3rdparty/osgXR/src/OpenXR/Action.cpp b/3rdparty/osgXR/src/OpenXR/Action.cpp
new file mode 100644
index 000000000..7fae4a85e
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Action.cpp
@@ -0,0 +1,188 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "Action.h"
+#include "Path.h"
+#include "Session.h"
+#include "Space.h"
+
+#include <cassert>
+#include <cstring>
+
+using namespace osgXR::OpenXR;
+
+Action::Action(ActionSet *actionSet,
+               const std::string &name,
+               const std::string &localizedName,
+               XrActionType type) :
+    _actionSet(actionSet),
+    _createInfo{ XR_TYPE_ACTION_CREATE_INFO },
+    _action(XR_NULL_HANDLE)
+{
+    strncpy(_createInfo.actionName, name.c_str(),
+            XR_MAX_ACTION_NAME_SIZE - 1);
+    strncpy(_createInfo.localizedActionName, localizedName.c_str(),
+            XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
+    _createInfo.actionType = type;
+}
+
+Action::~Action()
+{
+    if (_action != XR_NULL_HANDLE)
+    {
+        check(xrDestroyAction(_action),
+              "Failed to destroy OpenXR action");
+    }
+}
+
+void Action::addSubaction(const Path &path)
+{
+    assert(path.getInstance() == getInstance());
+    _subactionPaths.push_back(path.getXrPath());
+}
+
+bool Action::init()
+{
+    if (valid())
+        return true;
+
+    if (!_subactionPaths.empty())
+    {
+        _createInfo.countSubactionPaths = _subactionPaths.size();
+        _createInfo.subactionPaths = _subactionPaths.data();
+    }
+    return check(xrCreateAction(getXrActionSet(), &_createInfo, &_action),
+                 "Failed to create OpenXR action");
+}
+
+ActionStateBase::ActionStateBase(Action *action, Session *session,
+                                 Path subactionPath) :
+    _action(action),
+    _session(session),
+    _subactionPath(subactionPath),
+    _valid(false),
+    _syncCount(0)
+{
+}
+
+ActionStateBase::~ActionStateBase()
+{
+}
+
+bool ActionStateBase::checkUpdate()
+{
+    unsigned int sessionSyncCount = _session->getActionSyncCount();
+    // If an xrSyncActions has taken place, the state is out of date
+    bool needsUpdate = (_syncCount < sessionSyncCount);
+    // Update the counter as caller is expected to update the state
+    _syncCount = sessionSyncCount;
+    return needsUpdate;
+}
+
+template <>
+bool ActionStateCommonBoolean::updateState()
+{
+    XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
+    getInfo.action = _action->getXrAction();
+    getInfo.subactionPath = _subactionPath.getXrPath();
+
+    _state = { XR_TYPE_ACTION_STATE_BOOLEAN };
+
+    _valid = check(xrGetActionStateBoolean(_session->getXrSession(), &getInfo,
+                                           &_state),
+                   "Failed to get boolean OpenXR action state");
+    return _valid;
+}
+
+template <>
+bool ActionStateCommonFloat::updateState()
+{
+    XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
+    getInfo.action = _action->getXrAction();
+    getInfo.subactionPath = _subactionPath.getXrPath();
+
+    _state = { XR_TYPE_ACTION_STATE_FLOAT };
+
+    _valid = check(xrGetActionStateFloat(_session->getXrSession(), &getInfo,
+                                         &_state),
+                   "Failed to get float OpenXR action state");
+    return _valid;
+}
+
+template <>
+bool ActionStateCommonVector2f::updateState()
+{
+    XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
+    getInfo.action = _action->getXrAction();
+    getInfo.subactionPath = _subactionPath.getXrPath();
+
+    _state = { XR_TYPE_ACTION_STATE_VECTOR2F };
+
+    _valid = check(xrGetActionStateVector2f(_session->getXrSession(), &getInfo,
+                                            &_state),
+                   "Failed to get vector2f OpenXR action state");
+    return _valid;
+}
+
+template <>
+bool ActionStateCommonPose::updateState()
+{
+    XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
+    getInfo.action = _action->getXrAction();
+    getInfo.subactionPath = _subactionPath.getXrPath();
+
+    _state = { XR_TYPE_ACTION_STATE_POSE };
+
+    _valid = check(xrGetActionStatePose(_session->getXrSession(), &getInfo,
+                                        &_state),
+                   "Failed to get pose OpenXR action state");
+    return _valid;
+}
+
+ActionStatePose::ActionStatePose(ActionPose *action, Session *session,
+                                 Path subactionPath) :
+    Base(action, session, subactionPath),
+    _space(new Space(session, action, subactionPath))
+{
+}
+
+ActionStatePose::~ActionStatePose()
+{
+}
+
+ActionStateVibration::ActionStateVibration(ActionVibration *action,
+                                           Session *session,
+                                           Path subactionPath) :
+    _action(action),
+    _session(session),
+    _subactionPath(subactionPath)
+{
+}
+
+bool ActionStateVibration::applyHapticFeedback(int64_t duration_ns,
+                                               float frequency,
+                                               float amplitude) const
+{
+    XrHapticActionInfo actionInfo{ XR_TYPE_HAPTIC_ACTION_INFO };
+    actionInfo.action = _action->getXrAction();
+    actionInfo.subactionPath = _subactionPath.getXrPath();
+
+    XrHapticVibration vibration{ XR_TYPE_HAPTIC_VIBRATION };
+    vibration.duration = duration_ns;
+    vibration.frequency = frequency;
+    vibration.amplitude = amplitude;
+
+    return check(xrApplyHapticFeedback(_session->getXrSession(), &actionInfo,
+                       reinterpret_cast<XrHapticBaseHeader*>(&vibration)),
+                 "Failed to apply haptic feedback");
+}
+
+bool ActionStateVibration::stopHapticFeedback() const
+{
+    XrHapticActionInfo actionInfo{ XR_TYPE_HAPTIC_ACTION_INFO };
+    actionInfo.action = _action->getXrAction();
+    actionInfo.subactionPath = _subactionPath.getXrPath();
+
+    return check(xrStopHapticFeedback(_session->getXrSession(), &actionInfo),
+                 "Failed to stop haptic feedback");
+}
diff --git a/3rdparty/osgXR/src/OpenXR/Action.h b/3rdparty/osgXR/src/OpenXR/Action.h
new file mode 100644
index 000000000..2bbf71462
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Action.h
@@ -0,0 +1,371 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_ACTION
+#define OSGXR_OPENXR_ACTION 1
+
+#include "ActionSet.h"
+#include "Path.h"
+
+#include <osg/Vec2f>
+#include <osg/ref_ptr>
+
+#include <cassert>
+#include <string>
+#include <vector>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class Path;
+class Space;
+
+class Action : public osg::Referenced
+{
+    public:
+
+        Action(ActionSet *actionSet,
+               const std::string &name,
+               const std::string &localizedName,
+               XrActionType type);
+        virtual ~Action();
+
+        // Action initialisation
+
+        void addSubaction(const Path &path);
+
+        // Returns true on success
+        bool init();
+
+        // Error checking
+
+        inline bool valid() const
+        {
+            return _action != XR_NULL_HANDLE;
+        }
+
+        inline bool check(XrResult result, const char *warnMsg) const
+        {
+            return _actionSet->check(result, warnMsg);
+        }
+
+        // Conversions
+
+        inline const osg::ref_ptr<ActionSet> getActionSet() const
+        {
+            return _actionSet;
+        }
+
+        inline const osg::ref_ptr<Instance> getInstance() const
+        {
+            return _actionSet->getInstance();
+        }
+
+        inline XrInstance getXrInstance() const
+        {
+            return _actionSet->getXrInstance();
+        }
+
+        inline XrActionSet getXrActionSet() const
+        {
+            return _actionSet->getXrActionSet();
+        }
+
+        inline XrAction getXrAction() const
+        {
+            return _action;
+        }
+
+    protected:
+
+        // Action data
+        osg::ref_ptr<ActionSet> _actionSet;
+        std::vector<XrPath> _subactionPaths;
+        XrActionCreateInfo _createInfo;
+        XrAction _action;
+};
+
+/// Base action state for inputs.
+class ActionStateBase : public osg::Referenced
+{
+    public:
+
+        // Constructors
+
+        ActionStateBase(Action *action, Session *session,
+                        Path subactionPath = Path());
+        virtual ~ActionStateBase();
+
+        // Error checking
+
+        inline bool valid() const
+        {
+            return _valid;
+        }
+
+        inline bool check(XrResult result, const char *warnMsg) const
+        {
+            return _action->check(result, warnMsg);
+        }
+
+    protected:
+
+        // Utilities for synchronisation
+
+        // Find whether the state needs update and update sync counter
+        bool checkUpdate();
+
+        // Member data
+
+        osg::ref_ptr<Action> _action;
+        osg::ref_ptr<Session> _session;
+        Path _subactionPath;
+        bool _valid;
+        unsigned int _syncCount;
+};
+
+/// All action states have an isActive field.
+template <typename T>
+class ActionStateCommon : public ActionStateBase
+{
+    private:
+
+        typedef ActionStateBase Base;
+
+    public:
+
+        // Constructors
+
+        ActionStateCommon(Action *action, Session *session,
+                          Path subactionPath = Path()) :
+            Base(action, session, subactionPath)
+        {
+        }
+
+        // Accessors
+
+        bool isActive() const
+        {
+            assert(valid());
+            return _state.isActive;
+        }
+
+        // Operations
+
+        /// Update state if a sync has taken place
+        bool update()
+        {
+            if (Base::checkUpdate())
+                return updateState();
+            return valid();
+        }
+
+    protected:
+
+        // Protected operations
+
+        bool updateState();
+
+        // Data members
+
+        T _state;
+};
+
+// These are the base action state classes
+typedef ActionStateCommon<XrActionStateBoolean>  ActionStateCommonBoolean;
+typedef ActionStateCommon<XrActionStateFloat>    ActionStateCommonFloat;
+typedef ActionStateCommon<XrActionStateVector2f> ActionStateCommonVector2f;
+typedef ActionStateCommon<XrActionStatePose>     ActionStateCommonPose;
+
+// Convert action values to app / OSG friendly formats
+template <typename T>
+struct ActionTypeInfo;
+
+// XrBool32 -> bool
+template <>
+struct ActionTypeInfo<XrActionStateBoolean>
+{
+    static bool convert(XrBool32 value)
+    {
+        return value;
+    }
+
+    static bool defaultValue()
+    {
+        return false;
+    }
+};
+
+// float -> float
+template <>
+struct ActionTypeInfo<XrActionStateFloat>
+{
+    static float convert(float value)
+    {
+        return value;
+    }
+
+    static float defaultValue()
+    {
+        return 0.0f;
+    }
+};
+
+// XrVector2f -> osg::Vec2f
+template <>
+struct ActionTypeInfo<XrActionStateVector2f>
+{
+    static osg::Vec2f convert(const XrVector2f &value)
+    {
+        return osg::Vec2f(value.x, value.y);
+    }
+
+    static osg::Vec2f defaultValue()
+    {
+        return osg::Vec2f(0.0f, 0.0f);
+    }
+};
+
+/// Some action states have currentValue and related fields.
+template <typename T>
+class ActionStateSimple : public ActionStateCommon<T>
+{
+    private:
+
+        typedef ActionStateCommon<T> Base;
+
+    public:
+
+        typedef ActionTypeInfo<T> Info;
+
+        // Constructors
+
+        ActionStateSimple(Action *action, Session *session,
+                          Path subactionPath = Path()) :
+            Base(action, session, subactionPath)
+        {
+        }
+
+        // Accessors
+
+        auto getCurrentState() const
+        {
+            assert(this->valid());
+            return Info::convert(Base::_state.currentState);
+        }
+
+        bool hasChangedSinceLastSync() const
+        {
+            assert(this->valid());
+            return Base::_state.changedSinceLastSync;
+        }
+
+        XrTime getLastChangedTime() const
+        {
+            assert(this->valid());
+            return Base::_state.lastChangedTime;
+        }
+};
+
+// These are the simple action state classes
+typedef ActionStateSimple<XrActionStateBoolean>  ActionStateBoolean;
+typedef ActionStateSimple<XrActionStateFloat>    ActionStateFloat;
+typedef ActionStateSimple<XrActionStateVector2f> ActionStateVector2f;
+
+/// Specialise Action for a specific input type
+template <XrActionType type, typename T>
+class ActionTyped : public Action
+{
+    public:
+
+        typedef T State;
+
+        ActionTyped(ActionSet *actionSet,
+                    const std::string &name,
+                    const std::string &localizedName) :
+            Action(actionSet, name, localizedName, type)
+        {
+        }
+
+        osg::ref_ptr<State> createState(Session *session,
+                                        Path subactionPath = Path())
+        {
+            return new State(this, session, subactionPath);
+        }
+};
+
+// So ActionStatePose etc can take ActionPose etc in constructor
+class ActionStatePose;
+class ActionStateVibration;
+
+// These are the final typed action classes
+typedef ActionTyped<XR_ACTION_TYPE_BOOLEAN_INPUT,    ActionStateBoolean>   ActionBoolean;
+typedef ActionTyped<XR_ACTION_TYPE_FLOAT_INPUT,      ActionStateFloat>     ActionFloat;
+typedef ActionTyped<XR_ACTION_TYPE_VECTOR2F_INPUT,   ActionStateVector2f>  ActionVector2f;
+typedef ActionTyped<XR_ACTION_TYPE_POSE_INPUT,       ActionStatePose>      ActionPose;
+typedef ActionTyped<XR_ACTION_TYPE_VIBRATION_OUTPUT, ActionStateVibration> ActionVibration;
+
+/// Pose actions have their own way to get the pose
+class ActionStatePose : public ActionStateCommon<XrActionStatePose>
+{
+    private:
+
+        typedef ActionStateCommon<XrActionStatePose> Base;
+
+    public:
+
+        // Constructors
+
+        ActionStatePose(ActionPose *action, Session *session,
+                        Path subactionPath = Path());
+        ~ActionStatePose();
+
+        // Accessors
+
+        Space *getSpace()
+        {
+            return _space.get();
+        }
+
+    protected:
+
+        osg::ref_ptr<Space> _space;
+};
+
+class ActionStateVibration : public osg::Referenced
+{
+    public:
+
+        // Constructors
+
+        ActionStateVibration(ActionVibration *action, Session *session,
+                             Path subactionPath = Path());
+
+        // Error checking
+
+        inline bool check(XrResult result, const char *warnMsg) const
+        {
+            return _action->check(result, warnMsg);
+        }
+
+        // Haptic vibrations
+
+        bool applyHapticFeedback(int64_t duration_ns, float frequency,
+                                 float amplitude) const;
+        bool stopHapticFeedback() const;
+
+    protected:
+
+        // Member data
+
+        osg::ref_ptr<Action> _action;
+        osg::ref_ptr<Session> _session;
+        Path _subactionPath;
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/ActionSet.cpp b/3rdparty/osgXR/src/OpenXR/ActionSet.cpp
new file mode 100644
index 000000000..a4462b555
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/ActionSet.cpp
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "ActionSet.h"
+
+#include <cstring>
+
+using namespace osgXR::OpenXR;
+
+ActionSet::ActionSet(Instance *instance,
+                     const std::string &name,
+                     const std::string &localizedName,
+                     uint32_t priority) :
+    _instance(instance),
+    _actionSet(XR_NULL_HANDLE)
+{
+    XrActionSetCreateInfo createInfo{ XR_TYPE_ACTION_SET_CREATE_INFO };
+    strncpy(createInfo.actionSetName, name.c_str(),
+            XR_MAX_ACTION_SET_NAME_SIZE - 1);
+    strncpy(createInfo.localizedActionSetName, localizedName.c_str(),
+            XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE - 1);
+    createInfo.priority = priority;
+
+    check(xrCreateActionSet(getXrInstance(), &createInfo, &_actionSet),
+          "Failed to create OpenXR action set");
+}
+
+ActionSet::~ActionSet()
+{
+    if (_actionSet != XR_NULL_HANDLE)
+    {
+        check(xrDestroyActionSet(_actionSet),
+              "Failed to destroy OpenXR action set");
+    }
+}
diff --git a/3rdparty/osgXR/src/OpenXR/ActionSet.h b/3rdparty/osgXR/src/OpenXR/ActionSet.h
new file mode 100644
index 000000000..34966fde8
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/ActionSet.h
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_ACTION_SET
+#define OSGXR_OPENXR_ACTION_SET 1
+
+#include "Instance.h"
+
+#include <osg/ref_ptr>
+
+#include <cstdint>
+#include <string>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class ActionSet : public osg::Referenced
+{
+    public:
+
+        ActionSet(Instance *instance,
+                  const std::string &name,
+                  const std::string &localizedName,
+                  uint32_t priority);
+        virtual ~ActionSet();
+
+        // Error checking
+
+        inline bool valid() const
+        {
+            return _actionSet != XR_NULL_HANDLE;
+        }
+
+        inline bool check(XrResult result, const char *warnMsg) const
+        {
+            return _instance->check(result, warnMsg);
+        }
+
+        // Conversions
+
+        inline const osg::ref_ptr<Instance> getInstance() const
+        {
+            return _instance;
+        }
+
+        inline XrInstance getXrInstance() const
+        {
+            return _instance->getXrInstance();
+        }
+
+        inline XrActionSet getXrActionSet() const
+        {
+            return _actionSet;
+        }
+
+
+    protected:
+
+        // Action set data
+        osg::ref_ptr<Instance> _instance;
+        XrActionSet _actionSet;
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/Compositor.cpp b/3rdparty/osgXR/src/OpenXR/Compositor.cpp
new file mode 100644
index 000000000..df347eda7
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Compositor.cpp
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "Compositor.h"
+#include "DepthInfo.h"
+#include "Space.h"
+#include "SwapchainGroupSubImage.h"
+
+#include <cassert>
+
+using namespace osgXR::OpenXR;
+
+void CompositionLayerProjection::addView(osg::ref_ptr<Session::Frame> frame, uint32_t viewIndex,
+                                         const SwapchainGroup::SubImage &subImage,
+                                         const DepthInfo *depthInfo)
+{
+    assert(viewIndex < _projViews.size());
+
+    XrCompositionLayerProjectionView &projView = _projViews[viewIndex];
+    projView = { XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW };
+    projView.pose = frame->getViewPose(viewIndex);
+    projView.fov = frame->getViewFov(viewIndex);
+    subImage.getXrSubImage(&projView.subImage);
+
+    if (depthInfo && subImage.depthValid())
+    {
+        // depth info
+        XrCompositionLayerDepthInfoKHR &xrDepthInfo = _depthInfos[viewIndex];
+        xrDepthInfo = { XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR };
+        subImage.getDepthXrSubImage(&xrDepthInfo.subImage);
+        xrDepthInfo.minDepth = depthInfo->getMinDepth();
+        xrDepthInfo.maxDepth = depthInfo->getMaxDepth();
+        xrDepthInfo.nearZ    = depthInfo->getNearZ();
+        xrDepthInfo.farZ     = depthInfo->getFarZ();
+
+        // add depth info to projection view chain
+        projView.next = &xrDepthInfo;
+    }
+}
+
+const XrCompositionLayerBaseHeader *CompositionLayerProjection::getXr()
+{
+    unsigned int validDepthInfos = 0;
+    for (unsigned int i = 0; i < _projViews.size(); ++i)
+    {
+        auto &view = _projViews[i];
+        auto &depthInfo = _depthInfos[i];
+        if (view.type != XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW)
+        {
+            // Eek, some views have been omitted!
+            OSG_WARN << "Partial projection views!" << std::endl;
+        }
+
+        if (depthInfo.type == XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR)
+            ++validDepthInfos;
+    }
+
+    // Sanity check that depth info is entirely missing or complete
+    if (validDepthInfos > 0 && validDepthInfos < _projViews.size())
+    {
+        OSG_WARN << "Partial projection depth info, disabling depth information" << std::endl;
+        for (auto &view: _projViews)
+            view.next = nullptr;
+    }
+
+    _layer.layerFlags = _layerFlags;
+    _layer.space = _space->getXrSpace();
+    _layer.viewCount = _projViews.size();
+    _layer.views = _projViews.data();
+    return reinterpret_cast<const XrCompositionLayerBaseHeader*>(&_layer);
+}
diff --git a/3rdparty/osgXR/src/OpenXR/Compositor.h b/3rdparty/osgXR/src/OpenXR/Compositor.h
new file mode 100644
index 000000000..a8bbd2d1a
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Compositor.h
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_COMPOSITOR
+#define OSGXR_OPENXR_COMPOSITOR 1
+
+#include "Session.h"
+#include "Space.h"
+#include "SwapchainGroup.h"
+
+#include <osg/Referenced>
+#include <osg/ref_ptr>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class DepthInfo;
+
+class CompositionLayer : public osg::Referenced
+{
+    public:
+
+        CompositionLayer() :
+            _layerFlags(0)
+        {
+        }
+
+        virtual ~CompositionLayer()
+        {
+        }
+
+        inline XrCompositionLayerFlags getLayerFlags() const
+        {
+            return _layerFlags;
+        }
+        inline void setLayerFlags(XrCompositionLayerFlags layerFlags)
+        {
+            _layerFlags = layerFlags;
+        }
+
+        inline Space *getSpace() const
+        {
+            return _space;
+        }
+        inline void setSpace(Space *space)
+        {
+            _space = space;
+        }
+
+        virtual const XrCompositionLayerBaseHeader *getXr() = 0;
+
+    protected:
+
+        XrCompositionLayerFlags _layerFlags;
+        osg::ref_ptr<Space>     _space;
+};
+
+class CompositionLayerProjection : public CompositionLayer
+{
+    public:
+
+        CompositionLayerProjection(unsigned int viewCount)
+        {
+            _layer.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION;
+            _layer.next = nullptr;
+            _projViews.resize(viewCount);
+            _depthInfos.resize(viewCount);
+        }
+
+        virtual ~CompositionLayerProjection()
+        {
+        }
+
+        void addView(osg::ref_ptr<Session::Frame> frame, uint32_t viewIndex,
+                     const SwapchainGroup::SubImage &subImage,
+                     const DepthInfo *depthInfo = nullptr);
+
+        const XrCompositionLayerBaseHeader *getXr() override;
+
+    protected:
+
+        mutable XrCompositionLayerProjection _layer;
+        std::vector<XrCompositionLayerProjectionView> _projViews;
+        std::vector<XrCompositionLayerDepthInfoKHR> _depthInfos;
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/DepthInfo.h b/3rdparty/osgXR/src/OpenXR/DepthInfo.h
new file mode 100644
index 000000000..392c3eafc
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/DepthInfo.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_DEPTH_INFO
+#define OSGXR_OPENXR_DEPTH_INFO 1
+
+#include <osg/Matrixd>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+// Represents depth information for a view
+class DepthInfo
+{
+    public:
+
+        DepthInfo() :
+            _minDepth(0),
+            _maxDepth(1),
+            _nearZ(1),
+            _farZ(10)
+        {
+        }
+
+        // Mutators
+
+        void setDepthRange(float minDepth, float maxDepth)
+        {
+            _minDepth = minDepth;
+            _maxDepth = maxDepth;
+        }
+
+        void setZRange(float nearZ, float farZ)
+        {
+            _nearZ = nearZ;
+            _farZ = farZ;
+        }
+
+        void setZRangeFromProjection(const osg::Matrixd &proj)
+        {
+            float left, right, bottom, top;
+            proj.getFrustum(left, right, bottom, top, _nearZ, _farZ);
+        }
+
+        // Accessors
+
+        float getMinDepth() const
+        {
+            return _minDepth;
+        }
+
+        float getMaxDepth() const
+        {
+            return _maxDepth;
+        }
+
+        float getNearZ() const
+        {
+            return _nearZ;
+        }
+
+        float getFarZ() const
+        {
+            return _farZ;
+        }
+
+    protected:
+
+        float _minDepth;
+        float _maxDepth;
+        float _nearZ;
+        float _farZ;
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/EventHandler.cpp b/3rdparty/osgXR/src/OpenXR/EventHandler.cpp
new file mode 100644
index 000000000..9ec58c53e
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/EventHandler.cpp
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "EventHandler.h"
+#include "Instance.h"
+#include "Session.h"
+
+#include <osg/Notify>
+
+using namespace osgXR::OpenXR;
+
+void EventHandler::onEvent(Instance *instance,
+                           const XrEventDataBuffer *event)
+{
+    switch (event->type)
+    {
+    case XR_TYPE_EVENT_DATA_EVENTS_LOST:
+        onEventsLost(instance,
+            reinterpret_cast<const XrEventDataEventsLost *>(event));
+        break;
+    case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING:
+        onInstanceLossPending(instance,
+            reinterpret_cast<const XrEventDataInstanceLossPending *>(event));
+        break;
+    case XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED:
+        {
+            auto *profileEvent = reinterpret_cast<const XrEventDataInteractionProfileChanged *>(event);
+            Session *session = instance->getSession(profileEvent->session);
+            if (session)
+                onInteractionProfileChanged(session, profileEvent);
+            else
+                OSG_WARN << "Unhandled OpenXR interaction profile changed event: Session not registered" << std::endl;
+            break;
+        }
+    case XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING:
+        {
+            auto *spaceEvent = reinterpret_cast<const XrEventDataReferenceSpaceChangePending *>(event);
+            Session *session = instance->getSession(spaceEvent->session);
+            if (session)
+                onReferenceSpaceChangePending(session, spaceEvent);
+            else
+                OSG_WARN << "Unhandled OpenXR reference space change pending event: Session not registered" << std::endl;
+            break;
+        }
+    case XR_TYPE_EVENT_DATA_VISIBILITY_MASK_CHANGED_KHR:
+        {
+            auto *maskEvent = reinterpret_cast<const XrEventDataVisibilityMaskChangedKHR *>(event);
+            Session *session = instance->getSession(maskEvent->session);
+            if (session)
+                onVisibilityMaskChanged(session, maskEvent);
+            else
+                OSG_WARN << "Unhandled OpenXR visibility mask change event: Session not registered" << std::endl;
+            break;
+        }
+        break;
+    case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED:
+        {
+            auto *stateEvent = reinterpret_cast<const XrEventDataSessionStateChanged *>(event);
+            Session *session = instance->getSession(stateEvent->session);
+            if (session)
+                onSessionStateChanged(session, stateEvent);
+            else
+                OSG_WARN << "Unhandled OpenXR session state change event: Session not registered" << std::endl;
+            break;
+        }
+    default:
+        onUnhandledEvent(instance, event);
+        break;
+    }
+}
+
+void EventHandler::onUnhandledEvent(Instance *instance,
+                                    const XrEventDataBuffer *event)
+{
+    OSG_WARN << "Unhandled OpenXR Event: " << event->type << std::endl;
+}
+
+void EventHandler::onEventsLost(Instance *instance,
+                                const XrEventDataEventsLost *event)
+{
+    OSG_WARN << event->lostEventCount << " OpenXR events lost" << std::endl;
+}
+
+void EventHandler::onInstanceLossPending(Instance *instance,
+                                         const XrEventDataInstanceLossPending *event)
+{
+    OSG_WARN << "OpenXR instance loss pending" << std::endl;
+}
+
+void EventHandler::onInteractionProfileChanged(Session *session,
+                                               const XrEventDataInteractionProfileChanged *event)
+{
+    OSG_WARN << "OpenXR interaction profile changed" << std::endl;
+}
+
+void EventHandler::onReferenceSpaceChangePending(Session *session,
+                                                 const XrEventDataReferenceSpaceChangePending *event)
+{
+    OSG_WARN << "OpenXR reference space change penging" << std::endl;
+}
+
+void EventHandler::onVisibilityMaskChanged(Session *session,
+                                           const XrEventDataVisibilityMaskChangedKHR *event)
+{
+    session->updateVisibilityMasks(event->viewConfigurationType,
+                                   event->viewIndex);
+}
+
+void EventHandler::onSessionStateChanged(Session *session,
+                                         const XrEventDataSessionStateChanged *event)
+{
+    XrSessionState oldState = session->getState();
+    session->setState(event->state);
+    switch (event->state)
+    {
+    case XR_SESSION_STATE_IDLE:
+        // Either starting or soon to be stopping
+        if (oldState == XR_SESSION_STATE_UNKNOWN)
+            onSessionStateStart(session);
+        break;
+    case XR_SESSION_STATE_READY:
+        // Session ready to begin
+        onSessionStateReady(session);
+        break;
+    case XR_SESSION_STATE_SYNCHRONIZED:
+        // Either session synchronised or no longer visible
+        break;
+    case XR_SESSION_STATE_VISIBLE:
+        // Either session now visible or lost focus
+        if (oldState == XR_SESSION_STATE_FOCUSED)
+            onSessionStateUnfocus(session);
+        break;
+    case XR_SESSION_STATE_FOCUSED:
+        // Session visible and in focus
+        onSessionStateFocus(session);
+        break;
+    case XR_SESSION_STATE_STOPPING:
+        // Session now stopping
+        onSessionStateStopping(session, false);
+        break;
+    case XR_SESSION_STATE_LOSS_PENDING:
+        // Session loss is pending, which can happen at any time
+        if (oldState == XR_SESSION_STATE_FOCUSED)
+            onSessionStateUnfocus(session);
+        if (session->isRunning())
+            onSessionStateStopping(session, true);
+        // Attempt restart
+        onSessionStateEnd(session, true);
+        break;
+    case XR_SESSION_STATE_EXITING:
+        // Session is exiting and should be cleaned up
+        onSessionStateEnd(session, false);
+        break;
+    default:
+        OSG_WARN << "Unknown OpenXR session state: " << event->state << std::endl;
+        break;
+    }
+}
+
+void EventHandler::onSessionStateStart(Session *session)
+{
+}
+
+void EventHandler::onSessionStateEnd(Session *session, bool retry)
+{
+}
+
+void EventHandler::onSessionStateReady(Session *session)
+{
+}
+
+void EventHandler::onSessionStateStopping(Session *session, bool loss)
+{
+}
+
+void EventHandler::onSessionStateFocus(Session *session)
+{
+}
+
+void EventHandler::onSessionStateUnfocus(Session *session)
+{
+}
diff --git a/3rdparty/osgXR/src/OpenXR/EventHandler.h b/3rdparty/osgXR/src/OpenXR/EventHandler.h
new file mode 100644
index 000000000..6a4770663
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/EventHandler.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_EVENT_HANDLER
+#define OSGXR_OPENXR_EVENT_HANDLER 1
+
+#include <osg/Referenced>
+
+#include <openxr/openxr.h>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class Instance;
+class Session;
+
+/// This class handles OpenXR events.
+class EventHandler : public osg::Referenced
+{
+    public:
+
+        // Instance events
+
+        /// Top level OpenXR event handler.
+        void onEvent(Instance *instance, const XrEventDataBuffer *event);
+        /// Handle an otherwise unhandled event.
+        virtual void onUnhandledEvent(Instance *instance,
+                                      const XrEventDataBuffer *event);
+
+        /// Handle an events lost event.
+        virtual void onEventsLost(Instance *instance,
+                                  const XrEventDataEventsLost *event);
+        /// Handle an instance loss pending event.
+        virtual void onInstanceLossPending(Instance *instance,
+                                           const XrEventDataInstanceLossPending *event);
+
+        // Session events
+
+        /// Handle an interaction profile changed event.
+        virtual void onInteractionProfileChanged(Session *session,
+                                                 const XrEventDataInteractionProfileChanged *event);
+        /// Handle a reference space change pending event.
+        virtual void onReferenceSpaceChangePending(Session *session,
+                                                   const XrEventDataReferenceSpaceChangePending *event);
+        /// Handle a visibility mask change event.
+        virtual void onVisibilityMaskChanged(Session *session,
+                                             const XrEventDataVisibilityMaskChangedKHR *event);
+        /// Handle a session state change event.
+        virtual void onSessionStateChanged(Session *session,
+                                           const XrEventDataSessionStateChanged *event);
+
+        // Session state events
+
+        /// Transition into initial idle state (idle, after init).
+        virtual void onSessionStateStart(Session *session);
+        /// Transition into ending state (exiting / loss pending, before cleanup).
+        virtual void onSessionStateEnd(Session *session, bool retry);
+
+        /// Transition into a ready state.
+        virtual void onSessionStateReady(Session *session);
+        /// Transition out of running state (stopping, before end).
+        virtual void onSessionStateStopping(Session *session, bool loss);
+
+        /// Transition into focused session state.
+        virtual void onSessionStateFocus(Session *session);
+        /// Transition out of focused session state.
+        virtual void onSessionStateUnfocus(Session *session);
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/GraphicsBinding.cpp b/3rdparty/osgXR/src/OpenXR/GraphicsBinding.cpp
new file mode 100644
index 000000000..b5d26e2d0
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/GraphicsBinding.cpp
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "GraphicsBinding.h"
+#include "GraphicsBindingWin32.h"
+#include "GraphicsBindingX11.h"
+
+#include <vector>
+
+using namespace osgXR::OpenXR;
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class GraphicsBindingProxy : public osg::Referenced
+{
+    public:
+        virtual ~GraphicsBindingProxy() {}
+
+        virtual GraphicsBinding *create(osgViewer::GraphicsWindow *window) = 0;
+};
+
+template <typename GRAPHICS_BINDING>
+class GraphicsBindingProxyImpl : public GraphicsBindingProxy
+{
+    protected:
+        typedef GRAPHICS_BINDING Binding;
+        typedef typename Binding::GraphicsWindow Window;
+
+        virtual ~GraphicsBindingProxyImpl() {}
+
+    public:
+        GraphicsBinding *create(osgViewer::GraphicsWindow *window) override
+        {
+            Window *win = dynamic_cast<Window *>(window);
+            if (!win)
+                return nullptr;
+
+            return new Binding(win);
+        }
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+typedef std::vector<osg::ref_ptr<GraphicsBindingProxy> > ProxyList;
+
+static ProxyList proxies = {
+#ifdef OSGXR_USE_WIN32
+    new GraphicsBindingProxyImpl<GraphicsBindingWin32>(),
+#endif
+#ifdef OSGXR_USE_X11
+    new GraphicsBindingProxyImpl<GraphicsBindingX11>(),
+#endif
+};
+
+osg::ref_ptr<GraphicsBinding> osgXR::OpenXR::createGraphicsBinding(osgViewer::GraphicsWindow *window)
+{
+    GraphicsBinding *ret = nullptr;
+    for (GraphicsBindingProxy *proxy: proxies)
+    {
+        ret = proxy->create(window);
+        if (ret)
+            break;
+    }
+    return ret;
+}
diff --git a/3rdparty/osgXR/src/OpenXR/GraphicsBinding.h b/3rdparty/osgXR/src/OpenXR/GraphicsBinding.h
new file mode 100644
index 000000000..6caca9700
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/GraphicsBinding.h
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_GRAPHICS_BINDING
+#define OSGXR_OPENXR_GRAPHICS_BINDING 1
+
+#include <osg/Referenced>
+#include <osg/ref_ptr>
+#include <osgViewer/GraphicsWindow>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class GraphicsBinding : public osg::Referenced
+{
+    public:
+        virtual ~GraphicsBinding() { }
+
+        virtual void *getXrGraphicsBinding() = 0;
+};
+
+template <typename GRAPHICS_WINDOW, typename XR_BINDING>
+class GraphicsBindingImpl : public GraphicsBinding
+{
+    public:
+        typedef GRAPHICS_WINDOW GraphicsWindow;
+
+        GraphicsBindingImpl(GraphicsWindow *window);
+        virtual ~GraphicsBindingImpl() {}
+
+        void *getXrGraphicsBinding() override
+        {
+            return &_binding;
+        }
+
+    protected:
+
+        XR_BINDING _binding;
+};
+
+osg::ref_ptr<GraphicsBinding> createGraphicsBinding(osgViewer::GraphicsWindow *window);
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/GraphicsBindingWin32.cpp b/3rdparty/osgXR/src/OpenXR/GraphicsBindingWin32.cpp
new file mode 100644
index 000000000..d0f2c52bb
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/GraphicsBindingWin32.cpp
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "GraphicsBindingWin32.h"
+
+using namespace osgXR::OpenXR;
+
+template <>
+GraphicsBindingWin32::GraphicsBindingImpl(osgViewer::GraphicsWindowWin32 *window) :
+    _binding{ XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR }
+{
+    _binding.hDC = window->getHDC();
+    _binding.hGLRC = window->getWGLContext();
+}
diff --git a/3rdparty/osgXR/src/OpenXR/GraphicsBindingWin32.h b/3rdparty/osgXR/src/OpenXR/GraphicsBindingWin32.h
new file mode 100644
index 000000000..d7ed1e643
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/GraphicsBindingWin32.h
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_GRAPHICS_BINDING_WIN32
+#define OSGXR_OPENXR_GRAPHICS_BINDING_WIN32 1
+
+#ifdef OSGXR_USE_WIN32
+
+#include "GraphicsBinding.h"
+
+#include <osgViewer/api/Win32/GraphicsWindowWin32>
+
+#define XR_USE_GRAPHICS_API_OPENGL
+#define XR_USE_PLATFORM_WIN32
+#include <openxr/openxr_platform.h>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+typedef GraphicsBindingImpl<osgViewer::GraphicsWindowWin32, XrGraphicsBindingOpenGLWin32KHR> GraphicsBindingWin32;
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif // OSGXR_USE_WIN32
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/GraphicsBindingX11.cpp b/3rdparty/osgXR/src/OpenXR/GraphicsBindingX11.cpp
new file mode 100644
index 000000000..8889d4e7e
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/GraphicsBindingX11.cpp
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "GraphicsBindingX11.h"
+
+using namespace osgXR::OpenXR;
+
+namespace {
+
+/// Class to spy on protected members of GraphicsWindowX11.
+class GraphicsWindowX11Spy : public osgViewer::GraphicsWindowX11
+{
+public:
+    const XVisualInfo *getVisualInfo() const
+    {
+        return _visualInfo;
+    }
+
+    const GLXFBConfig &getFBConfig() const
+    {
+        return _fbConfig;
+    }
+};
+
+}
+
+template <>
+GraphicsBindingX11::GraphicsBindingImpl(osgViewer::GraphicsWindowX11 *window) :
+    _binding{ XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR }
+{
+    // window isn't actually of type GraphicsWindowX11Spy, but this allows us to
+    // spy on protected members that don't have public accessors.
+    auto spyWindow = static_cast<GraphicsWindowX11Spy *>(window);
+
+    _binding.xDisplay = window->getDisplay();
+    _binding.visualid = spyWindow->getVisualInfo()->visualid;
+    _binding.glxFBConfig = spyWindow->getFBConfig();
+    _binding.glxDrawable = window->getWindow();
+    _binding.glxContext = window->getContext();
+}
diff --git a/3rdparty/osgXR/src/OpenXR/GraphicsBindingX11.h b/3rdparty/osgXR/src/OpenXR/GraphicsBindingX11.h
new file mode 100644
index 000000000..bfba987a6
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/GraphicsBindingX11.h
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_GRAPHICS_BINDING_X11
+#define OSGXR_OPENXR_GRAPHICS_BINDING_X11 1
+
+#ifdef OSGXR_USE_X11
+
+#include "GraphicsBinding.h"
+
+#include <osgViewer/api/X11/GraphicsWindowX11>
+
+#define XR_USE_GRAPHICS_API_OPENGL
+#define XR_USE_PLATFORM_XLIB
+#include <openxr/openxr_platform.h>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+typedef GraphicsBindingImpl<osgViewer::GraphicsWindowX11, XrGraphicsBindingOpenGLXlibKHR> GraphicsBindingX11;
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif // OSGXR_USE_X11
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/Instance.cpp b/3rdparty/osgXR/src/OpenXR/Instance.cpp
new file mode 100644
index 000000000..329250e7f
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Instance.cpp
@@ -0,0 +1,398 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "EventHandler.h"
+#include "Instance.h"
+#include "Session.h"
+#include "System.h"
+#include "generated/Version.h"
+
+#include <osg/Notify>
+#include <osg/Version>
+#include <osg/ref_ptr>
+
+#include <cstring>
+#include <vector>
+
+#define ENGINE_NAME     "osgXR"
+#define ENGINE_VERSION  (OSGXR_MAJOR_VERSION << 16 | \
+                         OSGXR_MINOR_VERSION <<  8 | \
+                         OSGXR_PATCH_VERSION)
+#define API_VERSION     XR_MAKE_VERSION(1, 0, 0)
+
+using namespace osgXR::OpenXR;
+
+static std::vector<XrApiLayerProperties> layers;
+static std::vector<XrExtensionProperties> extensions;
+
+static bool enumerateLayers(bool invalidate = false)
+{
+    static bool layersEnumerated = false;
+    if (invalidate)
+    {
+        layers.resize(0);
+        layersEnumerated = false;
+        return false;
+    }
+    if (layersEnumerated)
+    {
+        return true;
+    }
+
+    // Count layers
+    uint32_t layerCount = 0;
+    XrResult res = xrEnumerateApiLayerProperties(0, &layerCount, nullptr);
+    if (XR_FAILED(res))
+    {
+        OSG_WARN << "Failed to count OpenXR API layers: " << res << std::endl;
+        return false;
+    }
+
+    if (layerCount)
+    {
+        // Allocate memory
+        layers.resize(layerCount);
+        for (auto &layer: layers)
+        {
+            layer.type = XR_TYPE_API_LAYER_PROPERTIES;
+            layer.next = nullptr;
+        }
+
+        // Enumerate layers
+        res = xrEnumerateApiLayerProperties(layers.size(), &layerCount, layers.data());
+        if (XR_FAILED(res))
+        {
+            OSG_WARN << "Failed to enumerate " << layerCount
+                     << " OpenXR API layers: " << res << std::endl;
+            return false;
+        }
+
+        // Layers may change at any time
+        layers.resize(layerCount);
+    }
+
+    layersEnumerated = true;
+    return true;
+}
+
+static bool enumerateExtensions(bool invalidate = false)
+{
+    static bool extensionsEnumerated = false;
+    if (invalidate)
+    {
+        extensions.resize(0);
+        extensionsEnumerated = false;
+        return false;
+    }
+    if (extensionsEnumerated)
+    {
+        return true;
+    }
+
+    // Count extensions
+    uint32_t extensionCount;
+    XrResult res = xrEnumerateInstanceExtensionProperties(nullptr, 0, &extensionCount, nullptr);
+    if (XR_FAILED(res))
+    {
+        OSG_WARN << "Failed to count OpenXR instance extensions: " << res << std::endl;
+        return false;
+    }
+
+    if (extensionCount)
+    {
+        // Allocate memory
+        extensions.resize(extensionCount);
+        for (auto &extension: extensions)
+        {
+            extension.type = XR_TYPE_EXTENSION_PROPERTIES;
+            extension.next = nullptr;
+        }
+
+        // Enumerate extensions
+        res = xrEnumerateInstanceExtensionProperties(nullptr, extensions.size(),
+                                                     &extensionCount, extensions.data());
+        if (XR_FAILED(res))
+        {
+            OSG_WARN << "Failed to enumerate " << extensionCount
+                     << " OpenXR instance extensions: " << res << std::endl;
+            return false;
+        }
+
+        // Extensions may change (?)
+        extensions.resize(extensionCount);
+    }
+
+    extensionsEnumerated = true;
+    return true;
+}
+
+void Instance::invalidateLayers()
+{
+    enumerateLayers(true);
+}
+
+void Instance::invalidateExtensions()
+{
+    enumerateExtensions(true);
+}
+
+bool Instance::hasLayer(const char *name)
+{
+    enumerateLayers();
+
+    for (auto &layer: layers)
+    {
+        if (!strncmp(name, layer.layerName, XR_MAX_API_LAYER_NAME_SIZE))
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+bool Instance::hasExtension(const char *name)
+{
+    enumerateExtensions();
+
+    for (auto &extension: extensions)
+    {
+        if (!strncmp(name, extension.extensionName, XR_MAX_EXTENSION_NAME_SIZE))
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+Instance *Instance::instance()
+{
+    static osg::ref_ptr<Instance> s_instance = new Instance();
+    return s_instance;
+}
+
+Instance::Instance(): 
+    _layerValidation(false),
+    _depthInfo(false),
+    _visibilityMask(true),
+    _instance(XR_NULL_HANDLE),
+    _lost(false)
+{
+}
+
+Instance::~Instance()
+{
+    if (_instance != XR_NULL_HANDLE)
+    {
+        // Delete the systems
+        for (System *system: _systems)
+        {
+            delete system;
+        }
+
+        // Destroy the OpenXR instance
+        XrResult res = xrDestroyInstance(_instance);
+        if (XR_FAILED(res))
+        {
+            OSG_WARN << "Failed to destroy OpenXR instance" << std::endl;
+        }
+    }
+}
+
+Instance::InitResult Instance::init(const char *appName, uint32_t appVersion)
+{
+    if (_instance != XR_NULL_HANDLE)
+    {
+        return INIT_SUCCESS;
+    }
+
+    std::vector<const char *> layerNames;
+    std::vector<const char *> extensionNames;
+
+    // Enable validation layer if selected
+    if (_layerValidation && hasLayer(XR_APILAYER_LUNARG_core_validation))
+    {
+        layerNames.push_back(XR_APILAYER_LUNARG_core_validation);
+    }
+
+    // We need OpenGL support
+    if (!hasExtension(XR_KHR_OPENGL_ENABLE_EXTENSION_NAME))
+    {
+        OSG_WARN << "OpenXR runtime doesn't support XR_KHR_opengl_enable extension" << std::endl;
+        return INIT_FAIL;
+    }
+    extensionNames.push_back(XR_KHR_OPENGL_ENABLE_EXTENSION_NAME);
+
+    // Enable depth composition layer support if supported
+    _supportsCompositionLayerDepth = hasExtension(XR_KHR_COMPOSITION_LAYER_DEPTH_EXTENSION_NAME);
+    if (_depthInfo)
+    {
+        if (_supportsCompositionLayerDepth)
+            extensionNames.push_back(XR_KHR_COMPOSITION_LAYER_DEPTH_EXTENSION_NAME);
+        else
+            _depthInfo = false;
+    }
+
+    // Enable visibility mask support if supported
+    _supportsVisibilityMask = hasExtension(XR_KHR_VISIBILITY_MASK_EXTENSION_NAME);
+    if (_visibilityMask)
+    {
+        if (_supportsVisibilityMask)
+            extensionNames.push_back(XR_KHR_VISIBILITY_MASK_EXTENSION_NAME);
+        else
+            _visibilityMask = false;
+    }
+
+    // Create the instance
+    XrInstanceCreateInfo info{ XR_TYPE_INSTANCE_CREATE_INFO };
+    strncpy(info.applicationInfo.applicationName, appName,
+            XR_MAX_APPLICATION_NAME_SIZE - 1);
+    info.applicationInfo.applicationVersion = appVersion;
+    strncpy(info.applicationInfo.engineName, ENGINE_NAME,
+            XR_MAX_ENGINE_NAME_SIZE - 1);
+    info.applicationInfo.engineVersion = ENGINE_VERSION;
+    info.applicationInfo.apiVersion = API_VERSION;
+    info.enabledApiLayerCount = layerNames.size();
+    info.enabledApiLayerNames = layerNames.data();
+    info.enabledExtensionCount = extensionNames.size();
+    info.enabledExtensionNames = extensionNames.data();
+
+    XrResult res = xrCreateInstance(&info, &_instance);
+    if (XR_FAILED(res))
+    {
+        OSG_WARN << "Failed to create OpenXR instance: " << res << std::endl;
+        if (res == XR_ERROR_INSTANCE_LOST)
+            return INIT_LATER;
+        return INIT_FAIL;
+    }
+
+    // Log the runtime properties
+    _properties.type = XR_TYPE_INSTANCE_PROPERTIES;
+    _properties.next = nullptr;
+
+    if (XR_SUCCEEDED(xrGetInstanceProperties(_instance, &_properties)))
+    {
+        OSG_INFO << "OpenXR Runtime: \"" << _properties.runtimeName
+                 << "\" version " << XR_VERSION_MAJOR(_properties.runtimeVersion)
+                 << "." << XR_VERSION_MINOR(_properties.runtimeVersion)
+                 << "." << XR_VERSION_PATCH(_properties.runtimeVersion) << std::endl;
+    }
+
+    // Get extension functions
+    _xrGetOpenGLGraphicsRequirementsKHR = (PFN_xrGetOpenGLGraphicsRequirementsKHR)getProcAddr("xrGetOpenGLGraphicsRequirementsKHR");
+    if (_visibilityMask)
+        _xrGetVisibilityMaskKHR = (PFN_xrGetVisibilityMaskKHR)getProcAddr("xrGetVisibilityMaskKHR");
+
+    return INIT_SUCCESS;
+}
+
+bool Instance::check(XrResult result, const char *warnMsg) const
+{
+    if (XR_FAILED(result))
+    {
+        if (result == XR_ERROR_INSTANCE_LOST)
+            _lost = true;
+
+        char resultName[XR_MAX_RESULT_STRING_SIZE];
+        if (XR_FAILED(xrResultToString(_instance, result, resultName)))
+        {
+            OSG_WARN << warnMsg << ": " << result << std::endl;
+        }
+        else
+        {
+            OSG_WARN << warnMsg << ": " << resultName << std::endl;
+        }
+        return false;
+    }
+    return true;
+}
+
+PFN_xrVoidFunction Instance::getProcAddr(const char *name) const
+{
+    PFN_xrVoidFunction ret = nullptr;
+    check(xrGetInstanceProcAddr(_instance, name, &ret),
+          "Failed to get OpenXR procedure address");
+    return ret;
+}
+
+System *Instance::getSystem(XrFormFactor formFactor, bool *supported)
+{
+    unsigned long ffId = formFactor - 1;
+    if (ffId < _systems.size() && _systems[ffId])
+    {
+        if (supported)
+            *supported = true;
+        return _systems[ffId];
+    }
+
+    XrSystemGetInfo getInfo{ XR_TYPE_SYSTEM_GET_INFO };
+    getInfo.formFactor = formFactor;
+
+    XrSystemId systemId;
+    XrResult res = xrGetSystem(_instance, &getInfo, &systemId);
+    if (res == XR_ERROR_FORM_FACTOR_UNAVAILABLE)
+    {
+        // The system is only *TEMPORARILY* unavailable
+        if (supported)
+            *supported = true;
+        return nullptr;
+    }
+    else if (check(res, "Failed to get OpenXR system"))
+    {
+        if (ffId >= _systems.size())
+            _systems.resize(ffId+1, nullptr);
+
+        if (supported)
+            *supported = true;
+        return _systems[ffId] = new System(this, systemId);
+    }
+
+    if (supported)
+        *supported = false;
+    return nullptr;
+}
+
+void Instance::invalidateSystem(XrFormFactor formFactor)
+{
+    unsigned long ffId = formFactor - 1;
+    if (ffId < _systems.size())
+    {
+        delete _systems[ffId];
+        _systems[ffId] = nullptr;
+    }
+}
+
+void Instance::registerSession(Session *session)
+{
+    _sessions[session->getXrSession()] = session;
+}
+
+void Instance::unregisterSession(Session *session)
+{
+    _sessions.erase(session->getXrSession());
+}
+
+Session *Instance::getSession(XrSession xrSession)
+{
+    auto it = _sessions.find(xrSession);
+    if (it == _sessions.end())
+        return nullptr;
+    return (*it).second;
+}
+
+void Instance::pollEvents(EventHandler *handler)
+{
+    for (;;)
+    {
+        XrEventDataBuffer event;
+        event.type = XR_TYPE_EVENT_DATA_BUFFER;
+        event.next = nullptr;
+
+        XrResult res = xrPollEvent(_instance, &event);
+        if (XR_FAILED(res))
+            break;
+        if (res == XR_EVENT_UNAVAILABLE)
+            break;
+
+        handler->onEvent(this, &event);
+    }
+}
diff --git a/3rdparty/osgXR/src/OpenXR/Instance.h b/3rdparty/osgXR/src/OpenXR/Instance.h
new file mode 100644
index 000000000..bf1da19fe
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Instance.h
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_INSTANCE
+#define OSGXR_OPENXR_INSTANCE 1
+
+#include <map>
+#include <vector>
+
+#include <osg/Referenced>
+#include <osg/observer_ptr>
+
+#include <openxr/openxr.h>
+#define XR_USE_GRAPHICS_API_OPENGL
+#include <openxr/openxr_platform.h>
+
+#define XR_APILAYER_LUNARG_core_validation  "XR_APILAYER_LUNARG_core_validation"
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class EventHandler;
+class System;
+class Session;
+
+class Instance : public osg::Referenced
+{
+    public:
+
+        static Instance *instance();
+
+        Instance();
+        virtual ~Instance();
+
+        // Layers and extensions
+
+        static void invalidateLayers();
+        static void invalidateExtensions();
+        static bool hasLayer(const char *name);
+        static bool hasExtension(const char *name);
+
+        // Instance initialisation
+
+        void setValidationLayer(bool layerValidation)
+        {
+            _layerValidation = layerValidation;
+        }
+
+        void setDepthInfo(bool depthInfo)
+        {
+            _depthInfo = depthInfo;
+        }
+
+        void setVisibilityMask(bool visibilityMask)
+        {
+            _visibilityMask = visibilityMask;
+        }
+
+        typedef enum {
+            /// Instance creation successful.
+            INIT_SUCCESS,
+            /// Instance creation not possible at the moment, try again later.
+            INIT_LATER,
+            /// Instance creation failed.
+            INIT_FAIL,
+        } InitResult;
+        InitResult init(const char *appName, uint32_t appVersion);
+
+        // Error checking
+
+        inline bool valid() const
+        {
+            return _instance != XR_NULL_SYSTEM_ID;
+        }
+
+        inline bool lost() const
+        {
+            return _lost;
+        }
+
+        bool check(XrResult result, const char *warnMsg) const;
+
+        // Conversions
+
+        inline XrInstance getXrInstance() const
+        {
+            return _instance;
+        }
+
+        // Instance properties
+        inline const char *getRuntimeName() const
+        {
+            return _properties.runtimeName;
+        }
+
+        // Extensions
+
+        bool supportsCompositionLayerDepth() const
+        {
+            return _supportsCompositionLayerDepth;
+        }
+
+        bool supportsVisibilityMask() const
+        {
+            return _supportsVisibilityMask;
+        }
+
+        PFN_xrVoidFunction getProcAddr(const char *name) const;
+
+        XrResult getOpenGLGraphicsRequirements(XrSystemId systemId,
+                                               XrGraphicsRequirementsOpenGLKHR* graphicsRequirements) const
+        {
+            if (!_xrGetOpenGLGraphicsRequirementsKHR)
+                return XR_ERROR_FUNCTION_UNSUPPORTED;
+            return _xrGetOpenGLGraphicsRequirementsKHR(_instance, systemId,
+                                                       graphicsRequirements);
+        }
+
+        XrResult xrGetVisibilityMask(XrSession session,
+                                     XrViewConfigurationType viewConfigurationType,
+                                     uint32_t viewIndex,
+                                     XrVisibilityMaskTypeKHR visibilityMaskType,
+                                     XrVisibilityMaskKHR *visibilityMask)
+        {
+            if (!_xrGetVisibilityMaskKHR)
+                return XR_ERROR_FUNCTION_UNSUPPORTED;
+            return _xrGetVisibilityMaskKHR(session, viewConfigurationType,
+                                           viewIndex, visibilityMaskType,
+                                           visibilityMask);
+        }
+
+        // Queries
+
+        System *getSystem(XrFormFactor formFactor, bool *supported = nullptr);
+
+        // Up to caller to ensure no session
+        void invalidateSystem(XrFormFactor formFactor);
+        void registerSession(Session *session);
+        void unregisterSession(Session *session);
+        Session *getSession(XrSession xrSession);
+
+        // Events
+
+        void pollEvents(EventHandler *handler);
+
+    protected:
+
+        // Setup data
+        bool _layerValidation;
+        bool _depthInfo;
+        bool _visibilityMask;
+
+        // Instance data
+        XrInstance _instance;
+        mutable bool _lost;
+
+        // Extension presence
+        bool _supportsCompositionLayerDepth;
+        bool _supportsVisibilityMask;
+        // Extension functions
+        mutable PFN_xrGetOpenGLGraphicsRequirementsKHR _xrGetOpenGLGraphicsRequirementsKHR;
+        mutable PFN_xrGetVisibilityMaskKHR _xrGetVisibilityMaskKHR;
+
+        // Instance properties
+        XrInstanceProperties _properties;
+
+        // Systems
+        mutable std::vector<System *> _systems;
+
+        // Sessions
+        std::map<XrSession, Session *> _sessions;
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/InteractionProfile.cpp b/3rdparty/osgXR/src/OpenXR/InteractionProfile.cpp
new file mode 100644
index 000000000..41d066697
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/InteractionProfile.cpp
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "InteractionProfile.h"
+
+#include <cassert>
+#include <vector>
+
+using namespace osgXR::OpenXR;
+
+InteractionProfile::InteractionProfile(const Path &path) :
+    _path(path)
+{
+}
+
+InteractionProfile::InteractionProfile(Instance *instance,
+                                       const char *vendor, const char *type) :
+    _path(instance, (std::string)"/interaction_profiles/" + vendor + "/" + type)
+{
+}
+
+InteractionProfile::~InteractionProfile()
+{
+}
+
+void InteractionProfile::addBinding(Action *action, const Path &binding)
+{
+    assert(binding.getInstance() == getInstance());
+    _bindings.insert(ActionBindingPair(action, binding.getXrPath()));
+}
+
+bool InteractionProfile::suggestBindings()
+{
+    // No bindings: nothing to do!
+    if (_bindings.empty())
+        return true;
+
+    // Construct binding vector from _bindings map
+    std::vector<XrActionSuggestedBinding> bindings;
+    bindings.reserve(_bindings.size());
+    for (auto pair: _bindings)
+    {
+        if (pair.first->init())
+            bindings.push_back({ pair.first->getXrAction(),
+                               pair.second });
+    }
+
+    // Suggest the bindings
+    XrInteractionProfileSuggestedBinding suggestedBinding{
+        XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING
+    };
+    suggestedBinding.interactionProfile = _path.getXrPath();
+    suggestedBinding.countSuggestedBindings = bindings.size();
+    suggestedBinding.suggestedBindings = bindings.data();
+
+    return check(xrSuggestInteractionProfileBindings(getXrInstance(),
+                                                     &suggestedBinding),
+                 "Failed to suggest interaction profile bindings");
+}
diff --git a/3rdparty/osgXR/src/OpenXR/InteractionProfile.h b/3rdparty/osgXR/src/OpenXR/InteractionProfile.h
new file mode 100644
index 000000000..525eda328
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/InteractionProfile.h
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_INTERACTION_PROFILE
+#define OSGXR_OPENXR_INTERACTION_PROFILE 1
+
+#include "Action.h"
+#include "Path.h"
+
+#include <osg/ref_ptr>
+
+#include <set>
+#include <utility>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class InteractionProfile : public osg::Referenced
+{
+    public:
+
+        InteractionProfile(const Path &path);
+        InteractionProfile(Instance *instance,
+                           const char *vendor, const char *type);
+        virtual ~InteractionProfile();
+
+        // Accessors
+        
+        void addBinding(Action *action, const std::string &binding)
+        {
+            Path path(_path.getInstance(), binding);
+            addBinding(action, path);
+        }
+
+        void addBinding(Action *action, const Path &binding);
+
+        // returns true on success
+        bool suggestBindings();
+
+        // Error checking
+
+        inline bool check(XrResult result, const char *warnMsg) const
+        {
+            return _path.check(result, warnMsg);
+        }
+
+        // Conversions
+
+        inline const osg::ref_ptr<Instance> getInstance() const
+        {
+            return _path.getInstance();
+        }
+
+        inline XrInstance getXrInstance() const
+        {
+            return _path.getXrInstance();
+        }
+
+        inline const Path &getPath() const
+        {
+            return _path;
+        }
+
+    protected:
+
+        // Interaction profile data
+        Path _path;
+        typedef std::pair<osg::ref_ptr<Action>, XrPath> ActionBindingPair;
+        std::set<ActionBindingPair> _bindings;
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/Path.cpp b/3rdparty/osgXR/src/OpenXR/Path.cpp
new file mode 100644
index 000000000..2a1293ee4
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Path.cpp
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "Path.h"
+
+using namespace osgXR::OpenXR;
+
+Path::Path(Instance *instance,
+           XrPath path) :
+    _instance(instance),
+    _path(path)
+{
+}
+
+Path::Path(Instance *instance,
+           const std::string &path) :
+    _instance(instance),
+    _path(XR_NULL_PATH)
+{
+    check(xrStringToPath(getXrInstance(), path.c_str(), &_path),
+          "Failed to create OpenXR path from string");
+}
+
+std::string Path::toString() const
+{
+    if (!valid())
+        return "";
+
+    uint32_t count;
+    if (!check(xrPathToString(getXrInstance(), _path,
+                              0, &count, nullptr),
+               "Failed to size OpenXR path string"))
+        return "";
+    std::vector<char> buffer(count);
+    if (!check(xrPathToString(getXrInstance(), _path,
+                              buffer.size(), &count, buffer.data()),
+               "Failed to get OpenXR path string"))
+        return "";
+
+    return buffer.data();
+}
diff --git a/3rdparty/osgXR/src/OpenXR/Path.h b/3rdparty/osgXR/src/OpenXR/Path.h
new file mode 100644
index 000000000..6675067b7
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Path.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_PATH
+#define OSGXR_OPENXR_PATH 1
+
+#include "Instance.h"
+
+#include <osg/ref_ptr>
+
+#include <string>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class Path
+{
+    public:
+
+        Path(Instance *instance = nullptr, XrPath path = XR_NULL_PATH);
+        Path(Instance *instance, const std::string &path);
+
+        // Error checking
+
+        inline bool valid() const
+        {
+            return _path != XR_NULL_PATH;
+        }
+
+        inline bool check(XrResult result, const char *warnMsg) const
+        {
+            return _instance->check(result, warnMsg);
+        }
+
+        // Conversions
+
+        inline const osg::ref_ptr<Instance> getInstance() const
+        {
+            return _instance;
+        }
+
+        inline XrInstance getXrInstance() const
+        {
+            return _instance->getXrInstance();
+        }
+
+        inline XrPath getXrPath() const
+        {
+            return _path;
+        }
+
+        std::string toString() const;
+
+        // Comparisons
+
+        bool operator == (const Path &other) const
+        {
+            return _path == other._path &&
+                   _instance == other._instance;
+        }
+
+        bool operator != (const Path &other) const
+        {
+            return _path != other._path ||
+                   _instance != other._instance;
+        }
+
+    protected:
+
+        // Path data
+        osg::ref_ptr<Instance> _instance;
+        XrPath _path;
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/Session.cpp b/3rdparty/osgXR/src/OpenXR/Session.cpp
new file mode 100644
index 000000000..82ec24db8
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Session.cpp
@@ -0,0 +1,519 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#define XR_USE_GRAPHICS_API_OPENGL
+#include <openxr/openxr_platform.h>
+
+#include "ActionSet.h"
+#include "Compositor.h"
+#include "Session.h"
+#include "GraphicsBinding.h"
+
+#include <osg/Notify>
+
+#include <cassert>
+#include <vector>
+
+#ifdef OSGXR_USE_X11
+#include <osgViewer/api/X11/GraphicsWindowX11>
+#include <GL/glx.h>
+#endif // OSGXR_USE_X11
+
+using namespace osgXR::OpenXR;
+
+Session::Session(System *system,
+                 osgViewer::GraphicsWindow *window) :
+    _window(window),
+    _instance(system->getInstance()),
+    _system(system),
+    _session(XR_NULL_HANDLE),
+    _viewConfiguration(nullptr),
+    _actionSyncCount(0),
+    _state(XR_SESSION_STATE_UNKNOWN),
+    _running(false),
+    _exiting(false),
+    _readSwapchainFormats(false),
+    _lastDisplayTime(0)
+{
+    XrSessionCreateInfo createInfo = { XR_TYPE_SESSION_CREATE_INFO };
+    createInfo.systemId = getXrSystemId();
+
+    // Get OpenGL graphics requirements
+    XrGraphicsRequirementsOpenGLKHR req;
+    req.type = XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR;
+    req.next = nullptr;
+    check(_instance->getOpenGLGraphicsRequirements(getXrSystemId(), &req),
+            "Failed to get OpenXR's OpenGL graphics requirements");
+    // ... and pretty much ignore what it says
+
+    osg::ref_ptr<GraphicsBinding> graphicsBinding = createGraphicsBinding(window);
+    if (graphicsBinding == nullptr)
+    {
+        OSG_WARN << "Failed to get OpenXR graphics binding" << std::endl;
+        return;
+    }
+
+    createInfo.next = graphicsBinding->getXrGraphicsBinding();
+
+    // GL context must not be bound in another thread
+    bool currentSet = checkCurrent();
+    // As of 2021-12-16 Monado expects the GL context to be current
+    // See https://gitlab.freedesktop.org/monado/monado/-/issues/145
+    if (!currentSet)
+        makeCurrent();
+    if (check(xrCreateSession(getXrInstance(), &createInfo, &_session),
+              "Failed to create OpenXR session"))
+    {
+        _instance->registerSession(this);
+    }
+    if (!currentSet)
+        releaseContext();
+}
+
+Session::~Session()
+{
+    if (_session != XR_NULL_HANDLE)
+    {
+        _instance->unregisterSession(this);
+        _localSpace = nullptr;
+        // GL context must not be bound in another thread
+        check(xrDestroySession(_session),
+              "Failed to destroy OpenXR session");
+    }
+}
+
+void Session::addActionSet(ActionSet *actionSet)
+{
+    assert(actionSet->getInstance() == getInstance());
+    _actionSets.insert(actionSet);
+}
+
+bool Session::attachActionSets()
+{
+    assert(valid());
+    if (_actionSets.empty())
+        return false;
+
+    // Construct vector of XrActionSets
+    std::vector<XrActionSet> actionSets;
+    actionSets.reserve(_actionSets.size());
+    for (auto actionSet: _actionSets)
+        actionSets.push_back(actionSet->getXrActionSet());
+
+    XrSessionActionSetsAttachInfo attachInfo{ XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO };
+    attachInfo.countActionSets = actionSets.size();
+    attachInfo.actionSets = actionSets.data();
+
+    return check(xrAttachSessionActionSets(_session, &attachInfo),
+                 "Failed to attach action sets to OpenXR session");
+}
+
+Path Session::getCurrentInteractionProfile(const Path &subactionPath) const
+{
+    XrInteractionProfileState interactionProfile{ XR_TYPE_INTERACTION_PROFILE_STATE };
+
+    if (check(xrGetCurrentInteractionProfile(_session, subactionPath.getXrPath(),
+                                             &interactionProfile),
+              "Failed to get OpenXR current interaction profile"))
+    {
+        return Path(getInstance(), interactionProfile.interactionProfile);
+    }
+    return Path();
+}
+
+bool Session::getActionBoundSources(Action *action,
+                                    std::vector<XrPath> &sourcePaths) const
+{
+    if (!valid())
+        return false;
+
+    // Count bound sources
+    XrBoundSourcesForActionEnumerateInfo enumerateInfo{ XR_TYPE_BOUND_SOURCES_FOR_ACTION_ENUMERATE_INFO };
+    enumerateInfo.action = action->getXrAction();
+    uint32_t count;
+    if (check(xrEnumerateBoundSourcesForAction(_session, &enumerateInfo,
+                                                0, &count, nullptr),
+               "Failed to count OpenXR action bound sources"))
+    {
+        // Resize output buffer
+        sourcePaths.resize(count);
+        if (!count)
+            return true;
+
+        // Fill buffer
+        if (check(xrEnumerateBoundSourcesForAction(_session, &enumerateInfo,
+                                                   sourcePaths.size(),
+                                                   &count,
+                                                   sourcePaths.data()),
+                  "Failed to enumerate OpenXR action bound sources"))
+        {
+            // Success!
+            if (count < sourcePaths.size())
+                sourcePaths.resize(count);
+            return true;
+        }
+    }
+
+    // Failure!
+    return false;
+}
+
+std::string Session::getInputSourceLocalizedName(XrPath sourcePath,
+                                                 XrInputSourceLocalizedNameFlags whichComponents) const
+{
+    if (!valid())
+        return "";
+
+    XrInputSourceLocalizedNameGetInfo getInfo{ XR_TYPE_INPUT_SOURCE_LOCALIZED_NAME_GET_INFO };
+    getInfo.sourcePath = sourcePath;
+    getInfo.whichComponents = whichComponents;
+
+    uint32_t count;
+    if (!check(xrGetInputSourceLocalizedName(_session, &getInfo,
+                              0, &count, nullptr),
+               "Failed to size OpenXR input source localized name string"))
+        return "";
+    std::vector<char> buffer(count);
+    if (!check(xrGetInputSourceLocalizedName(_session, &getInfo,
+                              buffer.size(), &count, buffer.data()),
+               "Failed to get OpenXR input source localized name string"))
+        return "";
+
+    return buffer.data();
+}
+
+void Session::activateActionSet(ActionSet *actionSet, Path subactionPath)
+{
+    assert(_actionSets.count(actionSet));
+    _activeActionSets.insert(ActionSetSubactionPair(actionSet, subactionPath.getXrPath()));
+}
+
+void Session::deactivateActionSet(ActionSet *actionSet, Path subactionPath)
+{
+    _activeActionSets.erase(ActionSetSubactionPair(actionSet, subactionPath.getXrPath()));
+}
+
+bool Session::syncActions()
+{
+    assert(valid());
+
+    XrActionsSyncInfo syncInfo{ XR_TYPE_ACTIONS_SYNC_INFO };
+    std::vector<XrActiveActionSet> actionSets;
+    if (!_activeActionSets.empty())
+    {
+        // Construct vector of XrActionSets
+        actionSets.reserve(_activeActionSets.size());
+        for (auto actionSet: _activeActionSets)
+        {
+            XrActiveActionSet activeActionSet;
+            activeActionSet.actionSet = actionSet.first->getXrActionSet();
+            activeActionSet.subactionPath = actionSet.second;
+            actionSets.push_back(activeActionSet);
+        }
+
+        syncInfo.countActiveActionSets = actionSets.size();
+        syncInfo.activeActionSets = actionSets.data();
+
+        bool ret = check(xrSyncActions(_session, &syncInfo),
+                         "Failed to sync action sets to OpenXR session");
+        if (ret)
+            ++_actionSyncCount;
+        return ret;
+    }
+    else
+    {
+        return false;
+    }
+}
+
+const Session::SwapchainFormats &Session::getSwapchainFormats() const
+{
+    if (!_readSwapchainFormats && valid())
+    {
+        uint32_t formatCount;
+        if (check(xrEnumerateSwapchainFormats(_session, 0, &formatCount, nullptr),
+                  "Failed to count OpenXR swapchain formats"))
+        {
+            if (formatCount)
+            {
+                _swapchainFormats.resize(formatCount);
+                if (!check(xrEnumerateSwapchainFormats(_session, formatCount,
+                                               &formatCount, _swapchainFormats.data()),
+                           "Failed to enumerate OpenXR swapchain formats"))
+                {
+                    _swapchainFormats.resize(0);
+                }
+            }
+        }
+
+        _readSwapchainFormats = true;
+    }
+
+    return _swapchainFormats;
+}
+
+Space *Session::getLocalSpace()
+{
+    if (!_localSpace.valid())
+        _localSpace = new Space(this, XR_REFERENCE_SPACE_TYPE_LOCAL);
+
+    return _localSpace;
+}
+
+void Session::updateVisibilityMasks(XrViewConfigurationType viewConfigurationType,
+                                    uint32_t viewIndex)
+{
+    // Session must be started ...
+    if (!_viewConfiguration)
+        return;
+    // ... and with a matching view configuration
+    if (viewConfigurationType != _viewConfiguration->getType())
+        return;
+
+    if (viewIndex >= _viewConfiguration->getViews().size())
+        return;
+    VisMaskGeometryView &visMaskView = _visMaskCache[viewIndex];
+
+    // Regenerate cached visibility mask geometries for this viewIndex
+    for (uint32_t visMaskType = 0; visMaskType < visMaskView.size(); ++visMaskType)
+        if (visMaskView[visMaskType].valid())
+            getVisibilityMask(viewIndex, static_cast<XrVisibilityMaskTypeKHR>(1 + visMaskType), true);
+}
+
+osg::ref_ptr<osg::Geometry> Session::getVisibilityMask(uint32_t viewIndex,
+                                                       XrVisibilityMaskTypeKHR visibilityMaskType,
+                                                       bool force)
+{
+    if (!_viewConfiguration)
+        return nullptr;
+    if (viewIndex >= _viewConfiguration->getViews().size())
+        return nullptr;
+    if (visibilityMaskType == 0 || visibilityMaskType > XR_VISIBILITY_MASK_TYPE_LINE_LOOP_KHR)
+        return nullptr;
+
+    // Size cache to match number of views...
+    if (_visMaskCache.size() == 0)
+        _visMaskCache.resize(_viewConfiguration->getViews().size());
+    // ... and number of vis mask types
+    VisMaskGeometryView &visMaskView = _visMaskCache[viewIndex];
+    if (visMaskView.size() == 0)
+        visMaskView.resize(XR_VISIBILITY_MASK_TYPE_LINE_LOOP_KHR);
+    // Cache hit?
+    VisMaskGeometry &visMaskGeometry = visMaskView[visibilityMaskType - 1];
+    if (!force && visMaskGeometry.valid())
+        return visMaskGeometry;
+
+    // Get counts of visibility mask
+    XrVisibilityMaskKHR visibilityMask{ XR_TYPE_VISIBILITY_MASK_KHR };
+    XrResult res = xrGetVisibilityMask(*_viewConfiguration, viewIndex,
+                                   visibilityMaskType, &visibilityMask);
+    if (res != XR_ERROR_FUNCTION_UNSUPPORTED &&
+        check(res, "Failed to size OpenXR visibility mask"))
+    {
+        osg::PrimitiveSet::Mode mode;
+        switch (visibilityMaskType)
+        {
+        case XR_VISIBILITY_MASK_TYPE_HIDDEN_TRIANGLE_MESH_KHR:
+            // fall through
+        case XR_VISIBILITY_MASK_TYPE_VISIBLE_TRIANGLE_MESH_KHR:
+            mode = osg::PrimitiveSet::TRIANGLES;
+            break;
+        case XR_VISIBILITY_MASK_TYPE_LINE_LOOP_KHR:
+            mode = osg::PrimitiveSet::LINE_LOOP;
+            break;
+        default:
+            return nullptr;
+        }
+
+        // Allocate space for data
+        osg::ref_ptr<osg::Vec2Array> vertices = new osg::Vec2Array(visibilityMask.vertexCountOutput);
+        osg::ref_ptr<osg::DrawElementsUInt> indices = new osg::DrawElementsUInt(mode, visibilityMask.indexCountOutput);
+
+        // Get the actual data
+        static_assert(sizeof((*vertices)[0]) == sizeof(XrVector2f));
+        static_assert(sizeof((*indices)[0]) == sizeof(uint32_t));
+        visibilityMask.vertexCapacityInput = vertices->size();
+        visibilityMask.vertices = reinterpret_cast<XrVector2f *>(&vertices->front());
+        visibilityMask.indexCapacityInput = indices->size();
+        visibilityMask.indices = reinterpret_cast<uint32_t *>(&indices->front());
+        XrResult res = xrGetVisibilityMask(*_viewConfiguration, viewIndex,
+                                           visibilityMaskType, &visibilityMask);
+        if (check(res, "Failed to get OpenXR visibility mask"))
+        {
+            if (!visMaskGeometry.valid())
+            {
+                // Create a new geometry object
+                osg::Geometry *geometry = new osg::Geometry();
+                geometry->setVertexArray(vertices);
+                geometry->addPrimitiveSet(indices);
+                visMaskGeometry = geometry;
+                return geometry;
+            }
+            else
+            {
+                // Update the existing geometry object
+                osg::Geometry *geometry = visMaskGeometry.get();
+                geometry->setVertexArray(vertices);
+                geometry->setPrimitiveSet(0, indices);
+                return geometry;
+            }
+        }
+    }
+
+    return nullptr;
+}
+
+bool Session::checkCurrent() const
+{
+#ifdef OSGXR_USE_X11
+    // Ugly X11 specific hack
+    const auto *window = dynamic_cast<const osgViewer::GraphicsWindowX11*>(_window.get());
+    return glXGetCurrentContext() == window->getContext();
+#else
+    return true;
+#endif
+}
+
+void Session::makeCurrent() const
+{
+#ifdef OSGXR_USE_X11
+    _window->makeCurrentImplementation();
+#endif
+}
+
+void Session::releaseContext() const
+{
+#ifdef OSGXR_USE_X11
+    _window->releaseContextImplementation();
+#endif
+}
+
+bool Session::begin(const System::ViewConfiguration &viewConfiguration)
+{
+    _viewConfiguration = &viewConfiguration;
+
+    XrSessionBeginInfo beginInfo{ XR_TYPE_SESSION_BEGIN_INFO };
+    beginInfo.primaryViewConfigurationType = viewConfiguration.getType();
+    if (check(xrBeginSession(_session, &beginInfo),
+              "Failed to begin OpenXR session"))
+    {
+        _running = true;
+        return true;
+    }
+    return false;
+}
+
+void Session::end()
+{
+    check(xrEndSession(_session),
+          "Failed to end OpenXR session");
+    _running = false;
+    _viewConfiguration = nullptr;
+    _visMaskCache.resize(0);
+}
+
+void Session::requestExit()
+{
+    _exiting = true;
+    if (isRunning())
+        check(xrRequestExitSession(_session),
+              "Failed to request OpenXR exit");
+}
+
+osg::ref_ptr<Session::Frame> Session::waitFrame()
+{
+    osg::ref_ptr<Frame> frame;
+
+    XrFrameWaitInfo frameWaitInfo{ XR_TYPE_FRAME_WAIT_INFO };
+    XrFrameState frameState;
+    frameState.type = XR_TYPE_FRAME_STATE;
+    frameState.next = nullptr;
+    if (check(xrWaitFrame(_session, &frameWaitInfo, &frameState),
+              "Failed to wait for OpenXR frame"))
+    {
+        frame = new Frame(this, &frameState);
+        _lastDisplayTime = frameState.predictedDisplayTime;
+    }
+
+    return frame;
+}
+
+Session::Frame::Frame(osg::ref_ptr<Session> session, XrFrameState *frameState) :
+    _session(session),
+    _time(frameState->predictedDisplayTime),
+    _period(frameState->predictedDisplayPeriod),
+    _shouldRender(frameState->shouldRender),
+    _osgFrameNumber(0),
+    _locatedViews(false),
+    _begun(false),
+    _envBlendMode(XR_ENVIRONMENT_BLEND_MODE_MAX_ENUM)
+{
+}
+
+Session::Frame::~Frame()
+{
+}
+
+void Session::Frame::locateViews()
+{
+    // Get view locations
+    XrViewLocateInfo locateInfo = { XR_TYPE_VIEW_LOCATE_INFO };
+    locateInfo.viewConfigurationType = _session->getViewConfiguration()->getType();
+    locateInfo.displayTime = _time;
+    locateInfo.space = _session->getLocalSpace()->getXrSpace();
+
+    _viewState = { XR_TYPE_VIEW_STATE };
+
+    uint32_t viewCount;
+    if (!check(xrLocateViews(_session->getXrSession(), &locateInfo, &_viewState, 0, &viewCount, nullptr),
+               "Failed to count OpenXR views"))
+    {
+        return;
+    }
+    _views.resize(viewCount);
+    for (auto &view: _views)
+        view = { XR_TYPE_VIEW };
+    if (!check(xrLocateViews(_session->getXrSession(), &locateInfo, &_viewState, _views.size(), &viewCount, _views.data()),
+               "Failed to locate OpenXR views"))
+    {
+        return;
+    }
+
+    _locatedViews = true;
+}
+
+void Session::Frame::addLayer(osg::ref_ptr<CompositionLayer> layer)
+{
+    _layers.push_back(layer);
+}
+
+bool Session::Frame::begin()
+{
+    XrFrameBeginInfo frameBeginInfo{ XR_TYPE_FRAME_BEGIN_INFO };
+    return _begun = check(xrBeginFrame(_session->getXrSession(), &frameBeginInfo),
+                          "Failed to begin OpenXR frame");
+}
+
+bool Session::Frame::end()
+{
+    std::vector<const XrCompositionLayerBaseHeader *> layers;
+    layers.reserve(_layers.size());
+    for (auto &layer: _layers)
+        layers.push_back(layer->getXr());
+
+    XrFrameEndInfo frameEndInfo{ XR_TYPE_FRAME_END_INFO };
+    frameEndInfo.displayTime = _time;
+    frameEndInfo.environmentBlendMode = _envBlendMode;
+    frameEndInfo.layerCount = layers.size();
+    frameEndInfo.layers = layers.data();
+
+    bool currentSet = _session->checkCurrent();
+    bool ret = check(xrEndFrame(_session->getXrSession(), &frameEndInfo),
+                 "Failed to end OpenXR frame");
+
+    // TODO: should not be necessary, but is for SteamVR 1.16.4 (but not 1.15.x)
+    if (currentSet)
+        _session->makeCurrent();
+
+    return ret;
+}
diff --git a/3rdparty/osgXR/src/OpenXR/Session.h b/3rdparty/osgXR/src/OpenXR/Session.h
new file mode 100644
index 000000000..33607a71a
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Session.h
@@ -0,0 +1,376 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_SESSION
+#define OSGXR_OPENXR_SESSION 1
+
+#include "Path.h"
+#include "System.h"
+
+#include <osg/Geometry>
+#include <osg/Referenced>
+#include <osg/ref_ptr>
+#include <osgViewer/GraphicsWindow>
+#include <OpenThreads/Mutex>
+
+#include <set>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class Action;
+class ActionSet;
+class CompositionLayer;
+class Space;
+
+class Session : public osg::Referenced
+{
+    public:
+
+        // GL context must not be bound in another thread
+        Session(System *system, osgViewer::GraphicsWindow *window);
+        // GL context must not be bound in another thread
+        virtual ~Session();
+
+        // Error checking
+
+        inline bool valid() const
+        {
+            return _session != XR_NULL_HANDLE;
+        }
+
+        inline bool check(XrResult result, const char *warnMsg) const
+        {
+            return _system->check(result, warnMsg);
+        }
+
+        // Action set attachment
+
+        /// Add an action set to the list.
+        void addActionSet(ActionSet *actionSet);
+        /**
+         * Attach the added action sets to the OpenXR session.
+         * @return true on success, false on failure.
+         */
+        bool attachActionSets();
+
+        /// Get the current interaction profile for the given subaction path.
+        Path getCurrentInteractionProfile(const Path &subactionPath) const;
+
+        /// Get a list of bound source paths for an action.
+        bool getActionBoundSources(Action *action,
+                                   std::vector<XrPath> &sourcePaths) const;
+
+        /**
+         * Get a localized name for an input source.
+         * @param sourcePath      Input source path.
+         * @param whichComponents Which components to include.
+         * @return Localized name string
+         */
+        std::string getInputSourceLocalizedName(XrPath sourcePath,
+                                                XrInputSourceLocalizedNameFlags whichComponents) const;
+
+        // Action syncing
+
+        /// Activate a certain action set.
+        void activateActionSet(ActionSet *actionSet,
+                               Path subactionPath = Path());
+        /// Deactivate a certain action set.
+        void deactivateActionSet(ActionSet *actionSet,
+                                 Path subactionPath = Path());
+        /// Sync active action sets.
+        bool syncActions();
+
+        /// Get the number of action sync counts that have taken place.
+        unsigned int getActionSyncCount() const
+        {
+            return _actionSyncCount;
+        }
+
+        // Accessors
+
+        // Find whether the session is ready to begin
+        inline bool isReady() const
+        {
+            return _state == XR_SESSION_STATE_READY;
+        }
+
+        // Find whether the session is running
+        inline bool isRunning() const
+        {
+            return _running;
+        }
+
+        // Find whether the session is already in the process of exiting
+        inline bool isExiting() const
+        {
+            return _exiting;
+        }
+
+        inline osgViewer::GraphicsWindow *getWindow() const
+        {
+            return _window.get();
+        }
+
+        // State management
+
+        inline XrSessionState getState() const
+        {
+            return _state;
+        }
+
+        inline void setState(XrSessionState state)
+        {
+            _state = state;
+        }
+
+
+        // Conversions
+
+        inline const osg::ref_ptr<Instance> getInstance() const
+        {
+            return _instance;
+        }
+
+        inline const System *getSystem() const
+        {
+            return _system;
+        }
+
+        inline XrInstance getXrInstance() const
+        {
+            return _system->getXrInstance();
+        }
+
+        inline XrSystemId getXrSystemId() const
+        {
+            return _system->getXrSystemId();
+        }
+
+        inline XrSession getXrSession()
+        {
+            return _session;
+        }
+
+        // Queries
+
+        typedef std::vector<int64_t> SwapchainFormats;
+        const SwapchainFormats &getSwapchainFormats() const;
+
+        Space *getLocalSpace();
+        XrTime getLastDisplayTime() const
+        {
+            return _lastDisplayTime;
+        }
+
+        void updateVisibilityMasks(XrViewConfigurationType viewConfigurationType,
+                                   uint32_t viewIndex);
+        osg::ref_ptr<osg::Geometry> getVisibilityMask(uint32_t viewIndex,
+                                                      XrVisibilityMaskTypeKHR visibilityMaskType,
+                                                      bool force = false);
+
+        // Operations
+
+        bool checkCurrent() const;
+        void makeCurrent() const;
+        void releaseContext() const;
+
+        bool begin(const System::ViewConfiguration &viewConfiguration);
+        void end();
+        void requestExit();
+
+        const System::ViewConfiguration *getViewConfiguration() const
+        {
+            return _viewConfiguration;
+        }
+
+        class Frame : public osg::Referenced
+        {
+            public:
+
+                Frame(osg::ref_ptr<Session> session, XrFrameState *frameState);
+
+                virtual ~Frame();
+
+                // Error checking
+
+                inline bool check(XrResult result, const char *warnMsg) const
+                {
+                    return _session->check(result, warnMsg);
+                }
+
+                // Accessors
+
+                inline bool shouldRender() const
+                {
+                    return _shouldRender;
+                }
+
+                inline bool hasBegun() const
+                {
+                    return _begun;
+                }
+
+                inline XrTime getTime() const
+                {
+                    return _time;
+                }
+
+                void locateViews();
+
+                void checkLocateViews()
+                {
+                    OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_locateViewsMutex);
+                    if (!_locatedViews)
+                        locateViews();
+                }
+
+                bool isOrientationValid()
+                {
+                    checkLocateViews();
+                    return _viewState.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT;
+                }
+                bool isPositionValid()
+                {
+                    checkLocateViews();
+                    return _viewState.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT;
+                }
+                bool isOrientationTracked()
+                {
+                    checkLocateViews();
+                    return _viewState.viewStateFlags & XR_VIEW_STATE_ORIENTATION_TRACKED_BIT;
+                }
+                bool isPositionTracked()
+                {
+                    checkLocateViews();
+                    return _viewState.viewStateFlags & XR_VIEW_STATE_POSITION_TRACKED_BIT;
+                }
+                uint32_t getNumViews()
+                {
+                    checkLocateViews();
+                    return _views.size();
+                }
+                const XrFovf &getViewFov(uint32_t index)
+                {
+                    checkLocateViews();
+                    return _views[index].fov;
+                }
+                const XrPosef &getViewPose(uint32_t index)
+                {
+                    checkLocateViews();
+                    return _views[index].pose;
+                }
+
+                // Modifiers
+
+                inline void setEnvBlendMode(XrEnvironmentBlendMode envBlendMode)
+                {
+                    _envBlendMode = envBlendMode;
+                }
+                inline XrEnvironmentBlendMode getEnvBlendMode() const
+                {
+                    return _envBlendMode;
+                }
+
+                inline void setOsgFrameNumber(unsigned int osgFrameNumber)
+                {
+                    _osgFrameNumber = osgFrameNumber;
+                }
+                inline unsigned int getOsgFrameNumber() const
+                {
+                    return _osgFrameNumber;
+                }
+
+                void addLayer(osg::ref_ptr<CompositionLayer> layer);
+
+                // Operations
+
+                bool begin();
+                bool end();
+
+            protected:
+
+                // Frame info
+                osg::ref_ptr<Session> _session;
+                XrTime _time;
+                XrDuration _period;
+                bool _shouldRender;
+
+                // OpenSceneGraph frame
+                unsigned int _osgFrameNumber;
+
+                // For access to _locatedViews etc
+                OpenThreads::Mutex _locateViewsMutex;
+
+                // View locations (protected by _locateViewsMutex)
+                bool _locatedViews;
+                XrViewState _viewState;
+                std::vector<XrView> _views;
+
+                // Frame end info
+                bool _begun;
+                XrEnvironmentBlendMode _envBlendMode;
+                std::vector<osg::ref_ptr<CompositionLayer> > _layers;
+        };
+
+        osg::ref_ptr<Frame> waitFrame();
+
+        // OpenXR extension wrappers
+        XrResult xrGetVisibilityMask(const System::ViewConfiguration &viewConfiguration,
+                                   uint32_t viewIndex,
+                                   XrVisibilityMaskTypeKHR visibilityMaskType,
+                                   XrVisibilityMaskKHR *visibilityMask)
+        {
+            return _instance->xrGetVisibilityMask(_session,
+                                                  viewConfiguration.getType(),
+                                                  viewIndex, visibilityMaskType,
+                                                  visibilityMask);
+        }
+
+    protected:
+
+        // Init data
+        osg::observer_ptr<osgViewer::GraphicsWindow> _window;
+
+        // Session data
+        osg::ref_ptr<Instance> _instance;
+        const System *_system;
+        XrSession _session;
+        const System::ViewConfiguration *_viewConfiguration;
+
+        // Action sets
+        std::set<osg::ref_ptr<ActionSet>> _actionSets;
+        typedef std::pair<ActionSet *, XrPath> ActionSetSubactionPair;
+        std::set<ActionSetSubactionPair> _activeActionSets;
+        unsigned int _actionSyncCount;
+
+        // Session state
+        XrSessionState _state;
+        bool _running;
+        bool _exiting;
+
+        // Swapchain formats
+        mutable bool _readSwapchainFormats;
+        mutable SwapchainFormats _swapchainFormats;
+
+        // Reference spaces
+        osg::ref_ptr<Space> _localSpace;
+        XrTime _lastDisplayTime;
+
+        /*
+         * Visibility mask geometry cache.
+         * We keep visibility mask geometries cached to avoid duplication and so
+         * we can update them after a VisibilityMaskChangedKHR event.
+         */
+        typedef osg::ref_ptr<osg::Geometry> VisMaskGeometry;
+        typedef std::vector<VisMaskGeometry> VisMaskGeometryView;
+        typedef std::vector<VisMaskGeometryView> VisMaskGeometries;
+        VisMaskGeometries _visMaskCache;
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/Space.cpp b/3rdparty/osgXR/src/OpenXR/Space.cpp
new file mode 100644
index 000000000..03054b9a2
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Space.cpp
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "Space.h"
+
+#include <cassert>
+
+using namespace osgXR::OpenXR;
+
+static XrPosef poseIdentity = { { 0.0f, 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f, 0.0f } };
+
+Space::Space(Session *session, XrReferenceSpaceType type) :
+    _session(session),
+    _space(XR_NULL_HANDLE)
+{
+    // Attempt to create a reference space
+    XrReferenceSpaceCreateInfo createInfo{ XR_TYPE_REFERENCE_SPACE_CREATE_INFO };
+    createInfo.referenceSpaceType = type;
+    createInfo.poseInReferenceSpace = poseIdentity;
+
+    check(xrCreateReferenceSpace(session->getXrSession(), &createInfo, &_space),
+          "Failed to create OpenXR reference space");
+}
+
+Space::Space(Session *session, ActionPose *action,
+             Path subactionPath) :
+    _session(session),
+    _space(XR_NULL_HANDLE)
+{
+    // Attempt to create an action space for this pose action
+    XrActionSpaceCreateInfo createInfo{ XR_TYPE_ACTION_SPACE_CREATE_INFO };
+    createInfo.action = action->getXrAction();
+    createInfo.subactionPath = subactionPath.getXrPath();
+    createInfo.poseInActionSpace = poseIdentity;
+
+    check(xrCreateActionSpace(session->getXrSession(), &createInfo, &_space),
+          "Failed to create OpenXR action space");
+}
+
+Space::~Space()
+{
+    if (_session.valid() && valid())
+    {
+        check(xrDestroySpace(_space),
+              "Failed to destroy OpenXR space");
+    }
+}
+
+Space::Location::Location() :
+    _flags(0)
+{
+}
+
+Space::Location::Location(XrSpaceLocationFlags flags,
+                          const osg::Quat &orientation,
+                          const osg::Vec3f &position) :
+    _flags(flags),
+    _orientation(orientation),
+    _position(position)
+{
+}
+
+bool Space::locate(const Space *baseSpace, XrTime time,
+                   Space::Location &location)
+{
+    if (!_session.valid() || !valid())
+        return false;
+    assert(_session == baseSpace->_session);
+
+    XrSpaceLocation spaceLocation{ XR_TYPE_SPACE_LOCATION };
+    bool ret = check(xrLocateSpace(getXrSpace(),
+                                   baseSpace->getXrSpace(),
+                                   time,
+                                   &spaceLocation),
+                     "Failed to locate OpenXR space");
+    if (ret)
+    {
+        osg::Quat orientation(spaceLocation.pose.orientation.x,
+                              spaceLocation.pose.orientation.y,
+                              spaceLocation.pose.orientation.z,
+                              spaceLocation.pose.orientation.w);
+        osg::Vec3f position(spaceLocation.pose.position.x,
+                            spaceLocation.pose.position.y,
+                            spaceLocation.pose.position.z);
+        location = Location(spaceLocation.locationFlags,
+                            orientation,
+                            position);
+    }
+    else
+    {
+        location = Location();
+    }
+    return ret;
+}
diff --git a/3rdparty/osgXR/src/OpenXR/Space.h b/3rdparty/osgXR/src/OpenXR/Space.h
new file mode 100644
index 000000000..ac6ef20c2
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Space.h
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_SPACE
+#define OSGXR_OPENXR_SPACE 1
+
+#include "Action.h"
+#include "Path.h"
+#include "Session.h"
+
+#include <osg/Quat>
+#include <osg/Vec3f>
+#include <osg/observer_ptr>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class Space : public osg::Referenced
+{
+    public:
+
+        /// Create a reference space
+        Space(Session *session, XrReferenceSpaceType type);
+        /// Create an action space
+        Space(Session *session, ActionPose *action,
+              Path subactionPath = Path());
+        virtual ~Space();
+
+        // Error checking
+
+        inline bool valid() const
+        {
+            return _space != XR_NULL_HANDLE;
+        }
+
+        inline bool check(XrResult result, const char *warnMsg) const
+        {
+            return _session->check(result, warnMsg);
+        }
+
+        // Conversions
+
+        inline XrSpace getXrSpace() const
+        {
+            return _space;
+        }
+
+        // Locating a space
+
+        class Location
+        {
+            public:
+
+                // Constructors
+
+                Location();
+                Location(XrSpaceLocationFlags flags,
+                         const osg::Quat &orientation,
+                         const osg::Vec3f &position);
+
+                // Error checking
+
+                inline bool valid() const
+                {
+                    return _flags != 0;
+                }
+
+                // Accessors
+
+                bool isOrientationValid() const
+                {
+                    return _flags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
+                }
+
+                bool isPositionValid() const
+                {
+                    return _flags & XR_SPACE_LOCATION_POSITION_VALID_BIT;
+                }
+
+                bool isOrientationTracked() const
+                {
+                    return _flags & XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT;
+                }
+
+                bool isPositionTracked() const
+                {
+                    return _flags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT;
+                }
+
+                XrSpaceLocationFlags getFlags() const
+                {
+                    return _flags;
+                }
+
+                const osg::Quat &getOrientation() const
+                {
+                    return _orientation;
+                }
+
+                const osg::Vec3f &getPosition() const
+                {
+                    return _position;
+                }
+
+            protected:
+
+                XrSpaceLocationFlags _flags;
+                osg::Quat _orientation;
+                osg::Vec3f _position;
+        };
+
+        bool locate(const Space *baseSpace, XrTime time,
+                    Location &location);
+
+    protected:
+
+        osg::observer_ptr<Session> _session;
+        XrSpace _space;
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/Swapchain.cpp b/3rdparty/osgXR/src/OpenXR/Swapchain.cpp
new file mode 100644
index 000000000..8393ff93b
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Swapchain.cpp
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include <openxr/openxr.h>
+
+#include <cassert>
+
+#include "Swapchain.h"
+
+using namespace osgXR::OpenXR;
+
+Swapchain::Swapchain(osg::ref_ptr<Session> session,
+                     const System::ViewConfiguration::View &view,
+                     XrSwapchainUsageFlags usageFlags,
+                     int64_t format) :
+    _session(session),
+    _swapchain(XR_NULL_HANDLE),
+    _width(view.getRecommendedWidth()),
+    _height(view.getRecommendedHeight()),
+    _samples(view.getRecommendedSamples()),
+    _format(format),
+    _readImageTextures(false)
+{
+    XrSwapchainCreateInfo createInfo{ XR_TYPE_SWAPCHAIN_CREATE_INFO };
+    createInfo.usageFlags = usageFlags;
+    createInfo.format = format;
+    createInfo.sampleCount = _samples;
+    createInfo.width = _width;
+    createInfo.height = _height;
+    createInfo.faceCount = 1;
+    createInfo.arraySize = 1;
+    createInfo.mipCount = 1;
+
+    bool currentSet = _session->checkCurrent();
+    // As of 2021-12-16 Monado expects the GL context to be current
+    // See https://gitlab.freedesktop.org/monado/monado/-/issues/145
+    if (!currentSet)
+        _session->makeCurrent();
+
+    // GL context must not be bound in another thread
+    check(xrCreateSwapchain(getXrSession(), &createInfo, &_swapchain),
+          "Failed to create OpenXR swapchain");
+
+    if (currentSet)
+        // SteamVR 1.16.4 (but not 1.15.x) change context then clear it
+        _session->makeCurrent();
+    else
+        // SteamVR linux_v1.14 changes context without changing back
+        // Monado doesn't change it at all (see above)
+        _session->releaseContext();
+}
+
+Swapchain::~Swapchain()
+{
+    if (valid())
+    {
+        // GL context must not be bound in another thread
+        check(xrDestroySwapchain(_swapchain),
+              "Failed to destroy OpenXR swapchain");
+    }
+}
+
+const Swapchain::ImageTextures &Swapchain::getImageTextures() const
+{
+    if (!_readImageTextures)
+    {
+        // Enumerate the images
+        uint32_t imageCount;
+        // GL context must not be bound in another thread
+        if (check(xrEnumerateSwapchainImages(_swapchain, 0, &imageCount, nullptr),
+                  "Failed to count OpenXR swapchain images"))
+        {
+            if (imageCount)
+            {
+                std::vector<XrSwapchainImageOpenGLKHR> images(imageCount, { XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR });
+                if (check(xrEnumerateSwapchainImages(_swapchain, images.size(), &imageCount,
+                                                     (XrSwapchainImageBaseHeader *)images.data()),
+                          "Failed to enumerate OpenXR swapchain images"))
+                {
+                    for (auto image: images)
+                        _imageTextures.push_back(image.image);
+                }
+            }
+        }
+
+        _readImageTextures = true;
+    }
+
+    return _imageTextures;
+}
+
+osg::ref_ptr<osg::Texture2D> Swapchain::getImageOsgTexture(unsigned int index) const
+{
+    if (_imageOsgTextures.empty())
+    {
+        getImageTextures();
+        _imageOsgTextures.resize(_imageTextures.size());
+    }
+
+    assert(index < _imageOsgTextures.size());
+    if (!_imageOsgTextures[index].valid())
+    {
+        // Create an OSG texture out of it
+        osg::Texture2D *texture = new osg::Texture2D;
+        texture->setTextureSize(getWidth(),
+                                getHeight());
+        texture->setInternalFormat(getFormat());
+        unsigned int contextID = _session->getWindow()->getState()->getContextID();
+        texture->setTextureObject(contextID, new osg::Texture::TextureObject(texture, _imageTextures[index], GL_TEXTURE_2D));
+
+        _imageOsgTextures[index] = texture;
+    }
+
+    return _imageOsgTextures[index];
+}
+
+int Swapchain::acquireImage() const
+{
+    // Acquire a swapchain image
+    uint32_t imageIndex;
+
+    bool currentSet = _session->checkCurrent();
+    // GL context must not be bound in another thread
+    if (check(xrAcquireSwapchainImage(_swapchain, nullptr, &imageIndex),
+              "Failed to acquire swapchain image"))
+    {
+        // TODO: should not be necessary, but is for SteamVR 1.16.4 (but not 1.15.x)
+        if (currentSet)
+            _session->makeCurrent();
+
+        return imageIndex;
+    }
+
+    // TODO: should not be necessary, but is for SteamVR 1.16.4 (but not 1.15.x)
+    if (currentSet)
+        _session->makeCurrent();
+
+    return -1;
+}
+
+bool Swapchain::waitImage(XrDuration timeoutNs) const
+{
+    // Wait on the swapchain image
+    XrSwapchainImageWaitInfo waitInfo = { XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO };
+    waitInfo.timeout = timeoutNs; // 100ms
+
+    bool currentSet = _session->checkCurrent();
+    // GL context must not be bound in another thread
+    bool ret = check(xrWaitSwapchainImage(_swapchain, &waitInfo),
+                     "Failed to wait for swapchain image");
+
+    // TODO: should not be necessary, but is for SteamVR 1.16.4 (but not 1.15.x)
+    if (currentSet)
+        _session->makeCurrent();
+
+    return ret;
+}
+
+void Swapchain::releaseImage() const
+{
+    // Release the swapchain image
+    bool currentSet = _session->checkCurrent();
+    // GL context must not be bound in another thread
+    check(xrReleaseSwapchainImage(_swapchain, nullptr),
+          "Failed to release OpenXR swapchain image");
+
+    // TODO: should not be necessary, but is for SteamVR 1.16.4 (but not 1.15.x)
+    if (currentSet)
+        _session->makeCurrent();
+}
diff --git a/3rdparty/osgXR/src/OpenXR/Swapchain.h b/3rdparty/osgXR/src/OpenXR/Swapchain.h
new file mode 100644
index 000000000..317b43761
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/Swapchain.h
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_SWAPCHAIN
+#define OSGXR_OPENXR_SWAPCHAIN 1
+
+#include "Session.h"
+#include "System.h"
+
+#include <osg/Referenced>
+#include <osg/Texture2D>
+#include <osg/ref_ptr>
+
+#include <cinttypes>
+#include <openxr/openxr.h>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class Swapchain : public osg::Referenced
+{
+    public:
+
+        // GL context must not be bound in another thread
+        Swapchain(osg::ref_ptr<Session> session,
+                  const System::ViewConfiguration::View &view,
+                  XrSwapchainUsageFlags usageFlags,
+                  int64_t format);
+        // GL context must not be bound in another thread
+        virtual ~Swapchain();
+
+        // Error checking
+
+        inline bool valid() const
+        {
+            return _swapchain != XR_NULL_HANDLE;
+        }
+
+        inline bool check(XrResult result, const char *warnMsg) const
+        {
+            return _session->check(result, warnMsg);
+        }
+
+        // Conversions
+
+        inline XrSession getXrSession() const
+        {
+            return _session->getXrSession();
+        }
+
+        inline XrSwapchain getXrSwapchain() const
+        {
+            return _swapchain;
+        }
+
+        // Accessors
+
+        inline uint32_t getWidth() const
+        {
+            return _width;
+        }
+
+        inline uint32_t getHeight() const
+        {
+            return _height;
+        }
+
+        inline uint32_t getSamples() const
+        {
+            return _samples;
+        }
+
+        inline int64_t getFormat() const
+        {
+            return _format;
+        }
+
+        // Queries
+
+        typedef std::vector<GLuint> ImageTextures;
+        // GL context must not be bound in another thread
+        const ImageTextures &getImageTextures() const;
+
+        osg::ref_ptr<osg::Texture2D> getImageOsgTexture(unsigned int index) const;
+
+        // Operations
+
+        // GL context must not be bound in another thread
+        int acquireImage() const;
+        // GL context must not be bound in another thread
+        bool waitImage(XrDuration timeoutNs) const;
+        // GL context must not be bound in another thread
+        void releaseImage() const;
+
+    protected:
+
+        // Session data
+        osg::ref_ptr<Session> _session;
+        XrSwapchain _swapchain;
+        uint32_t _width;
+        uint32_t _height;
+        uint32_t _samples;
+        int64_t _format;
+
+        // Image OpenGL textures
+        mutable bool _readImageTextures;
+        mutable ImageTextures _imageTextures;
+        mutable std::vector<osg::ref_ptr<osg::Texture2D>> _imageOsgTextures;
+
+        // Current image
+        mutable int _currentImage;
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/SwapchainGroup.cpp b/3rdparty/osgXR/src/OpenXR/SwapchainGroup.cpp
new file mode 100644
index 000000000..f24ac958e
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/SwapchainGroup.cpp
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "SwapchainGroup.h"
+
+#include <osg/Notify>
+
+using namespace osgXR::OpenXR;
+
+SwapchainGroup::SwapchainGroup(osg::ref_ptr<Session> session,
+                               const System::ViewConfiguration::View &view,
+                               XrSwapchainUsageFlags usageFlags,
+                               int64_t format,
+                               XrSwapchainUsageFlags depthUsageFlags,
+                               int64_t depthFormat) :
+    _swapchain(new Swapchain(session, view, usageFlags, format))
+{
+    if (depthFormat)
+        _depthSwapchain = new Swapchain(session, view, depthUsageFlags, depthFormat);
+}
+
+SwapchainGroup::~SwapchainGroup()
+{
+}
+
+int SwapchainGroup::acquireImages() const
+{
+    int imageIndex = _swapchain->acquireImage();
+    if (depthValid())
+    {
+        int depthImageIndex = _depthSwapchain->acquireImage();
+        if (imageIndex != depthImageIndex)
+            OSG_WARN << "Depth swapchain image mismatch, expected " << imageIndex
+                     << ", got " << depthImageIndex << std::endl;
+    }
+    return imageIndex;
+}
+
+bool SwapchainGroup::waitImages(XrDuration timeoutNs) const
+{
+    bool ret = _swapchain->waitImage(timeoutNs);
+    if (depthValid())
+    {
+        bool depthRet = _depthSwapchain->waitImage(timeoutNs);
+        ret = ret && depthRet;
+    }
+    return ret;
+}
+
+void SwapchainGroup::releaseImages() const
+{
+    _swapchain->releaseImage();
+    if (depthValid())
+        _depthSwapchain->releaseImage();
+}
diff --git a/3rdparty/osgXR/src/OpenXR/SwapchainGroup.h b/3rdparty/osgXR/src/OpenXR/SwapchainGroup.h
new file mode 100644
index 000000000..93f637f28
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/SwapchainGroup.h
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_SWAPCHAIN_GROUP
+#define OSGXR_OPENXR_SWAPCHAIN_GROUP 1
+
+#include "Swapchain.h"
+
+#include <osg/Referenced>
+#include <osg/ref_ptr>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class SwapchainGroupSubImage;
+
+/// Groups colour and depth swapchains together
+class SwapchainGroup : public osg::Referenced
+{
+    public:
+
+        typedef SwapchainGroupSubImage SubImage;
+
+        // GL context must not be bound in another thread
+        SwapchainGroup(osg::ref_ptr<Session> session,
+                       const System::ViewConfiguration::View &view,
+                       XrSwapchainUsageFlags usageFlags,
+                       int64_t format,
+                       XrSwapchainUsageFlags depthUsageFlags = 0,
+                       int64_t depthFormat = 0);
+        // GL context must not be bound in another thread
+        virtual ~SwapchainGroup();
+
+        // Error checking
+
+        inline bool valid() const
+        {
+            return _swapchain->valid();
+        }
+
+        inline bool depthValid() const
+        {
+            return _depthSwapchain.valid() && _depthSwapchain->valid();
+        }
+
+        // Accessors
+
+        inline osg::ref_ptr<Swapchain> getSwapchain() const
+        {
+            return _swapchain;
+        }
+
+        inline osg::ref_ptr<Swapchain> getDepthSwapchain() const
+        {
+            return _depthSwapchain;
+        }
+
+        inline XrSwapchain getXrSwapchain() const
+        {
+            return _swapchain->getXrSwapchain();
+        }
+
+        inline XrSwapchain getDepthXrSwapchain() const
+        {
+            if (_depthSwapchain.valid())
+                return _depthSwapchain->getXrSwapchain();
+            else
+                return XR_NULL_HANDLE;
+        }
+
+        inline uint32_t getWidth() const
+        {
+            return _swapchain->getWidth();
+        }
+
+        inline uint32_t getHeight() const
+        {
+            return _swapchain->getHeight();
+        }
+
+        inline uint32_t getSamples() const
+        {
+            return _swapchain->getSamples();
+        }
+
+        // Queries
+
+        typedef Swapchain::ImageTextures ImageTextures;
+        const ImageTextures &getImageTextures() const
+        {
+            return _swapchain->getImageTextures();
+        }
+        const ImageTextures &getDepthImageTextures() const
+        {
+            return _depthSwapchain->getImageTextures();
+        }
+
+        // Operations
+
+        int acquireImages() const;
+        bool waitImages(XrDuration timeoutNs) const;
+        void releaseImages() const;
+
+    protected:
+
+        osg::ref_ptr<Swapchain> _swapchain;
+        osg::ref_ptr<Swapchain> _depthSwapchain;
+};
+
+}
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/SwapchainGroupSubImage.h b/3rdparty/osgXR/src/OpenXR/SwapchainGroupSubImage.h
new file mode 100644
index 000000000..1ba66e16d
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/SwapchainGroupSubImage.h
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_SWAPCHAIN_GROUP_SUB_IMAGE
+#define OSGXR_OPENXR_SWAPCHAIN_GROUP_SUB_IMAGE 1
+
+#include "SwapchainGroup.h"
+
+#include <osg/ref_ptr>
+
+#include <openxr/openxr.h>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class SwapchainGroupSubImage
+{
+    public:
+        SwapchainGroupSubImage(SwapchainGroup *group) :
+            _group(group),
+            _x(0),
+            _y(0),
+            _width(group->getWidth()),
+            _height(group->getHeight()),
+            _arrayIndex(0)
+        {
+        }
+
+        SwapchainGroupSubImage(SwapchainGroup *group,
+                               const System::ViewConfiguration::View::Viewport &vp) :
+            _group(group),
+            _x(vp.x),
+            _y(vp.y),
+            _width(vp.width),
+            _height(vp.height),
+            _arrayIndex(vp.arrayIndex)
+        {
+        }
+
+        bool valid() const
+        {
+            return _group->valid();
+        }
+
+        bool depthValid() const
+        {
+            return _group->depthValid();
+        }
+
+        osg::ref_ptr<SwapchainGroup> getSwapchainGroup() const
+        {
+            return _group;
+        }
+
+        uint32_t getX() const
+        {
+            return _x;
+        }
+
+        uint32_t getY() const
+        {
+            return _y;
+        }
+
+        uint32_t getWidth() const
+        {
+            return _width;
+        }
+
+        uint32_t getHeight() const
+        {
+            return _height;
+        }
+
+        uint32_t getArrayIndex() const
+        {
+            return _arrayIndex;
+        }
+
+        void getXrSubImage(XrSwapchainSubImage *out) const
+        {
+            out->swapchain = _group->getXrSwapchain();
+            out->imageRect.offset = { (int32_t)_x,
+                (int32_t)_y };
+            out->imageRect.extent = { (int32_t)_width,
+                (int32_t)_height };
+            out->imageArrayIndex = _arrayIndex;
+        }
+
+        void getDepthXrSubImage(XrSwapchainSubImage *out) const
+        {
+            out->swapchain = _group->getDepthXrSwapchain();
+            out->imageRect.offset = { (int32_t)_x,
+                (int32_t)_y };
+            out->imageRect.extent = { (int32_t)_width,
+                (int32_t)_height };
+            out->imageArrayIndex = _arrayIndex;
+        }
+
+    protected:
+        osg::ref_ptr<SwapchainGroup> _group;
+        uint32_t _x;
+        uint32_t _y;
+        uint32_t _width;
+        uint32_t _height;
+        uint32_t _arrayIndex;
+};
+
+}
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXR/System.cpp b/3rdparty/osgXR/src/OpenXR/System.cpp
new file mode 100644
index 000000000..e19e4ee59
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/System.cpp
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "System.h"
+
+#include <cstring>
+
+using namespace osgXR::OpenXR;
+
+void System::getProperties() const
+{
+    XrSystemProperties properties;
+    properties.type = XR_TYPE_SYSTEM_PROPERTIES;
+    properties.next = nullptr;
+
+    if (check(xrGetSystemProperties(getXrInstance(), _systemId, &properties),
+              "Failed to get OpenXR system properties"))
+    {
+        memcpy(_systemName, properties.systemName, sizeof(_systemName));
+        _orientationTracking = properties.trackingProperties.orientationTracking;
+        _positionTracking = properties.trackingProperties.positionTracking;
+    }
+
+    _readProperties = true;
+}
+
+const System::ViewConfiguration::Views &System::ViewConfiguration::getViews() const
+{
+    if (!_readViews)
+    {
+        uint32_t viewCount = 0;
+        if (check(xrEnumerateViewConfigurationViews(_system->getXrInstance(),
+                                                    _system->getXrSystemId(),
+                                                    _type,
+                                                    0, &viewCount, nullptr),
+                  "Failed to count OpenXR view configuration views"))
+        {
+            if (viewCount)
+            {
+                std::vector<XrViewConfigurationView> views(viewCount,
+                                        { XR_TYPE_VIEW_CONFIGURATION_VIEW });
+                if (check(xrEnumerateViewConfigurationViews(_system->getXrInstance(),
+                                                            _system->getXrSystemId(),
+                                                            _type,
+                                                            views.size(), &viewCount,
+                                                            views.data()),
+                          "Failed to enumerate OpenXR view configuration views"))
+                {
+                    for (auto &view: views)
+                        _views.push_back(View(view));
+                }
+            }
+        }
+
+        _readViews = true;
+    }
+
+    return _views;
+}
+
+const System::ViewConfiguration::EnvBlendModes &System::ViewConfiguration::getEnvBlendModes() const
+{
+    if (!_readEnvBlendModes)
+    {
+        uint32_t blendModeCount = 0;
+        if (check(xrEnumerateEnvironmentBlendModes(_system->getXrInstance(),
+                                                   _system->getXrSystemId(),
+                                                   _type,
+                                                   0, &blendModeCount, nullptr),
+                  "Failed to count OpenXR environment blend modes"))
+        {
+            if (blendModeCount)
+            {
+                _envBlendModes.resize(blendModeCount);
+                if (!check(xrEnumerateEnvironmentBlendModes(_system->getXrInstance(),
+                                                           _system->getXrSystemId(),
+                                                           _type,
+                                                           _envBlendModes.size(),
+                                                           &blendModeCount,
+                                                           _envBlendModes.data()),
+                          "Failed to enumerate OpenXR environment blend modes"))
+                {
+                    _envBlendModes.resize(0);
+                }
+            }
+        }
+
+        _readEnvBlendModes = true;
+    }
+
+    return _envBlendModes;
+}
+
+const System::ViewConfigurations &System::getViewConfigurations() const
+{
+    if (!_readViewConfigurations)
+    {
+        uint32_t viewConfigCount = 0;
+        if (check(xrEnumerateViewConfigurations(getXrInstance(), getXrSystemId(),
+                                                0, &viewConfigCount, nullptr),
+                  "Failed to count OpenXR view configuration types"))
+        {
+            if (viewConfigCount)
+            {
+                std::vector<XrViewConfigurationType> types(viewConfigCount);
+                if (check(xrEnumerateViewConfigurations(getXrInstance(), getXrSystemId(),
+                                                        types.size(), &viewConfigCount,
+                                                        types.data()),
+                          "Failed to enumerate OpenXR view configuration types"))
+                {
+                    _viewConfigurations.reserve(viewConfigCount);
+                    for (auto type: types)
+                        _viewConfigurations.push_back(ViewConfiguration(this, type));
+                }
+            }
+        }
+
+        _readViewConfigurations = true;
+    }
+
+    return _viewConfigurations;
+}
diff --git a/3rdparty/osgXR/src/OpenXR/System.h b/3rdparty/osgXR/src/OpenXR/System.h
new file mode 100644
index 000000000..bfc412731
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXR/System.h
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_OPENXR_SYSTEM
+#define OSGXR_OPENXR_SYSTEM 1
+
+#include "Instance.h"
+
+#include <osg/DisplaySettings>
+#include <osg/ref_ptr>
+
+#include <algorithm>
+#include <vector>
+
+namespace osgXR {
+
+namespace OpenXR {
+
+class System
+{
+    public:
+
+        System(Instance *instance, XrSystemId systemId) :
+            _instance(instance),
+            _systemId(systemId),
+            _readProperties(false),
+            _orientationTracking(false),
+            _positionTracking(false),
+            _readViewConfigurations(false)
+        {
+        }
+
+        // Error checking
+
+        inline bool check(XrResult result, const char *warnMsg) const
+        {
+            return _instance->check(result, warnMsg);
+        }
+
+        // Conversions
+
+        inline Instance *getInstance()
+        {
+            return _instance;
+        }
+        inline const Instance *getInstance() const
+        {
+            return _instance;
+        }
+
+        inline XrInstance getXrInstance() const
+        {
+            return _instance->getXrInstance();
+        }
+
+        inline XrSystemId getXrSystemId() const
+        {
+            return _systemId;
+        }
+
+        // Queries
+
+        void getProperties() const;
+
+        inline const char *getSystemName() const
+        {
+            if (!_readProperties)
+                getProperties();
+            return _systemName;
+        }
+
+        inline bool getOrientationTracking() const
+        {
+            if (!_readProperties)
+                getProperties();
+            return _orientationTracking;
+        }
+
+        inline bool getPositionTracking() const
+        {
+            if (!_readProperties)
+                getProperties();
+            return _positionTracking;
+        }
+
+        class ViewConfiguration
+        {
+
+            public:
+
+                ViewConfiguration(const System *system, XrViewConfigurationType type) :
+                    _system(system),
+                    _type(type),
+                    _readViews(false),
+                    _readEnvBlendModes(false)
+                {
+                }
+
+                XrViewConfigurationType getType() const
+                {
+                    return _type;
+                }
+
+                // Queries
+
+                class View
+                {
+
+                    public:
+
+                        struct Viewport
+                        {
+                            uint32_t x, y, width, height, arrayIndex;
+                        };
+
+                        View(uint32_t recommendedWidth,
+                             uint32_t recommendedHeight,
+                             uint32_t recommendedSamples = 1) :
+                            _recommendedWidth(recommendedWidth),
+                            _recommendedHeight(recommendedHeight),
+                            _recommendedSamples(recommendedSamples)
+                        {
+                        }
+
+                        View(const XrViewConfigurationView &view) :
+                            _recommendedWidth(view.recommendedImageRectWidth),
+                            _recommendedHeight(view.recommendedImageRectHeight),
+                            _recommendedSamples(view.recommendedSwapchainSampleCount)
+                        {
+                        }
+
+                        uint32_t getRecommendedWidth() const
+                        {
+                            return _recommendedWidth;
+                        }
+
+                        uint32_t getRecommendedHeight() const
+                        {
+                            return _recommendedHeight;
+                        }
+
+
+                        uint32_t getRecommendedSamples() const
+                        {
+                            return _recommendedSamples;
+                        }
+
+                        /// Tile another view horizontally after this one
+                        struct Viewport tileHorizontally(const View &other)
+                        {
+                            struct Viewport vp;
+                            vp.x = _recommendedWidth;
+                            vp.y = 0;
+                            vp.width = other._recommendedWidth;
+                            vp.height = other._recommendedHeight;
+                            vp.arrayIndex = 0;
+
+                            _recommendedWidth += other._recommendedWidth;
+                            _recommendedHeight = std::max(_recommendedHeight,
+                                                          other._recommendedHeight);
+                            return vp;
+                        }
+
+                    protected:
+
+                        uint32_t _recommendedWidth;
+                        uint32_t _recommendedHeight;
+                        uint32_t _recommendedSamples;
+                };
+
+                typedef std::vector<View> Views;
+                const Views &getViews() const;
+
+                typedef std::vector<XrEnvironmentBlendMode> EnvBlendModes;
+                const EnvBlendModes &getEnvBlendModes() const;
+
+            protected:
+
+                inline bool check(XrResult result, const char *warnMsg) const
+                {
+                    return _system->getInstance()->check(result, warnMsg);
+                }
+
+                const System *_system;
+                XrViewConfigurationType _type;
+
+                // Views
+                mutable bool _readViews;
+                mutable Views _views;
+
+                // Environment blend modes
+                mutable bool _readEnvBlendModes;
+                mutable EnvBlendModes _envBlendModes;
+        };
+
+        typedef std::vector<ViewConfiguration> ViewConfigurations;
+        const ViewConfigurations &getViewConfigurations() const;
+
+    protected:
+
+        // System data
+        Instance *_instance;
+        XrSystemId _systemId;
+
+        // Properties
+        mutable char _systemName[XR_MAX_SYSTEM_NAME_SIZE];
+        mutable bool _readProperties;
+        mutable bool _orientationTracking;
+        mutable bool _positionTracking;
+
+        // View configurations
+        mutable bool _readViewConfigurations;
+        mutable ViewConfigurations _viewConfigurations;
+
+};
+
+} // osgXR::OpenXR
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/OpenXRDisplay.cpp b/3rdparty/osgXR/src/OpenXRDisplay.cpp
new file mode 100644
index 000000000..63cbfa892
--- /dev/null
+++ b/3rdparty/osgXR/src/OpenXRDisplay.cpp
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include <osgXR/OpenXRDisplay>
+
+#include <osg/ref_ptr>
+#include <osgViewer/ViewerBase>
+
+#include "XRState.h"
+#include "XRRealizeOperation.h"
+
+using namespace osgXR;
+
+OpenXRDisplay::OpenXRDisplay()
+{
+}
+
+OpenXRDisplay::OpenXRDisplay(Settings *settings):
+    _settings(settings)
+{
+}
+
+OpenXRDisplay::OpenXRDisplay(const OpenXRDisplay& rhs,
+                             const osg::CopyOp& copyop):
+    ViewConfig(rhs,copyop),
+    _settings(rhs._settings)
+{
+}
+
+OpenXRDisplay::~OpenXRDisplay()
+{
+}
+
+void OpenXRDisplay::configure(osgViewer::View &view) const
+{
+    osgViewer::ViewerBase *viewer = dynamic_cast<osgViewer::ViewerBase *>(&view);
+    if (!viewer)
+        return;
+
+    _state = new XRState(_settings);
+    viewer->setRealizeOperation(new XRRealizeOperation(_state, &view));
+}
diff --git a/3rdparty/osgXR/src/Settings.cpp b/3rdparty/osgXR/src/Settings.cpp
new file mode 100644
index 000000000..5b56096d9
--- /dev/null
+++ b/3rdparty/osgXR/src/Settings.cpp
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include <osgXR/Settings>
+
+#include <openxr/openxr.h>
+
+#include "OpenXR/Instance.h"
+
+using namespace osgXR;
+
+Settings::Settings() :
+    _appName("osgXR"),
+    _appVersion(1),
+    _validationLayer(false),
+    _depthInfo(false),
+    _visibilityMask(true),
+    _formFactor(HEAD_MOUNTED_DISPLAY),
+    _preferredEnvBlendModeMask(0),
+    _allowedEnvBlendModeMask(0),
+    _vrMode(VRMODE_AUTOMATIC),
+    _swapchainMode(SWAPCHAIN_AUTOMATIC),
+    _unitsPerMeter(1.0f)
+{
+}
+
+Settings::~Settings()
+{
+}
+
+Settings *Settings::instance()
+{
+    static osg::ref_ptr<Settings> settings = new Settings();
+    return settings;
+}
+
+unsigned int Settings::_diff(const Settings &other) const
+{
+    unsigned int ret = DIFF_NONE;
+    if (_appName != other._appName ||
+        _appVersion != other._appVersion)
+        ret |= DIFF_APP_INFO;
+    if (_validationLayer != other._validationLayer)
+        ret |= DIFF_VALIDATION_LAYER;
+    if (_depthInfo != other._depthInfo)
+        ret |= DIFF_DEPTH_INFO;
+    if (_visibilityMask != other._visibilityMask)
+        ret |= DIFF_VISIBILITY_MASK;
+    if (_formFactor != other._formFactor)
+        ret |= DIFF_FORM_FACTOR;
+    if (_preferredEnvBlendModeMask != other._preferredEnvBlendModeMask ||
+        _allowedEnvBlendModeMask != other._allowedEnvBlendModeMask)
+        ret |= DIFF_BLEND_MODE;
+    if (_vrMode != other._vrMode)
+        ret |= DIFF_VR_MODE;
+    if (_swapchainMode != other._swapchainMode)
+        ret |= DIFF_SWAPCHAIN_MODE;
+    if (_mirrorSettings != other._mirrorSettings)
+        ret |= DIFF_MIRROR;
+    if (_unitsPerMeter != other._unitsPerMeter)
+        ret |= DIFF_SCALE;
+    return ret;
+}
diff --git a/3rdparty/osgXR/src/Subaction.cpp b/3rdparty/osgXR/src/Subaction.cpp
new file mode 100644
index 000000000..6584335dd
--- /dev/null
+++ b/3rdparty/osgXR/src/Subaction.cpp
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "Subaction.h"
+#include "XRState.h"
+
+#include <osgXR/Manager>
+
+using namespace osgXR;
+
+// Internal API
+
+Subaction::Private::Private(XRState *state,
+                            const std::string &path) :
+    _state(state),
+    _pathString(path)
+{
+}
+
+void Subaction::Private::registerPublic(Subaction *subaction)
+{
+    _publics.insert(subaction);
+}
+
+void Subaction::Private::unregisterPublic(Subaction *subaction)
+{
+    _publics.erase(subaction);
+}
+
+InteractionProfile *Subaction::Private::getCurrentProfile()
+{
+    if (!_currentProfile.valid())
+    {
+        if (_path.valid())
+            _currentProfile = _state->getCurrentInteractionProfile(_path);
+    }
+
+    return _currentProfile.get();
+}
+
+void Subaction::Private::onInteractionProfileChanged(OpenXR::Session *session)
+{
+    // Ensure path is set up
+    setup(session->getInstance());
+
+    // Find whether this subaction's current interaction profile has changed
+    InteractionProfile *prevProfile = _currentProfile.get();
+    _currentProfile = nullptr;
+    InteractionProfile *newProfile = getCurrentProfile();
+    if (newProfile != prevProfile)
+    {
+        // Notify any derived Subaction classes from the app
+        for (auto *pub: _publics)
+            pub->onProfileChanged(newProfile);
+    }
+}
+
+const OpenXR::Path &Subaction::Private::setup(OpenXR::Instance *instance)
+{
+    if (!_path.valid())
+        _path = OpenXR::Path(instance, _pathString);
+    return _path;
+}
+
+void Subaction::Private::cleanupSession()
+{
+    bool hadProfile = _currentProfile.valid();
+    _currentProfile = nullptr;
+    if (hadProfile)
+    {
+        // Notify any derived Subaction classes from the app
+        for (auto *pub: _publics)
+            pub->onProfileChanged(nullptr);
+    }
+}
+
+void Subaction::Private::cleanupInstance()
+{
+    _path = OpenXR::Path();
+}
+
+// Public API
+
+Subaction::Subaction(Manager *manager,
+                             const std::string &path) :
+    _private(manager->_getXrState()->getSubaction(path))
+{
+    _private->registerPublic(this);
+}
+
+Subaction::~Subaction()
+{
+    _private->unregisterPublic(this);
+}
+
+const std::string &Subaction::getPath() const
+{
+    return _private->getPathString();
+}
+
+InteractionProfile *Subaction::getCurrentProfile()
+{
+    return _private->getCurrentProfile();
+}
+
+void Subaction::onProfileChanged(InteractionProfile *newProfile)
+{
+    // This is for derived classes to implement to their own ends
+}
diff --git a/3rdparty/osgXR/src/Subaction.h b/3rdparty/osgXR/src/Subaction.h
new file mode 100644
index 000000000..272227dd4
--- /dev/null
+++ b/3rdparty/osgXR/src/Subaction.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_SUBACTION
+#define OSGXR_SUBACTION 1
+
+#include <osgXR/Subaction>
+
+#include "OpenXR/Path.h"
+
+#include <set>
+#include <string>
+
+namespace osgXR {
+
+class XRState;
+
+namespace OpenXR {
+    class Instance;
+};
+
+class Subaction::Private
+{
+    public:
+
+        static std::shared_ptr<Private> get(Subaction *pub)
+        {
+            if (pub)
+                return pub->_private;
+            else
+                return nullptr;
+        }
+
+        Private(XRState *state, const std::string &path);
+
+        // Public object registration
+        void registerPublic(Subaction *subaction);
+        void unregisterPublic(Subaction *subaction);
+
+        // Accessors
+
+        /// Get the subaction's path as a string.
+        const std::string &getPathString() const
+        {
+            return _pathString;
+        }
+
+        /// Find the current interaction profile.
+        InteractionProfile *getCurrentProfile();
+
+        // Events
+
+        /// Notify that an interaction profile has changed.
+        void onInteractionProfileChanged(OpenXR::Session *session);
+
+        /// Setup path with an OpenXR instance.
+        const OpenXR::Path &setup(OpenXR::Instance *instance);
+        /// Clean up current profile before an OpenXR session is destroyed.
+        void cleanupSession();
+        /// Clean up path before an OpenXR instance is destroyed.
+        void cleanupInstance();
+
+    private:
+
+        XRState *_state;
+        std::string _pathString;
+        std::set<Subaction *> _publics;
+
+        OpenXR::Path _path;
+        osg::ref_ptr<InteractionProfile> _currentProfile;
+};
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/Version.h.in b/3rdparty/osgXR/src/Version.h.in
new file mode 100644
index 000000000..7b77d8d54
--- /dev/null
+++ b/3rdparty/osgXR/src/Version.h.in
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_Version
+#define OSGXR_Version 1
+
+#define OSGXR_MAJOR_VERSION    @osgXR_MAJOR_VERSION@
+#define OSGXR_MINOR_VERSION    @osgXR_MINOR_VERSION@
+#define OSGXR_PATCH_VERSION    @osgXR_PATCH_VERSION@
+#define OSGXR_SOVERSION        @osgXR_SOVERSION@
+
+#endif
diff --git a/3rdparty/osgXR/src/View.cpp b/3rdparty/osgXR/src/View.cpp
new file mode 100644
index 000000000..fe8009d23
--- /dev/null
+++ b/3rdparty/osgXR/src/View.cpp
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include <osgXR/View>
+
+using namespace osgXR;
+
+View::View(osgViewer::GraphicsWindow *window, osgViewer::View *osgView) :
+    _window(window),
+    _osgView(osgView)
+{
+}
+
+View::~View()
+{
+}
diff --git a/3rdparty/osgXR/src/XRFramebuffer.cpp b/3rdparty/osgXR/src/XRFramebuffer.cpp
new file mode 100644
index 000000000..e8cce0819
--- /dev/null
+++ b/3rdparty/osgXR/src/XRFramebuffer.cpp
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "XRFramebuffer.h"
+
+#include <osg/FrameBufferObject>
+#include <osg/Image>
+#include <osg/State>
+#include <osg/Version>
+
+using namespace osgXR;
+
+#if(OSG_VERSION_GREATER_OR_EQUAL(3, 4, 0))
+typedef osg::GLExtensions OSG_GLExtensions;
+#else
+typedef osg::FBOExtensions OSG_GLExtensions;
+#endif
+
+static const OSG_GLExtensions* getGLExtensions(const osg::State& state)
+{
+#if(OSG_VERSION_GREATER_OR_EQUAL(3, 4, 0))
+    return state.get<osg::GLExtensions>();
+#else
+    return osg::FBOExtensions::instance(state.getContextID(), true);
+#endif
+}
+
+XRFramebuffer::XRFramebuffer(uint32_t width, uint32_t height,
+                             GLuint texture, GLuint depthTexture) :
+    _width(width),
+    _height(height),
+    _depthFormat(GL_DEPTH_COMPONENT16),
+    _fbo(0),
+    _texture(texture),
+    _depthTexture(depthTexture),
+    _generated(false),
+    _boundTexture(false),
+    _boundDepthTexture(false),
+    _deleteDepthTexture(false)
+{
+}
+
+XRFramebuffer::~XRFramebuffer()
+{
+}
+
+bool XRFramebuffer::valid(osg::State &state) const
+{
+    if (!_fbo)
+        return false;
+
+    const OSG_GLExtensions *fbo_ext = getGLExtensions(state);
+    GLenum complete = fbo_ext->glCheckFramebufferStatus(GL_FRAMEBUFFER_EXT);
+    switch (complete)
+    {
+    case GL_FRAMEBUFFER_COMPLETE_EXT:
+        return true;
+    case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT:
+        OSG_WARN << "FBO Incomplete attachment" << std::endl;
+        break;
+    case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
+        OSG_WARN << "FBO Incomplete missing attachment" << std::endl;
+        break;
+    case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
+        OSG_WARN << "FBO Incomplete draw buffer" << std::endl;
+        break;
+    case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
+        OSG_WARN << "FBO Incomplete read buffer" << std::endl;
+        break;
+    case GL_FRAMEBUFFER_UNSUPPORTED_EXT:
+        OSG_WARN << "FBO Incomplete unsupported" << std::endl;
+        break;
+    case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE_EXT:
+        OSG_WARN << "FBO Incomplete multisample" << std::endl;
+        break;
+    default:
+        OSG_WARN << "FBO Incomplete ??? (0x" << std::hex << complete << std::dec << ")" << std::endl;
+        break;
+    }
+    return false;
+}
+
+void XRFramebuffer::bind(osg::State &state)
+{
+    const OSG_GLExtensions *fbo_ext = getGLExtensions(state);
+
+    if (!_fbo && !_generated)
+    {
+        fbo_ext->glGenFramebuffers(1, &_fbo);
+        _generated = true;
+    }
+
+    if (_fbo)
+    {
+        fbo_ext->glBindFramebuffer(GL_FRAMEBUFFER_EXT, _fbo);
+        if (!_boundTexture && _texture)
+        {
+            fbo_ext->glFramebufferTexture2D(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, _texture, 0);
+            _boundTexture = true;
+        }
+        if (!_boundDepthTexture)
+        {
+            if (!_depthTexture)
+            {
+                glGenTextures(1, &_depthTexture);
+                glBindTexture(GL_TEXTURE_2D, _depthTexture);
+                glTexImage2D(GL_TEXTURE_2D, 0, _depthFormat, _width, _height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, nullptr);
+                glBindTexture(GL_TEXTURE_2D, 0);
+
+                _deleteDepthTexture = true;
+            }
+
+            fbo_ext->glFramebufferTexture2D(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, _depthTexture, 0);
+            _boundDepthTexture = true;
+
+            valid(state);
+        }
+    }
+}
+
+void XRFramebuffer::unbind(osg::State &state)
+{
+    const OSG_GLExtensions *fbo_ext = getGLExtensions(state);
+
+    if (_fbo && _generated)
+        fbo_ext->glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0);
+}
+
+void XRFramebuffer::releaseGLObjects(osg::State &state)
+{
+    // FIXME can we do it like RenderBuffer::releaseGLObjects?
+    // FIXME better yet, switch to use FrameBufferObject, dynamically bound
+
+    // GL context must be current
+    if (_fbo)
+    {
+        /*
+        unsigned int contextID = state->getContextID();
+        osg::get<GLFrameBufferObjectManager>(contextID)->scheduleGLObjectForDeletion(_fbo);
+        */
+        const OSG_GLExtensions *fbo_ext = getGLExtensions(state);
+        fbo_ext->glDeleteFramebuffers(1, &_fbo);
+        _fbo = 0;
+    }
+    if (_deleteDepthTexture)
+    {
+        glDeleteTextures(1, &_depthTexture);
+        _depthTexture = 0;
+        _deleteDepthTexture = false;
+    }
+}
diff --git a/3rdparty/osgXR/src/XRFramebuffer.h b/3rdparty/osgXR/src/XRFramebuffer.h
new file mode 100644
index 000000000..6f04a93d1
--- /dev/null
+++ b/3rdparty/osgXR/src/XRFramebuffer.h
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_XRFRAMEBUFFER
+#define OSGXR_XRFRAMEBUFFER 1
+
+#include <osg/GL>
+#include <osg/Referenced>
+
+#include <cinttypes>
+
+namespace osgXR {
+
+class XRFramebuffer : public osg::Referenced
+{
+    public:
+
+        explicit XRFramebuffer(uint32_t width, uint32_t height,
+                               GLuint texture, GLuint depthTexture = 0);
+        // releaseGLObjects() first
+        virtual ~XRFramebuffer();
+
+        void setDepthFormat(GLenum depthFormat)
+        {
+            _depthFormat = depthFormat;
+        }
+
+        bool valid(osg::State &state) const;
+        void bind(osg::State &state);
+        void unbind(osg::State &state);
+        // GL context must be current
+        void releaseGLObjects(osg::State &state);
+
+    protected:
+
+        uint32_t _width;
+        uint32_t _height;
+        GLenum _depthFormat;
+
+        GLuint _fbo;
+        GLuint _texture;
+        GLuint _depthTexture;
+
+        bool _generated;
+        bool _boundTexture;
+        bool _boundDepthTexture;
+        bool _deleteDepthTexture;
+};
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/XRRealizeOperation.cpp b/3rdparty/osgXR/src/XRRealizeOperation.cpp
new file mode 100644
index 000000000..ae12d9477
--- /dev/null
+++ b/3rdparty/osgXR/src/XRRealizeOperation.cpp
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "XRRealizeOperation.h"
+
+#include "XRState.h"
+
+#include <osgViewer/GraphicsWindow>
+
+using namespace osgXR;
+
+XRRealizeOperation::XRRealizeOperation(osg::ref_ptr<XRState> state,
+                                       osgViewer::View *view) :
+    osg::GraphicsOperation("XRRealizeOperation", false),
+    _state(state),
+    _view(view),
+    _realized(false)
+{
+}
+
+void XRRealizeOperation::operator () (osg::GraphicsContext *gc)
+{
+    if (!_realized)
+    {
+        OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_mutex);
+        gc->makeCurrent();
+
+        auto *window = dynamic_cast<osgViewer::GraphicsWindow *>(gc);
+        if (window)
+        {
+            _state->init(window, _view);
+            _realized = true;
+        }
+    }
+}
diff --git a/3rdparty/osgXR/src/XRRealizeOperation.h b/3rdparty/osgXR/src/XRRealizeOperation.h
new file mode 100644
index 000000000..ec4954fa2
--- /dev/null
+++ b/3rdparty/osgXR/src/XRRealizeOperation.h
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_XRREALIZEOPERATION
+#define OSGXR_XRREALIZEOPERATION 1
+
+#include <osg/ref_ptr>
+#include <osgViewer/View>
+
+namespace osgXR {
+
+class XRState;
+
+class XRRealizeOperation : public osg::GraphicsOperation
+{
+    public:
+
+        explicit XRRealizeOperation(osg::ref_ptr<XRState> state,
+                                    osgViewer::View *view);
+
+        void operator () (osg::GraphicsContext *gc) override;
+
+        bool realized() const
+        {
+            return _realized;
+        }
+
+    protected:
+
+        OpenThreads::Mutex _mutex;
+        osg::ref_ptr<XRState> _state;
+        osgViewer::View *_view;
+        bool _realized;
+};
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/XRState.cpp b/3rdparty/osgXR/src/XRState.cpp
new file mode 100644
index 000000000..5beda9e5a
--- /dev/null
+++ b/3rdparty/osgXR/src/XRState.cpp
@@ -0,0 +1,1589 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include "XRState.h"
+#include "XRStateCallbacks.h"
+#include "ActionSet.h"
+#include "InteractionProfile.h"
+#include "Subaction.h"
+#include "projection.h"
+
+#include <osgXR/Manager>
+
+#include <osg/Camera>
+#include <osg/ColorMask>
+#include <osg/Depth>
+#include <osg/DisplaySettings>
+#include <osg/FrameBufferObject>
+#include <osg/Notify>
+#include <osg/MatrixTransform>
+#include <osg/RenderInfo>
+#include <osg/View>
+
+#include <osgUtil/SceneView>
+
+#include <osgViewer/GraphicsWindow>
+#include <osgViewer/Renderer>
+#include <osgViewer/View>
+
+#include <cassert>
+#include <climits>
+#include <cmath>
+#include <sstream>
+
+using namespace osgXR;
+
+XRState::XRState(Settings *settings, Manager *manager) :
+    _settings(settings),
+    _settingsCopy(*settings),
+    _manager(manager),
+    _visibilityMaskLeft(0),
+    _visibilityMaskRight(0),
+    _actionsUpdated(false),
+    _currentState(VRSTATE_DISABLED),
+    _downState(VRSTATE_MAX),
+    _upState(VRSTATE_DISABLED),
+    _upDelay(0),
+    _probing(false),
+    _stateChanged(false),
+    _probed(false),
+    _useDepthInfo(false),
+    _useVisibilityMask(false),
+    _formFactor(XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY),
+    _system(nullptr),
+    _chosenViewConfig(nullptr),
+    _chosenEnvBlendMode(XR_ENVIRONMENT_BLEND_MODE_MAX_ENUM),
+    _vrMode(VRMode::VRMODE_AUTOMATIC),
+    _swapchainMode(SwapchainMode::SWAPCHAIN_AUTOMATIC)
+{
+}
+
+XRState::XRSwapchain::XRSwapchain(XRState *state,
+                                  osg::ref_ptr<OpenXR::Session> session,
+                                  const OpenXR::System::ViewConfiguration::View &view,
+                                  int64_t chosenSwapchainFormat,
+                                  int64_t chosenDepthSwapchainFormat) :
+    OpenXR::SwapchainGroup(session, view,
+                           XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT,
+                           chosenSwapchainFormat,
+                           XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
+                           chosenDepthSwapchainFormat),
+    _state(state),
+    _numDrawPasses(0),
+    _drawPassesDone(0),
+    _imagesReady(false)
+{
+    if (valid())
+    {
+        // Create framebuffer objects for each image in swapchain
+        auto &textures = getImageTextures();
+        const ImageTextures *depthTextures = nullptr;
+        if (depthValid())
+        {
+            depthTextures = &getDepthImageTextures();
+            if (textures.size() != depthTextures->size())
+                OSG_WARN << "Depth swapchain image count mismatch, expected " << textures.size() << ", got " << depthTextures->size() << std::endl;
+        }
+
+        _imageFramebuffers.reserve(textures.size());
+        for (unsigned int i = 0; i < textures.size(); ++i)
+        {
+            GLuint texture = textures[i];
+            GLuint depthTexture = 0;
+            if (depthTextures)
+                depthTexture = (*depthTextures)[i];
+            XRFramebuffer *fb = new XRFramebuffer(getWidth(),
+                                                  getHeight(),
+                                                  texture, depthTexture);
+            _imageFramebuffers.push_back(fb);
+        }
+    }
+}
+
+XRState::XRSwapchain::~XRSwapchain()
+{
+    osg::State *state = _state->_window->getState();
+    // FIXME window has no state on shutdown...
+    if (!state)
+        return;
+    // Explicitly release FBOs etc
+    // GL context must be current
+    for (unsigned int i = 0; i < _imageFramebuffers.size(); ++i)
+        _imageFramebuffers[i]->releaseGLObjects(*state);
+}
+
+void XRState::XRSwapchain::setupImage(const osg::FrameStamp *stamp)
+{
+    auto opt_fbo = _imageFramebuffers[stamp];
+    bool firstPass = !opt_fbo.has_value();
+    int imageIndex;
+    if (firstPass)
+    {
+        // Acquire a swapchain image
+        imageIndex = acquireImages();
+        if (imageIndex < 0 || (unsigned int)imageIndex >= _imageFramebuffers.size())
+        {
+            OSG_WARN << "XRView::preDrawCallback(): Failure to acquire OpenXR swapchain image (got image index " << imageIndex << ")" << std::endl;
+            return;
+        }
+        _imageFramebuffers.setStamp(imageIndex, stamp);
+        opt_fbo.emplace(_imageFramebuffers[imageIndex]);
+        _drawPassesDone = 0;
+        // Images aren't ready until we've waited for them to be so
+        _imagesReady = false;
+    }
+}
+
+void XRState::XRSwapchain::preDrawCallback(osg::RenderInfo &renderInfo)
+{
+    const osg::FrameStamp *stamp = renderInfo.getState()->getFrameStamp();
+    setupImage(stamp);
+
+    auto opt_fbo = _imageFramebuffers[stamp];
+    if (!opt_fbo.has_value())
+        return;
+
+    const auto &fbo = opt_fbo.value();
+
+    // Bind the framebuffer
+    osg::State &state = *renderInfo.getState();
+    fbo->bind(state);
+
+    if (!_imagesReady)
+    {
+        // Wait for the image to be ready to render into
+        if (!waitImages(100e6 /* 100ms */))
+        {
+            OSG_WARN << "XRView::preDrawCallback(): Failure to wait for OpenXR swapchain image" << std::endl;
+
+            // Unclear what the best course of action is here...
+            fbo->unbind(state);
+            return;
+        }
+
+        _imagesReady = true;
+    }
+}
+
+void XRState::XRSwapchain::postDrawCallback(osg::RenderInfo &renderInfo)
+{
+    const osg::FrameStamp *stamp = renderInfo.getState()->getFrameStamp();
+    auto opt_fbo = _imageFramebuffers[stamp];
+    if (!opt_fbo.has_value())
+        return;
+    const auto &fbo = opt_fbo.value();
+
+    // Unbind the framebuffer
+    osg::State& state = *renderInfo.getState();
+    fbo->unbind(state);
+
+    if (++_drawPassesDone == _numDrawPasses && _imagesReady)
+    {
+        // Done rendering. release the swapchain image
+        releaseImages();
+
+        _imagesReady = false;
+    }
+}
+
+void XRState::XRSwapchain::endFrame()
+{
+    // Double check images are released
+    if (_imagesReady)
+    {
+        releaseImages();
+        _imagesReady = false;
+    }
+}
+
+osg::ref_ptr<osg::Texture2D> XRState::XRSwapchain::getOsgTexture(const osg::FrameStamp *stamp)
+{
+    int index = _imageFramebuffers.findStamp(stamp);
+    if (index < 0)
+        return nullptr;
+    return getSwapchain()->getImageOsgTexture(index);
+}
+
+XRState::XRView::XRView(XRState *state,
+                        uint32_t viewIndex,
+                        osg::ref_ptr<XRSwapchain> swapchain) :
+    _state(state),
+    _swapchainSubImage(swapchain),
+    _viewIndex(viewIndex)
+{
+}
+
+XRState::XRView::XRView(XRState *state,
+                        uint32_t viewIndex,
+                        osg::ref_ptr<XRSwapchain> swapchain,
+                        const OpenXR::System::ViewConfiguration::View::Viewport &viewport) :
+    _state(state),
+    _swapchainSubImage(swapchain, viewport),
+    _viewIndex(viewIndex)
+{
+}
+
+XRState::XRView::~XRView()
+{
+}
+
+void XRState::XRView::setupCamera(osg::ref_ptr<osg::Camera> camera)
+{
+    camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT);
+    // FIXME necessary I expect...
+    //camera->setRenderOrder(osg::Camera::PRE_RENDER, eye);
+    //camera->setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR);
+    camera->setAllowEventFocus(false);
+    camera->setReferenceFrame(osg::Camera::RELATIVE_RF);
+    //camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF);
+    //camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF_INHERIT_VIEWPOINT);
+    camera->setViewport(_swapchainSubImage.getX(),
+                        _swapchainSubImage.getY(),
+                        _swapchainSubImage.getWidth(),
+                        _swapchainSubImage.getHeight());
+
+    // Here we avoid doing anything regarding OSG camera RTT attachment.
+    // Ideally we would use automatic methods within OSG for handling RTT but in this
+    // case it seemed simpler to handle FBO creation and selection within this class.
+
+    // This initial draw callback is used to disable normal OSG camera setup which
+    // would undo our RTT FBO configuration.
+    camera->setInitialDrawCallback(new InitialDrawCallback(_state));
+
+    camera->setPreDrawCallback(new PreDrawCallback(getSwapchain()));
+    camera->setFinalDrawCallback(new PostDrawCallback(getSwapchain()));
+}
+
+void XRState::XRView::endFrame(OpenXR::Session::Frame *frame)
+{
+    // Double check images are released
+    getSwapchain()->endFrame();
+
+    // Add view info to projection layer for compositor
+    osg::ref_ptr<OpenXR::CompositionLayerProjection> proj = _state->getProjectionLayer();
+    if (proj != nullptr)
+    {
+        proj->addView(frame, _viewIndex, _swapchainSubImage,
+                      _state->_useDepthInfo ? &_state->_depthInfo : nullptr);
+    }
+    else
+    {
+        OSG_WARN << "No projection layer" << std::endl;
+    }
+}
+
+XRState::AppView::AppView(XRState *state,
+                          osgViewer::GraphicsWindow *window,
+                          osgViewer::View *osgView) :
+    View(window, osgView),
+    _valid(false),
+    _state(state)
+{
+}
+
+void XRState::AppView::init()
+{
+    // Notify app to create a new view
+    if (_state->_manager.valid())
+        _state->_manager->doCreateView(this);
+    _valid = true;
+}
+
+XRState::AppView::~AppView()
+{
+    destroy();
+}
+
+void XRState::AppView::destroy()
+{
+    // Notify app to destroy this view
+    if (_valid && _state->_manager.valid())
+        _state->_manager->doDestroyView(this);
+    _valid = false;
+}
+
+XRState::SlaveCamsAppView::SlaveCamsAppView(XRState *state,
+                                            uint32_t viewIndex,
+                                            osgViewer::GraphicsWindow *window,
+                                            osgViewer::View *osgView) :
+    AppView(state, window, osgView),
+    _viewIndex(viewIndex)
+{
+}
+
+void XRState::SlaveCamsAppView::addSlave(osg::Camera *slaveCamera)
+{
+    XRView *xrView = _state->_xrViews[_viewIndex];
+    xrView->setupCamera(slaveCamera);
+    xrView->getSwapchain()->incNumDrawPasses();
+
+    osg::ref_ptr<osg::MatrixTransform> visMaskTransform;
+    // Set up visibility mask for this slave camera
+    // We'll keep track of the transform in the slave callback so it can be
+    // positioned at the appropriate range
+    if (_state->needsVisibilityMask(slaveCamera))
+        _state->setupVisibilityMask(slaveCamera, _viewIndex, visMaskTransform);
+
+    osg::View::Slave *slave = _osgView->findSlaveForCamera(slaveCamera);
+    slave->_updateSlaveCallback = new SlaveCamsUpdateSlaveCallback(_viewIndex, _state, visMaskTransform.get());
+}
+
+void XRState::SlaveCamsAppView::removeSlave(osg::Camera *slaveCamera)
+{
+    XRView *xrView = _state->_xrViews[_viewIndex];
+    xrView->getSwapchain()->decNumDrawPasses();
+}
+
+XRState::SceneViewAppView::SceneViewAppView(XRState *state,
+                                            osgViewer::GraphicsWindow *window,
+                                            osgViewer::View *osgView) :
+    AppView(state, window, osgView)
+{
+}
+
+void XRState::SceneViewAppView::addSlave(osg::Camera *slaveCamera)
+{
+    _state->setupSceneViewCamera(slaveCamera);
+    _state->_xrViews[0]->getSwapchain()->incNumDrawPasses(2);
+
+    osg::ref_ptr<osg::MatrixTransform> visMaskTransform;
+    // Set up visibility masks for this slave camera
+    // We'll keep track of the transform in the slave callback so it can be
+    // positioned at the appropriate range
+    if (_state->needsVisibilityMask(slaveCamera))
+        _state->setupSceneViewVisibilityMasks(slaveCamera, visMaskTransform);
+
+    if (visMaskTransform.valid())
+    {
+        osg::View::Slave *slave = _osgView->findSlaveForCamera(slaveCamera);
+        slave->_updateSlaveCallback = new SceneViewUpdateSlaveCallback(_state, visMaskTransform.get());
+    }
+}
+
+void XRState::SceneViewAppView::removeSlave(osg::Camera *slaveCamera)
+{
+    _state->_xrViews[0]->getSwapchain()->decNumDrawPasses(2);
+}
+
+std::shared_ptr<Subaction::Private> XRState::getSubaction(const std::string &path)
+{
+    auto it = _subactions.find(path);
+    if (it != _subactions.end())
+    {
+        auto ret = (*it).second.lock();
+        if (ret)
+            return ret;
+    }
+
+    auto subaction = std::make_shared<Subaction::Private>(this, path);
+    _subactions[path] = subaction;
+    return subaction;
+}
+
+InteractionProfile *XRState::getCurrentInteractionProfile(const OpenXR::Path &subactionPath) const
+{
+    if (_session.valid())
+    {
+        // Find the path of the current profile
+        OpenXR::Path profilePath = _session->getCurrentInteractionProfile(subactionPath);
+        if (!profilePath.valid())
+            return nullptr;
+
+        // Compare against the paths of known interaction profiles
+        for (auto *profile: _interactionProfiles)
+            if (profile->getPath() == profilePath)
+                return profile->getPublic();
+    }
+    return nullptr;
+}
+
+const char *XRState::getStateString() const
+{
+    static const char *vrStateNames[VRSTATE_MAX] = {
+        "disabled",
+        "inactive",
+        "detected",
+        "session",
+        "actions",
+    };
+    static const char *sessionStateNames[] = {
+        "unknown",
+        "idle",
+        "starting",
+        "invisible",
+        "visible unfocused",
+        "focused",
+        "stopping",
+        "loss pending",
+        "ending"
+    };
+    static const char *vrStateChangeNames[VRSTATE_MAX + 1][VRSTATE_MAX] = {
+        {   // down = VRSTATE_DISABLED
+            "disabling",                // up = VRSTATE_DISABLED
+            "reinitialising",           // up = VRSTATE_INSTANCE
+            "reinitialising & probing", // up = VRSTATE_SYSTEM
+            "restarting session",       // up = VRSTATE_SESSION
+            "restarting"                // up = VRSTATE_ACTIONS
+        },
+        {   // down = VRSTATE_INSTANCE
+            nullptr,                    // up = VRSTATE_DISABLED
+            "deactivating",             // up = VRSTATE_INSTANCE
+            "reprobing",                // up = VRSTATE_SYSTEM
+            "reprobing session",        // up = VRSTATE_SESSION
+            "reprobing session"         // up = VRSTATE_ACTIONS
+        },
+        {   // down = VRSTATE_SYSTEM
+            nullptr,                    // up = VRSTATE_DISABLED
+            nullptr,                    // up = VRSTATE_INSTANCE
+            "ending session",           // up = VRSTATE_SYSTEM
+            "restarting session",       // up = VRSTATE_SESSION
+            "restarting"                // up = VRSTATE_ACTIONS
+        },
+        {   // down = VRSTATE_SESSION
+            nullptr,                    // up = VRSTATE_DISABLED
+            nullptr,                    // up = VRSTATE_INSTANCE
+            nullptr,                    // up = VRSTATE_SYSTEM
+            nullptr,                    // up = VRSTATE_SESSION
+            "attaching actions"         // up = VRSTATE_ACTIONS
+        },
+        {   // down = VRSTATE_ACTIONS
+            nullptr,                    // up = VRSTATE_DISABLED
+            nullptr,                    // up = VRSTATE_INSTANCE
+            nullptr,                    // up = VRSTATE_SYSTEM
+            nullptr,                    // up = VRSTATE_SESSION
+            nullptr,                    // up = VRSTATE_ACTIONS
+        },
+        {   // down = VRSTATE_MAX
+            nullptr,                    // up = VRSTATE_DISABLED
+            "initialising",             // up = VRSTATE_INSTANCE
+            "probing",                  // up = VRSTATE_SYSTEM
+            "starting session",         // up = VRSTATE_SESSION
+            "attaching actions"         // up = VRSTATE_ACTIONS
+        },
+    };
+
+    std::string out = vrStateNames[_currentState];
+    if (_currentState >= VRSTATE_SESSION)
+    {
+        out += " ";
+        out += sessionStateNames[_session->getState()];
+    }
+    if (isStateUpdateNeeded())
+    {
+        const char *str = vrStateChangeNames[_downState][_upState];
+        if (str)
+        {
+            out += " (";
+            out += str;
+            out += ")";
+        }
+    }
+
+    _stateString = out;
+    return _stateString.c_str();
+}
+
+bool XRState::hasValidationLayer() const
+{
+    if (!_probed)
+        probe();
+    return _hasValidationLayer;
+}
+
+bool XRState::hasDepthInfoExtension() const
+{
+    if (!_probed)
+        probe();
+    return _hasDepthInfoExtension;
+}
+
+bool XRState::hasVisibilityMaskExtension() const
+{
+    if (!_probed)
+        probe();
+    return _hasVisibilityMaskExtension;
+}
+
+void XRState::syncSettings()
+{
+    unsigned int diff = _settingsCopy._diff(*_settings.get());
+    if (diff & (Settings::DIFF_APP_INFO |
+                Settings::DIFF_VALIDATION_LAYER))
+        // Recreate instance
+        setDownState(VRSTATE_DISABLED);
+    else if (diff & (Settings::DIFF_FORM_FACTOR |
+                     Settings::DIFF_BLEND_MODE))
+        // Reread system
+        setDownState(VRSTATE_INSTANCE);
+    else if (diff & (Settings::DIFF_DEPTH_INFO |
+                     Settings::DIFF_VISIBILITY_MASK |
+                     Settings::DIFF_VR_MODE |
+                     Settings::DIFF_SWAPCHAIN_MODE))
+        // Recreate session
+        setDownState(VRSTATE_SYSTEM);
+}
+
+bool XRState::getActionsUpdated() const
+{
+    // Have action sets or interaction profiles been added or removed?
+    if (_actionsUpdated)
+        return true;
+
+    // Have action sets or their actions been altered?
+    for (auto *actionSet: _actionSets)
+        if (actionSet->getUpdated())
+            return true;
+
+    // Have interaction profile bindings been altered?
+    for (auto *interactionProfile: _interactionProfiles)
+        if (interactionProfile->getUpdated())
+            return true;
+
+    return false;
+}
+
+void XRState::syncActionSetup()
+{
+    // Nothing is required if actions haven't been attached yet
+    if (_currentState < VRSTATE_ACTIONS)
+        return;
+
+    // Restart session if actions have been updated
+    if (getActionsUpdated())
+        setDownState(VRSTATE_SYSTEM);
+}
+
+bool XRState::checkAndResetStateChanged()
+{
+    bool ret = _stateChanged;
+    _stateChanged = false;
+    return ret;
+}
+
+void XRState::update()
+{
+    assert(_manager.valid());
+
+    typedef UpResult (XRState::*UpHandler)();
+    static UpHandler upStateHandlers[VRSTATE_MAX - 1] = {
+        &XRState::upInstance,
+        &XRState::upSystem,
+        &XRState::upSession,
+        &XRState::upActions,
+    };
+    typedef DownResult (XRState::*DownHandler)();
+    static DownHandler downStateHandlers[VRSTATE_MAX - 1] = {
+        &XRState::downInstance,
+        &XRState::downSystem,
+        &XRState::downSession,
+        &XRState::downActions,
+    };
+
+    bool pollNeeded = true;
+    for (;;)
+    {
+        // Poll first
+        if (pollNeeded && _instance.valid() && _instance->valid())
+        {
+            // Poll for events
+            _instance->pollEvents(this);
+
+            // Sync actions
+            if (_session.valid())
+                _session->syncActions();
+
+            // Check for instance lost
+            if (_instance->lost())
+                setDownState(VRSTATE_DISABLED);
+
+            pollNeeded = false;
+        }
+        // Then down transitions
+        else if (_downState < _currentState)
+        {
+            DownResult res = (this->*downStateHandlers[_currentState-1])();
+            if (res == DOWN_SUCCESS)
+            {
+                _currentState = (VRState)((int)_currentState - 1);
+                if (_currentState == _downState)
+                    _downState = VRSTATE_MAX;
+                _stateChanged = true;
+            }
+            else // DOWN_SOON
+            {
+                break;
+            }
+        }
+        // Then up transitions
+        else if (_upState > _currentState)
+        {
+            if (_upDelay > 0)
+            {
+                // try again soon
+                --_upDelay;
+                break;
+            }
+            UpResult res = (this->*upStateHandlers[_currentState])();
+            if (res == UP_SUCCESS)
+            {
+                if (_currentState <= _downState)
+                    _downState = VRSTATE_MAX;
+                _currentState = (VRState)((int)_currentState + 1);
+                // Poll events again after bringing up session
+                if (_currentState >= VRSTATE_SESSION)
+                    pollNeeded = true;
+                _stateChanged = true;
+            }
+            else if (res == UP_ABORT)
+            {
+                VRState probingState = getProbingState();
+                if (probingState < _currentState)
+                    // Drop down to probing state
+                    setDestState(getProbingState());
+                else
+                    // Go up no further
+                    _upState = _currentState;
+                _stateChanged = true;
+            }
+            else // UP_SOON or UP_LATER
+            {
+                if (res == UP_LATER)
+                {
+                    // Don't poll incessantly
+                    _upDelay = 100;
+                }
+                break;
+            }
+        }
+        else
+        {
+            _upDelay = 0;
+            break;
+        }
+    }
+
+    // Restart threading in case we had to disable it to prevent the GL context
+    // being bound in another thread during certain OpenXR calls.
+    if (_viewer.valid())
+        _viewer->startThreading();
+}
+
+void XRState::onInstanceLossPending(OpenXR::Instance *instance,
+                                    const XrEventDataInstanceLossPending *event)
+{
+    // Reinitialize instance
+    setDownState(VRSTATE_DISABLED);
+    // FIXME use event.lossTime?
+    _upDelay = 100;
+}
+
+void XRState::onInteractionProfileChanged(OpenXR::Session *session,
+                                          const XrEventDataInteractionProfileChanged *event)
+{
+    // notify subactions so they can invalidate their cached current profile
+    for (auto &pair: _subactions)
+    {
+        auto subaction = pair.second.lock();
+        if (subaction)
+            subaction->onInteractionProfileChanged(session);
+    }
+}
+
+void XRState::onSessionStateChanged(OpenXR::Session *session,
+                                    const XrEventDataSessionStateChanged *event)
+{
+    OpenXR::EventHandler::onSessionStateChanged(session, event);
+    _stateChanged = true;
+}
+
+void XRState::onSessionStateStart(OpenXR::Session *session)
+{
+}
+
+void XRState::onSessionStateEnd(OpenXR::Session *session, bool retry)
+{
+    if (!session->isExiting())
+    {
+        // If the exit wasn't requested, drop back to a safe state
+        if (retry)
+            setDownState(VRSTATE_INSTANCE);
+        else
+            setDestState(getProbingState());
+    }
+}
+
+void XRState::onSessionStateReady(OpenXR::Session *session)
+{
+    assert(session == _session);
+    if (!session->begin(*_chosenViewConfig))
+    {
+        // This should normally have succeeded
+        setDestState(getProbingState());
+        return;
+    }
+
+    // Set up cameras
+    switch (_vrMode)
+    {
+        case VRMode::VRMODE_SLAVE_CAMERAS:
+            setupSlaveCameras();
+            break;
+
+        case VRMode::VRMODE_AUTOMATIC:
+            // Should already have been handled by upSession()
+        case VRMode::VRMODE_SCENE_VIEW:
+            setupSceneViewCameras();
+            break;
+    }
+
+    // Attach a callback to detect swap
+    osg::ref_ptr<osg::GraphicsContext> gc = _window.get();
+    osg::ref_ptr<SwapCallback> swapCallback = new SwapCallback(this);
+    gc->setSwapCallback(swapCallback);
+
+    // Finally set up any mirrors that may be queued in the manager
+    if (_manager.valid())
+    {
+        // FIXME consider
+        _manager->_setupMirrors();
+        _manager->onRunning();
+    }
+}
+
+void XRState::onSessionStateStopping(OpenXR::Session *session, bool loss)
+{
+    // check no frame in progress
+
+    // clean up appViews
+    for (auto appView: _appViews)
+        appView->destroy();
+    _appViews.resize(0);
+
+    osg::ref_ptr<osg::GraphicsContext> gc = _window.get();
+    gc->setSwapCallback(nullptr);
+
+    if (!loss)
+        session->end();
+
+    if (_manager.valid())
+        _manager->onStopped();
+}
+
+void XRState::onSessionStateFocus(OpenXR::Session *session)
+{
+    if (_manager.valid())
+        _manager->onFocus();
+}
+
+void XRState::onSessionStateUnfocus(OpenXR::Session *session)
+{
+    if (_manager.valid())
+        _manager->onUnfocus();
+}
+
+void XRState::probe() const
+{
+    _hasValidationLayer = OpenXR::Instance::hasLayer(XR_APILAYER_LUNARG_core_validation);
+    _hasDepthInfoExtension = OpenXR::Instance::hasExtension(XR_KHR_COMPOSITION_LAYER_DEPTH_EXTENSION_NAME);
+    _hasVisibilityMaskExtension = OpenXR::Instance::hasExtension(XR_KHR_VISIBILITY_MASK_EXTENSION_NAME);
+
+    _probed = true;
+}
+
+void XRState::unprobe() const
+{
+    OpenXR::Instance::invalidateLayers();
+    OpenXR::Instance::invalidateExtensions();
+
+    _probed = false;
+}
+
+XRState::UpResult XRState::upInstance()
+{
+    assert(!_instance.valid());
+
+    // Create OpenXR instance
+
+    // Update needed settings that may have changed
+    _settingsCopy.setApp(_settings->getAppName(), _settings->getAppVersion());
+    _settingsCopy.setValidationLayer(_settings->getValidationLayer());
+
+    _instance = new OpenXR::Instance();
+    _instance->setValidationLayer(_settingsCopy.getValidationLayer());
+    _instance->setDepthInfo(true);
+    _instance->setVisibilityMask(true);
+    switch (_instance->init(_settingsCopy.getAppName().c_str(),
+                            _settingsCopy.getAppVersion()))
+    {
+    case OpenXR::Instance::INIT_SUCCESS:
+        break;
+    case OpenXR::Instance::INIT_LATER:
+        _instance = nullptr;
+        return UP_LATER;
+    case OpenXR::Instance::INIT_FAIL:
+        _instance = nullptr;
+        return UP_ABORT;
+    }
+
+    return UP_SUCCESS;
+}
+
+XRState::DownResult XRState::downInstance()
+{
+    assert(_instance.valid());
+
+    // This should destroy actions and action sets
+    for (auto *profile: _interactionProfiles)
+        profile->cleanupInstance();
+    for (auto *actionSet: _actionSets)
+        actionSet->cleanupInstance();
+
+    for (auto &pair: _subactions)
+    {
+        auto subaction = pair.second.lock();
+        if (subaction)
+            subaction->cleanupInstance();
+    }
+
+    if (_probed)
+        unprobe();
+
+    osg::observer_ptr<OpenXR::Instance> oldInstance = _instance;
+    _instance = nullptr;
+    assert(!oldInstance.valid());
+
+    return DOWN_SUCCESS;
+}
+
+XRState::UpResult XRState::upSystem()
+{
+    assert(!_system);
+
+    // Update needed settings that may have changed
+    _settingsCopy.setFormFactor(_settings->getFormFactor());
+    _settingsCopy.setPreferredEnvBlendModeMask(_settings->getPreferredEnvBlendModeMask());
+    _settingsCopy.setAllowedEnvBlendModeMask(_settings->getAllowedEnvBlendModeMask());
+
+    // Get OpenXR system for chosen form factor
+
+    switch (_settingsCopy.getFormFactor())
+    {
+        case Settings::HEAD_MOUNTED_DISPLAY:
+            _formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
+            break;
+        case Settings::HANDHELD_DISPLAY:
+            _formFactor = XR_FORM_FACTOR_HANDHELD_DISPLAY;
+            break;
+    }
+    bool supported;
+    _system = _instance->getSystem(_formFactor, &supported);
+    if (!_system)
+        return supported ? UP_LATER : UP_ABORT;
+
+    // Choose the first supported view configuration
+
+    for (const auto &viewConfig: _system->getViewConfigurations())
+    {
+        switch (viewConfig.getType())
+        {
+            case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MONO:
+            case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO:
+                _chosenViewConfig = &viewConfig;
+                break;
+            default:
+                break;
+        }
+        if (_chosenViewConfig)
+            break;
+    }
+    if (!_chosenViewConfig)
+    {
+        OSG_WARN << "XRState::XRState(): No supported view configuration" << std::endl;
+        _system = nullptr;
+        return UP_ABORT;
+    }
+
+    // Choose an environment blend mode
+
+    for (XrEnvironmentBlendMode envBlendMode: _chosenViewConfig->getEnvBlendModes())
+    {
+        if ((unsigned int)envBlendMode > 31)
+            continue;
+        uint32_t mask = (1u << (unsigned int)envBlendMode);
+        if (_settingsCopy.getPreferredEnvBlendModeMask() & mask)
+        {
+            _chosenEnvBlendMode = envBlendMode;
+            break;
+        }
+        if (_chosenEnvBlendMode != XR_ENVIRONMENT_BLEND_MODE_MAX_ENUM &&
+            _settingsCopy.getAllowedEnvBlendModeMask() & mask)
+        {
+            _chosenEnvBlendMode = envBlendMode;
+        }
+    }
+    if (_chosenEnvBlendMode == XR_ENVIRONMENT_BLEND_MODE_MAX_ENUM)
+    {
+        OSG_WARN << "XRState::XRState(): No supported environment blend mode" << std::endl;
+        _system = nullptr;
+        return UP_ABORT;
+    }
+
+    return UP_SUCCESS;
+}
+
+XRState::DownResult XRState::downSystem()
+{
+    _system = nullptr;
+    _instance->invalidateSystem(_formFactor);
+    return DOWN_SUCCESS;
+}
+
+XRState::UpResult XRState::upSession()
+{
+    assert(_system);
+    assert(!_session.valid());
+
+    if (!_window.valid() || !_view.valid())
+        // Maybe window & view haven't been initialized yet
+        return UP_SOON;
+
+    // Update needed settings that may have changed
+    _settingsCopy.setDepthInfo(_settings->getDepthInfo());
+    _settingsCopy.setVisibilityMask(_settings->getVisibilityMask());
+    _settingsCopy.setVRMode(_settings->getVRMode());
+    _settingsCopy.setSwapchainMode(_settings->getSwapchainMode());
+    _useDepthInfo = _settingsCopy.getDepthInfo();
+    _useVisibilityMask = _settingsCopy.getVisibilityMask();
+    _vrMode = _settingsCopy.getVRMode();
+    _swapchainMode = _settingsCopy.getSwapchainMode();
+
+    if (_useDepthInfo && !_instance->supportsCompositionLayerDepth())
+    {
+        OSG_WARN << "osgXR: CompositionLayerDepth extension not supported, depth info will be disabled" << std::endl;
+        _useDepthInfo = false;
+    }
+    if (_useVisibilityMask && !_instance->supportsVisibilityMask())
+    {
+        OSG_WARN << "osgXR: VisibilityMask extension not supported, visibility masking will be disabled" << std::endl;
+        _useVisibilityMask = false;
+    }
+
+    // Decide on the algorithm to use. SceneView mode is faster.
+    if (_vrMode == VRMode::VRMODE_AUTOMATIC)
+        _vrMode = VRMode::VRMODE_SCENE_VIEW;
+
+    // SceneView mode only works with a stereo view config
+    if (_vrMode == VRMode::VRMODE_SCENE_VIEW &&
+        _chosenViewConfig->getType() != XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO)
+    {
+        _vrMode = VRMode::VRMODE_SLAVE_CAMERAS;
+        if (_settingsCopy.getVRMode() == VRMode::VRMODE_SCENE_VIEW)
+            OSG_WARN << "osgXR: No stereo view config for VR mode SCENE_VIEW, falling back to SLAVE_CAMERAS" << std::endl;
+    }
+
+    // SceneView mode requires a single swapchain
+    if (_vrMode == VRMode::VRMODE_SCENE_VIEW)
+    {
+        if (_swapchainMode != SwapchainMode::SWAPCHAIN_AUTOMATIC &&
+            _swapchainMode != SwapchainMode::SWAPCHAIN_SINGLE)
+        {
+            OSG_WARN << "osgXR: Overriding VR swapchain mode to SINGLE for VR mode SCENE_VIEW" << std::endl;
+        }
+        _swapchainMode = SwapchainMode::SWAPCHAIN_SINGLE;
+    }
+
+    // Decide on a swapchain mode to use
+    if (_swapchainMode == SwapchainMode::SWAPCHAIN_AUTOMATIC)
+        _swapchainMode = SwapchainMode::SWAPCHAIN_MULTIPLE;
+
+    // Stop threading to prevent the GL context being bound in another thread
+    // during certain OpenXR calls (session & swapchain handling).
+    if (_viewer.valid())
+        _viewer->stopThreading();
+
+    // Create session using the GraphicsWindow
+    _session = new OpenXR::Session(_system, _window.get());
+    if (!_session)
+    {
+        OSG_WARN << "XRState::init(): No suitable GraphicsWindow to create an OpenXR session" << std::endl;
+        return UP_ABORT;
+    }
+
+    // Decide on ideal depth bits
+    unsigned int bestDepthBits = 24;
+    auto *traits = _window->getTraits();
+    if (traits)
+        bestDepthBits = traits->depth;
+
+    // Choose a swapchain format
+    int64_t chosenSwapchainFormat = 0;
+    int64_t chosenDepthSwapchainFormat = 0;
+    unsigned int chosenDepthBits = 0;
+    for (int64_t format: _session->getSwapchainFormats())
+    {
+        unsigned int thisDepthBits = 0;
+        switch (format)
+        {
+            case GL_RGBA16:
+            case GL_RGB10_A2:
+            case GL_RGBA8:
+                // Choose the first supported format suggested by the runtime
+                if (!chosenSwapchainFormat)
+                    chosenSwapchainFormat = format;
+                break;
+            case GL_DEPTH_COMPONENT16:
+                thisDepthBits = 16;
+                goto handle_depth;
+            case GL_DEPTH_COMPONENT24:
+                thisDepthBits = 24;
+                goto handle_depth;
+            case GL_DEPTH_COMPONENT32:
+                thisDepthBits = 32;
+                // fall through
+            handle_depth:
+                if (_useDepthInfo)
+                {
+                    if (// Anything is better than nothing
+                        !chosenDepthSwapchainFormat ||
+                        // A higher number of bits is better than not enough
+                        (thisDepthBits > chosenDepthBits && chosenDepthBits < bestDepthBits) ||
+                        // A lower number of bits may still be enough
+                        (bestDepthBits < thisDepthBits && thisDepthBits < chosenDepthBits))
+                    {
+                        chosenDepthSwapchainFormat = format;
+                        chosenDepthBits = thisDepthBits;
+                    }
+                }
+                break;
+            default:
+                break;
+        }
+    }
+    if (!chosenSwapchainFormat)
+    {
+        std::stringstream formats;
+        formats << std::hex;
+        for (int64_t format: _session->getSwapchainFormats())
+            formats << " 0x" << format;
+        OSG_WARN << "XRState::init(): No supported swapchain format found in ["
+                 << formats.str() << " ]" << std::endl;
+        _session = nullptr;
+        return UP_ABORT;
+    }
+    if (_useDepthInfo && !chosenDepthSwapchainFormat)
+    {
+        std::stringstream formats;
+        formats << std::hex;
+        for (int64_t format: _session->getSwapchainFormats())
+            formats << " 0x" << format;
+        OSG_WARN << "XRState::init(): No supported depth swapchain format found in ["
+                 << formats.str() << " ]" << std::endl;
+        _useDepthInfo = false;
+    }
+
+    // Set up swapchains & viewports
+    switch (_swapchainMode)
+    {
+        case SwapchainMode::SWAPCHAIN_SINGLE:
+            if (!setupSingleSwapchain(chosenSwapchainFormat,
+                                      chosenDepthSwapchainFormat))
+            {
+                _session = nullptr;
+                return UP_ABORT;
+            }
+            break;
+
+        case SwapchainMode::SWAPCHAIN_AUTOMATIC:
+            // Should already have been handled by upSession()
+        case SwapchainMode::SWAPCHAIN_MULTIPLE:
+            if (!setupMultipleSwapchains(chosenSwapchainFormat,
+                                         chosenDepthSwapchainFormat))
+            {
+                _session = nullptr;
+                return UP_ABORT;
+            }
+            break;
+    }
+
+    return UP_SUCCESS;
+}
+
+XRState::DownResult XRState::downSession()
+{
+    assert(_session.valid());
+
+    if (_session->isRunning())
+    {
+        if (!_session->isExiting())
+            _session->requestExit();
+        return DOWN_SOON;
+    }
+
+    // no frames should be in progress
+
+    // Stop threading to prevent the GL context being bound in another thread
+    // during certain OpenXR calls (session & swapchain destruction).
+    if (_viewer.valid())
+        _viewer->stopThreading();
+
+    // Ensure the GL context is active for destruction of FBOs in XRFramebuffer
+    _session->makeCurrent();
+    _xrViews.resize(0);
+    _session->releaseContext();
+
+    // this will destroy the session
+    for (auto *actionSet: _actionSets)
+        actionSet->cleanupSession();
+    for (auto &pair: _subactions)
+    {
+        auto subaction = pair.second.lock();
+        if (subaction)
+            subaction->cleanupSession();
+    }
+    osg::observer_ptr<OpenXR::Session> oldSession = _session;
+    _session = nullptr;
+    assert(!oldSession.valid());
+
+    return DOWN_SUCCESS;
+}
+
+XRState::UpResult XRState::upActions()
+{
+    // Wait until the app has set up action sets and interaction profiles
+    if (_actionSets.empty() || _interactionProfiles.empty())
+        return UP_SOON;
+
+    // Set up anything needed for interaction profiles
+    for (auto *profile: _interactionProfiles)
+        profile->setup(_instance);
+
+    // Attach action sets to the session
+    for (auto *actionSet: _actionSets)
+        actionSet->setup(_session);
+    if (_session->attachActionSets())
+        _actionsUpdated = false;
+    // Treat attach fail as success, as VR can still continue without input
+    return UP_SUCCESS;
+}
+
+XRState::DownResult XRState::downActions()
+{
+    // Action setup cannot be undone
+    return DOWN_SUCCESS;
+}
+
+bool XRState::setupSingleSwapchain(int64_t format, int64_t depthFormat)
+{
+    const auto &views = _chosenViewConfig->getViews();
+    _xrViews.reserve(views.size());
+
+    // Arrange viewports on a single swapchain image
+    OpenXR::System::ViewConfiguration::View singleView(0, 0);
+    std::vector<OpenXR::System::ViewConfiguration::View::Viewport> viewports;
+    viewports.resize(views.size());
+    for (uint32_t i = 0; i < views.size(); ++i)
+        viewports[i] = singleView.tileHorizontally(views[i]);
+
+    // Create a single swapchain
+    osg::ref_ptr<XRSwapchain> xrSwapchain = new XRSwapchain(this, _session,
+                                                            singleView, format,
+                                                            depthFormat);
+    // And the views
+    _xrViews.reserve(views.size());
+    for (uint32_t i = 0; i < views.size(); ++i)
+    {
+        osg::ref_ptr<XRView> xrView = new XRView(this, i, xrSwapchain,
+                                                 viewports[i]);
+        if (!xrView.valid())
+        {
+            _xrViews.resize(0);
+            return false; // failure
+        }
+        _xrViews.push_back(xrView);
+    }
+
+    return true;
+}
+
+bool XRState::setupMultipleSwapchains(int64_t format, int64_t depthFormat)
+{
+    const auto &views = _chosenViewConfig->getViews();
+    _xrViews.reserve(views.size());
+
+    for (uint32_t i = 0; i < views.size(); ++i)
+    {
+        const auto &vcView = views[i];
+        osg::ref_ptr<XRSwapchain> xrSwapchain = new XRSwapchain(this, _session,
+                                                                vcView, format,
+                                                                depthFormat);
+        osg::ref_ptr<XRView> xrView = new XRView(this, i, xrSwapchain);
+        if (!xrView.valid())
+        {
+            _xrViews.resize(0);
+            return false; // failure
+        }
+        _xrViews.push_back(xrView);
+    }
+
+    return true;
+}
+
+void XRState::setupSlaveCameras()
+{
+    osg::ref_ptr<osg::GraphicsContext> gc = _window.get();
+    osg::Camera *camera = _view.valid() ? _view->getCamera() : nullptr;
+    //camera->setName("Main");
+
+    _appViews.resize(_xrViews.size());
+    for (uint32_t i = 0; i < _xrViews.size(); ++i)
+    {
+        SlaveCamsAppView *appView = new SlaveCamsAppView(this, i, _window.get(),
+                                                         _view.get());
+        appView->init();
+        _appViews[i] = appView;
+
+        if (camera && !_manager.valid())
+        {
+            // The app isn't using a manager class, so create the new slave
+            // camera ourselves
+            osg::ref_ptr<osg::Camera> cam = new osg::Camera();
+            cam->setClearColor(camera->getClearColor());
+            cam->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+            cam->setGraphicsContext(gc);
+
+            // Add as a slave to the OSG view
+            if (!_view->addSlave(cam.get(), osg::Matrix::identity(),
+                                 osg::Matrix::identity(), true))
+            {
+                OSG_WARN << "XRState::init(): Couldn't add slave camera" << std::endl;
+                continue;
+            }
+
+            // And ensure it gets configured for VR
+            appView->addSlave(cam.get());
+        }
+    }
+
+    if (camera && !_manager.valid())
+    {
+        // Disable rendering of main camera since its being overwritten by the swap texture anyway
+        camera->setGraphicsContext(nullptr);
+    }
+}
+
+void XRState::setupSceneViewCameras()
+{
+    _stereoDisplaySettings = new osg::DisplaySettings(*osg::DisplaySettings::instance().get());
+    _stereoDisplaySettings->setStereo(true);
+    _stereoDisplaySettings->setStereoMode(osg::DisplaySettings::HORIZONTAL_SPLIT);
+    _stereoDisplaySettings->setSplitStereoHorizontalEyeMapping(osg::DisplaySettings::LEFT_EYE_LEFT_VIEWPORT);
+    _stereoDisplaySettings->setUseSceneViewForStereoHint(true);
+
+    _appViews.resize(1);
+    SceneViewAppView *appView = new SceneViewAppView(this, _window.get(),
+                                                     _view.get());
+    appView->init();
+    _appViews[0] = appView;
+
+    if (_view.valid() && !_manager.valid())
+    {
+        // If the main camera is for rendering, set up that
+        osg::ref_ptr<osg::Camera> camera = _view->getCamera();
+        if (camera->getGraphicsContext() != nullptr)
+        {
+            _appViews[0]->addSlave(camera);
+        }
+        else
+        {
+            // Otherwise, we'll have to go and poke about in the slave cameras
+            unsigned int numSlaves = _view->getNumSlaves();
+            for (unsigned int i = 0; i < numSlaves; ++i)
+            {
+                osg::ref_ptr<osg::Camera> slaveCam = _view->getSlave(i)._camera;
+                if (slaveCam->getRenderTargetImplementation() == osg::Camera::FRAME_BUFFER)
+                {
+                    OSG_WARN << "XRState::setupSceneViewCameras(): slave " << slaveCam->getName() << std::endl;
+                    _appViews[0]->addSlave(slaveCam);
+                }
+            }
+
+            if (!_xrViews[0]->getSwapchain()->getNumDrawPasses())
+            {
+                OSG_WARN << "XRState::setupSceneViewCameras(): Failed to find suitable slave camera" << std::endl;
+                return;
+            }
+        }
+    }
+}
+
+void XRState::setupSceneViewCamera(osg::Camera *camera)
+{
+    camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT);
+    camera->setDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);
+    camera->setReadBuffer(GL_COLOR_ATTACHMENT0_EXT);
+
+    // Here we avoid doing anything regarding OSG camera RTT attachment.
+    // Ideally we would use automatic methods within OSG for handling RTT but in this
+    // case it seemed simpler to handle FBO creation and selection within this class.
+
+    // This initial draw callback is used to disable normal OSG camera setup which
+    // would undo our RTT FBO configuration.
+    camera->setInitialDrawCallback(new InitialDrawCallback(this));
+
+    camera->setPreDrawCallback(new PreDrawCallback(_xrViews[0]->getSwapchain()));
+    camera->setFinalDrawCallback(new PostDrawCallback(_xrViews[0]->getSwapchain()));
+
+    // Set the viewport (seems to need redoing!)
+    camera->setViewport(0, 0,
+                        _xrViews[0]->getSwapchain()->getWidth(),
+                        _xrViews[0]->getSwapchain()->getHeight());
+
+    // Set the stereo matrices callback on each SceneView
+    osgViewer::Renderer *renderer = static_cast<osgViewer::Renderer *>(camera->getRenderer());
+    for (unsigned int i = 0; i < 2; ++i)
+    {
+        osgUtil::SceneView *sceneView = renderer->getSceneView(i);
+        sceneView->setComputeStereoMatricesCallback(
+            new ComputeStereoMatricesCallback(this, sceneView));
+    }
+
+    camera->setDisplaySettings(_stereoDisplaySettings);
+}
+
+void XRState::setupSceneViewVisibilityMasks(osg::Camera *camera,
+                                            osg::ref_ptr<osg::MatrixTransform> &transform)
+{
+    for (uint32_t i = 0; i < _xrViews.size(); ++i)
+    {
+        osg::ref_ptr<osg::Geode> geode = setupVisibilityMask(camera, i, transform);
+        if (geode.valid())
+        {
+            if (i == 0)
+                geode->setNodeMask(_visibilityMaskLeft);
+            else
+                geode->setNodeMask(_visibilityMaskRight);
+        }
+    }
+}
+
+osg::ref_ptr<osg::Geode> XRState::setupVisibilityMask(osg::Camera *camera, uint32_t viewIndex,
+                                                      osg::ref_ptr<osg::MatrixTransform> &transform)
+{
+    osg::ref_ptr<osg::Geometry> geometry;
+    geometry = _session->getVisibilityMask(viewIndex,
+                                           XR_VISIBILITY_MASK_TYPE_HIDDEN_TRIANGLE_MESH_KHR);
+    if (!geometry.valid())
+        return nullptr;
+
+    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
+    geode->setCullingActive(false);
+    geode->addDrawable(geometry);
+
+    osg::ref_ptr<osg::StateSet> state = geode->getOrCreateStateSet();
+    int forceOff = osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED;
+    state->setMode(GL_LIGHTING, forceOff);
+    state->setAttribute(new osg::ColorMask(false, false, false, false),
+                        osg::StateAttribute::OVERRIDE);
+    state->setAttribute(new osg::Depth(osg::Depth::ALWAYS, 0.0f, 0.0f, true),
+                        osg::StateAttribute::OVERRIDE);
+    state->setRenderBinDetails(INT_MIN, "RenderBin");
+
+    if (!transform.valid())
+    {
+        transform = new osg::MatrixTransform;
+        transform->setReferenceFrame(osg::Camera::ABSOLUTE_RF);
+    }
+    transform->addChild(geode);
+
+    camera->addChild(transform);
+
+    return geode;
+}
+
+osg::ref_ptr<OpenXR::Session::Frame> XRState::getFrame(osg::FrameStamp *stamp)
+{
+    // Fast path
+    osg::ref_ptr<OpenXR::Session::Frame> frame = _frames.getFrame(stamp);
+    if (frame.valid())
+        return frame;
+
+    if (!_session->isRunning())
+        return nullptr;
+
+    // Slow path
+    return _frames.getFrame(stamp, _session);
+}
+
+void XRState::startRendering(osg::FrameStamp *stamp)
+{
+    osg::ref_ptr<OpenXR::Session::Frame> frame = getFrame(stamp);
+    if (frame.valid() && !frame->hasBegun())
+    {
+        frame->begin();
+        _projectionLayer = new OpenXR::CompositionLayerProjection(_xrViews.size());
+        _projectionLayer->setLayerFlags(XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT);
+        _projectionLayer->setSpace(_session->getLocalSpace());
+    }
+}
+
+void XRState::endFrame(osg::FrameStamp *stamp)
+{
+    osg::ref_ptr<OpenXR::Session::Frame> frame = _frames.getFrame(stamp);
+    if (!frame.valid())
+    {
+        OSG_WARN << "OpenXR frame not waited for" << std::endl;
+        return;
+    }
+    if (!frame->hasBegun())
+    {
+        OSG_WARN << "OpenXR frame not begun" << std::endl;
+        return;
+    }
+    for (auto &view: _xrViews)
+        view->endFrame(frame);
+    frame->setEnvBlendMode(_chosenEnvBlendMode);
+    frame->addLayer(_projectionLayer.get());
+    _frames.endFrame(stamp);
+}
+
+void XRState::updateSlave(uint32_t viewIndex, osg::View& view,
+                          osg::View::Slave& slave)
+{
+    bool setProjection = false;
+    osg::Matrix projectionMatrix;
+
+    osg::ref_ptr<OpenXR::Session::Frame> frame = getFrame(view.getFrameStamp());
+    if (frame.valid())
+    {
+        if (frame->isPositionValid() && frame->isOrientationValid())
+        {
+            const auto &pose = frame->getViewPose(viewIndex);
+            osg::Vec3 position(pose.position.x,
+                               pose.position.y,
+                               pose.position.z);
+            osg::Quat orientation(pose.orientation.x,
+                                  pose.orientation.y,
+                                  pose.orientation.z,
+                                  pose.orientation.w);
+
+            osg::Matrix viewOffset;
+            viewOffset.setTrans(viewOffset.getTrans() + position * _settings->getUnitsPerMeter());
+            viewOffset.preMultRotate(orientation);
+            viewOffset = osg::Matrix::inverse(viewOffset);
+            slave._viewOffset = viewOffset;
+
+            double left, right, bottom, top, zNear, zFar;
+            if (view.getCamera()->getProjectionMatrixAsFrustum(left, right,
+                                                               bottom, top,
+                                                               zNear, zFar))
+            {
+                const auto &fov = frame->getViewFov(viewIndex);
+                createProjectionFov(projectionMatrix, fov, zNear, zFar);
+                setProjection = true;
+            }
+        }
+    }
+
+    //slave._camera->setViewMatrix(view.getCamera()->getViewMatrix() * slave._viewOffset);
+    slave.updateSlaveImplementation(view);
+    if (setProjection)
+    {
+        slave._camera->setProjectionMatrix(projectionMatrix);
+    }
+}
+
+void XRState::updateVisibilityMaskTransform(osg::Camera *camera,
+                                            osg::MatrixTransform *transform)
+{
+    float scale = 1.0f;
+    double left, right, bottom, top, zNear, zFar;
+    if (camera->getProjectionMatrixAsFrustum(left, right,
+                                             bottom, top,
+                                             zNear, zFar))
+    {
+        if (isinf(zFar))
+            scale = zNear * 1.1;
+        else
+            scale = (zNear + zFar) / 2;
+    }
+    transform->setMatrix(osg::Matrix::translate(0, 0, -1));
+    transform->postMult(osg::Matrix::scale(scale, scale, scale));
+}
+
+osg::Matrixd XRState::getEyeProjection(osg::FrameStamp *stamp,
+                                       uint32_t viewIndex,
+                                       const osg::Matrixd& projection)
+{
+    osg::ref_ptr<OpenXR::Session::Frame> frame = getFrame(stamp);
+    if (frame.valid())
+    {
+        double left, right, bottom, top, zNear, zFar;
+        if (projection.getFrustum(left, right,
+                                  bottom, top,
+                                  zNear, zFar))
+        {
+            const auto &fov = frame->getViewFov(viewIndex);
+            osg::Matrix projectionMatrix;
+            createProjectionFov(projectionMatrix, fov, zNear, zFar);
+            return projectionMatrix;
+        }
+    }
+    return projection;
+}
+
+osg::Matrixd XRState::getEyeView(osg::FrameStamp *stamp, uint32_t viewIndex,
+                                 const osg::Matrixd& view)
+{
+    osg::ref_ptr<OpenXR::Session::Frame> frame = getFrame(stamp);
+    if (frame.valid())
+    {
+        if (frame->isPositionValid() && frame->isOrientationValid())
+        {
+            const auto &pose = frame->getViewPose(viewIndex);
+            osg::Vec3 position(pose.position.x,
+                               pose.position.y,
+                               pose.position.z);
+            osg::Quat orientation(pose.orientation.x,
+                                  pose.orientation.y,
+                                  pose.orientation.z,
+                                  pose.orientation.w);
+
+            osg::Matrix viewOffset;
+            viewOffset.setTrans(viewOffset.getTrans() + position * _settings->getUnitsPerMeter());
+            viewOffset.preMultRotate(orientation);
+            viewOffset = osg::Matrix::inverse(viewOffset);
+            return view * viewOffset;
+        }
+    }
+    return view;
+}
+
+void XRState::initialDrawCallback(osg::RenderInfo &renderInfo)
+{
+    osg::GraphicsOperation *graphicsOperation = renderInfo.getCurrentCamera()->getRenderer();
+    osgViewer::Renderer *renderer = dynamic_cast<osgViewer::Renderer*>(graphicsOperation);
+    if (renderer != nullptr)
+    {
+        // Disable normal OSG FBO camera setup because it will undo the MSAA FBO configuration.
+        renderer->setCameraRequiresSetUp(false);
+    }
+
+    startRendering(renderInfo.getState()->getFrameStamp());
+
+    // Get up to date depth info from camera's projection matrix
+    _depthInfo.setZRangeFromProjection(renderInfo.getCurrentCamera()->getProjectionMatrix());
+}
+
+void XRState::swapBuffersImplementation(osg::GraphicsContext* gc)
+{
+    // Submit rendered frame to compositor
+    //m_device->submitFrame();
+
+    endFrame(gc->getState()->getFrameStamp());
+
+    // Blit mirror texture to backbuffer
+    //m_device->blitMirrorTexture(gc);
+
+    // Run the default system swapBufferImplementation
+    gc->swapBuffersImplementation();
+}
diff --git a/3rdparty/osgXR/src/XRState.h b/3rdparty/osgXR/src/XRState.h
new file mode 100644
index 000000000..65cdbd83d
--- /dev/null
+++ b/3rdparty/osgXR/src/XRState.h
@@ -0,0 +1,590 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_XRSTATE
+#define OSGXR_XRSTATE 1
+
+#include "OpenXR/ActionSet.h"
+#include "OpenXR/EventHandler.h"
+#include "OpenXR/Instance.h"
+#include "OpenXR/InteractionProfile.h"
+#include "OpenXR/System.h"
+#include "OpenXR/Session.h"
+#include "OpenXR/SwapchainGroup.h"
+#include "OpenXR/SwapchainGroupSubImage.h"
+#include "OpenXR/Compositor.h"
+#include "OpenXR/DepthInfo.h"
+
+#include "XRFramebuffer.h"
+#include "FrameStampedVector.h"
+#include "FrameStore.h"
+
+#include <osg/DisplaySettings>
+#include <osg/Referenced>
+#include <osg/observer_ptr>
+#include <osg/ref_ptr>
+
+#include <osgXR/ActionSet>
+#include <osgXR/InteractionProfile>
+#include <osgXR/Settings>
+#include <osgXR/Subaction>
+#include <osgXR/View>
+
+#include <memory>
+#include <vector>
+
+namespace osg {
+    class FrameStamp;
+}
+
+namespace osgViewer {
+    class ViewerBase;
+}
+
+namespace osgXR {
+
+class Manager;
+
+class XRState : public OpenXR::EventHandler
+{
+    public:
+        typedef Settings::VRMode VRMode;
+        typedef Settings::SwapchainMode SwapchainMode;
+
+        XRState(Settings *settings, Manager *manager = nullptr);
+
+        /// Represents a swapchain group
+        class XRSwapchain : public OpenXR::SwapchainGroup
+        {
+            public:
+
+                XRSwapchain(XRState *state,
+                            osg::ref_ptr<OpenXR::Session> session,
+                            const OpenXR::System::ViewConfiguration::View &view,
+                            int64_t chosenSwapchainFormat,
+                            int64_t chosenDepthSwapchainFormat);
+
+                // GL context must be current (for XRFramebuffer)
+                virtual ~XRSwapchain();
+
+                void incNumDrawPasses(unsigned int num = 1)
+                {
+                    _numDrawPasses += num;
+                }
+
+                void decNumDrawPasses(unsigned int num = 1)
+                {
+                    _numDrawPasses -= num;
+                }
+
+                unsigned int getNumDrawPasses()
+                {
+                    return _numDrawPasses;
+                }
+
+                void setupImage(const osg::FrameStamp *stamp);
+
+                void preDrawCallback(osg::RenderInfo &renderInfo);
+                void postDrawCallback(osg::RenderInfo &renderInfo);
+                void endFrame();
+
+                osg::ref_ptr<osg::Texture2D> getOsgTexture(const osg::FrameStamp *stamp);
+
+            protected:
+
+                XRState *_state;
+                FrameStampedVector<osg::ref_ptr<XRFramebuffer> > _imageFramebuffers;
+
+                /// Number of expected draw passes.
+                unsigned int _numDrawPasses;
+                unsigned int _drawPassesDone;
+                bool _imagesReady;
+        };
+
+        /// Represents an OpenXR view
+        class XRView : public osg::Referenced
+        {
+            public:
+
+                XRView(XRState *state,
+                       uint32_t viewIndex,
+                       osg::ref_ptr<XRSwapchain> swapchain);
+                XRView(XRState *state,
+                       uint32_t viewIndex,
+                       osg::ref_ptr<XRSwapchain> swapchain,
+                       const OpenXR::System::ViewConfiguration::View::Viewport &viewport);
+
+                // GL context must be current (for XRFramebuffer)
+                virtual ~XRView();
+
+                bool valid() const
+                {
+                    return _swapchainSubImage.valid();
+                }
+
+                osg::ref_ptr<XRSwapchain> getSwapchain()
+                {
+                    return static_cast<XRSwapchain *>(_swapchainSubImage.getSwapchainGroup().get());
+                }
+
+                const XRSwapchain::SubImage &getSubImage() const
+                {
+                    return _swapchainSubImage;
+                }
+
+                void setupCamera(osg::ref_ptr<osg::Camera> camera);
+
+                void endFrame(OpenXR::Session::Frame *frame);
+
+            protected:
+
+                XRState *_state;
+                XRSwapchain::SubImage _swapchainSubImage;
+
+                uint32_t _viewIndex;
+        };
+
+        /** Represents a generic app level view.
+         * This may handle multiple OpenXR views.
+         */
+        class AppView : public View
+        {
+            public:
+
+                AppView(XRState *state,
+                        osgViewer::GraphicsWindow *window,
+                        osgViewer::View *osgView);
+                virtual ~AppView();
+
+                void destroy();
+
+                void init();
+
+            protected:
+
+                bool _valid;
+
+                XRState *_state;
+        };
+
+        /// Represents an app level view in slave cams mode
+        class SlaveCamsAppView : public AppView
+        {
+            public:
+
+                SlaveCamsAppView(XRState *state,
+                                 uint32_t viewIndex,
+                                 osgViewer::GraphicsWindow *window,
+                                 osgViewer::View *osgView);
+
+                void addSlave(osg::Camera *slaveCamera) override;
+                void removeSlave(osg::Camera *slaveCamera) override;
+
+            protected:
+
+                uint32_t _viewIndex;
+        };
+
+        /// Represents an app level view in scene view mode
+        class SceneViewAppView : public AppView
+        {
+            public:
+
+                SceneViewAppView(XRState *state,
+                                 osgViewer::GraphicsWindow *window,
+                                 osgViewer::View *osgView);
+
+                void addSlave(osg::Camera *slaveCamera) override;
+                void removeSlave(osg::Camera *slaveCamera) override;
+        };
+
+        bool hasValidationLayer() const;
+        bool hasDepthInfoExtension() const;
+        bool hasVisibilityMaskExtension() const;
+
+        inline const char *getRuntimeName() const
+        {
+            if (_currentState < VRSTATE_INSTANCE)
+                return "";
+            return _instance->getRuntimeName();
+        }
+
+        inline const char *getSystemName() const
+        {
+            if (_currentState < VRSTATE_SYSTEM)
+                return "";
+            return _system->getSystemName();
+        }
+
+        inline bool getPresent() const
+        {
+            return _instance.valid() && _instance->valid();
+        }
+
+        inline bool valid() const
+        {
+            return _currentState >= VRSTATE_SESSION;
+        }
+
+        typedef enum {
+            /// No OpenXR instance.
+            VRSTATE_DISABLED = 0,
+            /// OpenXR instance created.
+            VRSTATE_INSTANCE,
+            /// Valid OpenXR system found.
+            VRSTATE_SYSTEM,
+            /// Session created
+            VRSTATE_SESSION,
+            /// Actions configured
+            VRSTATE_ACTIONS,
+
+            VRSTATE_MAX,
+        } VRState;
+
+        /// Set the init state to drop down to before returning to prior level.
+        void setDownState(VRState downState)
+        {
+            if (downState < _downState && downState < _currentState)
+            {
+                _downState = downState;
+                _stateChanged = true;
+            }
+        }
+        /// Get the current init state to rise up to.
+        VRState getUpState() const
+        {
+            return _upState;
+        }
+        /// Get the current init state to rise up to.
+        VRState getCurrentState() const
+        {
+            return _currentState;
+        }
+        /// Set the init state to rise up to.
+        void setUpState(VRState upState)
+        {
+            if (upState != _upState)
+            {
+                _upState = upState;
+                _stateChanged = true;
+            }
+        }
+        /// Set the minimum init state to rise up to.
+        void setMinUpState(VRState minUpState)
+        {
+            if (minUpState > _upState)
+            {
+                _upState = minUpState;
+                _stateChanged = true;
+            }
+        }
+        /// Set destination state, both up and down.
+        void setDestState(VRState destState)
+        {
+            setDownState(destState);
+            setUpState(destState);
+        }
+        /// Find if updates are needed for state changes.
+        bool isStateUpdateNeeded() const
+        {
+            return _currentState > _downState || _currentState < _upState;
+        }
+
+        /// Find if a VR session is running.
+        bool isRunning() const
+        {
+            if (_currentState < VRSTATE_SESSION)
+                return false;
+            return _session->isRunning();
+        }
+
+        /// Set whether probing should be active.
+        void setProbing(bool probing)
+        {
+            if (_probing == probing)
+                return;
+            _probing = probing;
+            if (probing)
+            {
+                // Init at least up to system
+                setMinUpState(VRSTATE_SYSTEM);
+            }
+            else
+            {
+                // If only initing to system, shutdown
+                if (_upState <= VRSTATE_SYSTEM)
+                    setDestState(VRSTATE_DISABLED);
+            }
+        }
+
+        VRState getProbingState() const
+        {
+            return _probing ? VRSTATE_SYSTEM : VRSTATE_DISABLED;
+        }
+
+        void setViewer(osgViewer::ViewerBase *viewer)
+        {
+            _viewer = viewer;
+        }
+
+        /// Set the NodeMasks to use for visibility masks.
+        void setVisibilityMaskNodeMasks(osg::Node::NodeMask left,
+                                        osg::Node::NodeMask right)
+        {
+            _visibilityMaskLeft = left;
+            _visibilityMaskRight = right;
+        }
+
+        /// Get the subaction object for a subaction path string.
+        std::shared_ptr<Subaction::Private> getSubaction(const std::string &path);
+
+        /// Add an action set
+        void addActionSet(ActionSet::Private *actionSet)
+        {
+            _actionSets.insert(actionSet);
+            _actionsUpdated = true;
+        }
+
+        /// Remove an action set
+        void removeActionSet(ActionSet::Private *actionSet)
+        {
+            _actionSets.erase(actionSet);
+            _actionsUpdated = true;
+        }
+
+        /// Add an interaction profile
+        void addInteractionProfile(InteractionProfile::Private *interactionProfile)
+        {
+            _interactionProfiles.insert(interactionProfile);
+            _actionsUpdated = true;
+        }
+
+        /// Remove an interaction profile
+        void removeInteractionProfile(InteractionProfile::Private *interactionProfile)
+        {
+            _interactionProfiles.erase(interactionProfile);
+            _actionsUpdated = true;
+        }
+
+        /// Get the current interaction profile for the given subaction path.
+        InteractionProfile *getCurrentInteractionProfile(const OpenXR::Path &subactionPath) const;
+
+        /// Get a string describing the state (for user consumption).
+        const char *getStateString() const;
+
+        // Initialize information required for setting up VR
+        void init(osgViewer::GraphicsWindow *window,
+                  osgViewer::View *view = nullptr)
+        {
+            _window = window;
+            _view = view;
+        }
+
+        /// Update down state depending on any changed settings.
+        void syncSettings();
+
+        /// Find whether actions have been updated.
+        bool getActionsUpdated() const;
+
+        /// Arrange reinit as needed of action setup.
+        void syncActionSetup();
+
+        /// Find whether state has changed since last call, and reset.
+        bool checkAndResetStateChanged();
+
+        /// Perform a regular update.
+        void update();
+
+        // Extending OpenXR::EventManager
+        void onInstanceLossPending(OpenXR::Instance *instance,
+                                   const XrEventDataInstanceLossPending *event) override;
+        void onInteractionProfileChanged(OpenXR::Session *session,
+                                         const XrEventDataInteractionProfileChanged *event) override;
+        void onSessionStateChanged(OpenXR::Session *session,
+                                   const XrEventDataSessionStateChanged *event) override;
+        void onSessionStateStart(OpenXR::Session *session) override;
+        void onSessionStateEnd(OpenXR::Session *session, bool retry) override;
+        void onSessionStateReady(OpenXR::Session *session) override;
+        void onSessionStateStopping(OpenXR::Session *session, bool loss) override;
+        void onSessionStateFocus(OpenXR::Session *session) override;
+        void onSessionStateUnfocus(OpenXR::Session *session) override;
+
+        osg::ref_ptr<OpenXR::Session::Frame> getFrame(osg::FrameStamp *stamp);
+        void startRendering(osg::FrameStamp *stamp);
+        void endFrame(osg::FrameStamp *stamp);
+
+        void updateSlave(uint32_t viewIndex, osg::View& view,
+                         osg::View::Slave& slave);
+        void updateVisibilityMaskTransform(osg::Camera *camera,
+                                           osg::MatrixTransform *transform);
+
+        osg::Matrixd getEyeProjection(osg::FrameStamp *stamp,
+                                      uint32_t viewIndex,
+                                      const osg::Matrixd& projection);
+        osg::Matrixd getEyeView(osg::FrameStamp *stamp, uint32_t viewIndex,
+                                const osg::Matrixd& view);
+
+        void initialDrawCallback(osg::RenderInfo &renderInfo);
+        void swapBuffersImplementation(osg::GraphicsContext* gc);
+
+        inline osg::ref_ptr<OpenXR::CompositionLayerProjection> getProjectionLayer()
+        {
+            return _projectionLayer;
+        }
+
+        class TextureRect
+        {
+            public:
+
+                float x, y;
+                float width, height;
+
+                TextureRect(const OpenXR::SwapchainGroup::SubImage &subImage)
+                {
+                    float w = subImage.getSwapchainGroup()->getWidth();
+                    float h = subImage.getSwapchainGroup()->getHeight();
+                    x = (float)subImage.getX() / w;
+                    y = (float)subImage.getY() / h;
+                    width = (float)subImage.getWidth() / w;
+                    height = (float)subImage.getHeight() / h;
+                }
+        };
+
+        unsigned int getViewCount() const
+        {
+            return _xrViews.size();
+        }
+
+        TextureRect getViewTextureRect(unsigned int viewIndex) const
+        {
+            return TextureRect(_xrViews[viewIndex]->getSubImage());
+        }
+
+        // Caller must validate viewIndex using getViewCount()
+        osg::ref_ptr<osg::Texture2D> getViewTexture(unsigned int viewIndex,
+                                                    const osg::FrameStamp *stamp) const
+        {
+            return _xrViews[viewIndex]->getSwapchain()->getOsgTexture(stamp);
+        }
+
+    protected:
+
+        typedef enum {
+            /// Successfully completed operation.
+            UP_SUCCESS,
+            /// Operation not possible at the moment, try again soon.
+            UP_SOON,
+            /// Operation not possible at the moment, try again later.
+            UP_LATER,
+            /// Operation permanently failed, disable VR.
+            UP_ABORT,
+        } UpResult;
+
+        typedef enum {
+            /// Successfully completed operation.
+            DOWN_SUCCESS,
+            /// Operation not possible at the moment, try again soon.
+            DOWN_SOON,
+        } DownResult;
+
+        // Pre-instance probing
+        void probe() const;
+        void unprobe() const;
+
+        // These are called during update to raise or lower VR state level
+        UpResult upInstance();
+        DownResult downInstance();
+        UpResult upSystem();
+        DownResult downSystem();
+        UpResult upSession();
+        DownResult downSession();
+        UpResult upActions();
+        DownResult downActions();
+
+        // Set up a single swapchain containing multiple viewports
+        bool setupSingleSwapchain(int64_t format, int64_t depthFormat = 0);
+        // Set up a swapchain for each view
+        bool setupMultipleSwapchains(int64_t format, int64_t depthFormat = 0);
+        // Set up slave cameras
+        void setupSlaveCameras();
+        // Set up SceneView VR mode cameras
+        void setupSceneViewCameras();
+        void setupSceneViewCamera(osg::Camera *camera);
+        // Visibility mask setup
+        inline bool needsVisibilityMask(osg::Camera *camera)
+        {
+            return _useVisibilityMask &&
+                (camera->getClearMask() & GL_DEPTH_BUFFER_BIT);
+        }
+        void setupSceneViewVisibilityMasks(osg::Camera *camera,
+                                           osg::ref_ptr<osg::MatrixTransform> &transform);
+        osg::ref_ptr<osg::Geode> setupVisibilityMask(osg::Camera *camera,
+                                                     uint32_t viewIndex,
+                                                     osg::ref_ptr<osg::MatrixTransform> &transform);
+
+        osg::ref_ptr<Settings> _settings;
+        Settings _settingsCopy;
+        osg::observer_ptr<Manager> _manager;
+
+        // app configuration
+        osg::Node::NodeMask _visibilityMaskLeft;
+        osg::Node::NodeMask _visibilityMaskRight;
+
+        // Actions
+        bool _actionsUpdated;
+        std::set<ActionSet::Private *> _actionSets;
+        std::set<InteractionProfile::Private *> _interactionProfiles;
+        std::map<std::string, std::weak_ptr<Subaction::Private>> _subactions;
+
+        /// Current state of OpenXR initialization.
+        VRState _currentState;
+        /// State of OpenXR initialisation to drop down to.
+        VRState _downState;
+        /// State of OpenXR initialisation to rise up to.
+        VRState _upState;
+        /// Number of attempts made to rise VR state.
+        unsigned int _upDelay;
+        /// Whether probing should be kept active.
+        bool _probing;
+        /// Last read state as a user readable string.
+        mutable std::string _stateString;
+        /// Whether state has changed since the last update.
+        bool _stateChanged;
+
+        // Session setup
+        osg::observer_ptr<osgViewer::ViewerBase> _viewer;
+        osg::observer_ptr<osgViewer::GraphicsWindow> _window;
+        osg::observer_ptr<osgViewer::View> _view;
+
+        // Pre-Instance related
+        mutable bool _probed;
+        mutable bool _hasValidationLayer;
+        mutable bool _hasDepthInfoExtension;
+        mutable bool _hasVisibilityMaskExtension;
+
+        // Instance related
+        osg::ref_ptr<OpenXR::Instance> _instance;
+        bool _useDepthInfo;
+        bool _useVisibilityMask;
+
+        // System related
+        XrFormFactor _formFactor;
+        OpenXR::System *_system;
+        const OpenXR::System::ViewConfiguration *_chosenViewConfig;
+        XrEnvironmentBlendMode _chosenEnvBlendMode;
+
+        // Session related
+        VRMode _vrMode;
+        SwapchainMode _swapchainMode;
+        osg::ref_ptr<OpenXR::Session> _session;
+        std::vector<osg::ref_ptr<XRView> > _xrViews;
+        std::vector<osg::ref_ptr<AppView> > _appViews;
+        FrameStore _frames;
+        osg::ref_ptr<OpenXR::CompositionLayerProjection> _projectionLayer;
+        OpenXR::DepthInfo _depthInfo;
+        osg::ref_ptr<osg::DisplaySettings> _stereoDisplaySettings;
+};
+
+} // osgXR
+
+#endif
diff --git a/3rdparty/osgXR/src/XRStateCallbacks.h b/3rdparty/osgXR/src/XRStateCallbacks.h
new file mode 100644
index 000000000..45f285549
--- /dev/null
+++ b/3rdparty/osgXR/src/XRStateCallbacks.h
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_XRSTATE_CALLBACKS
+#define OSGXR_XRSTATE_CALLBACKS 1
+
+#include "XRState.h"
+
+#include <osg/Camera>
+#include <osg/GraphicsContext>
+#include <osg/View>
+
+#include <osgUtil/SceneView>
+
+namespace osgXR {
+
+class SlaveCamsUpdateSlaveCallback : public osg::View::Slave::UpdateSlaveCallback
+{
+    public:
+
+        SlaveCamsUpdateSlaveCallback(uint32_t viewIndex,
+                                     XRState *xrState,
+                                     osg::MatrixTransform *visMaskTransform) :
+            _viewIndex(viewIndex),
+            _xrState(xrState),
+            _visMaskTransform(visMaskTransform)
+        {
+        }
+
+        void updateSlave(osg::View& view, osg::View::Slave& slave) override
+        {
+            _xrState->updateSlave(_viewIndex, view, slave);
+            if (_visMaskTransform.valid())
+                _xrState->updateVisibilityMaskTransform(slave._camera,
+                                                        _visMaskTransform.get());
+        }
+
+    protected:
+
+        uint32_t _viewIndex;
+        osg::observer_ptr<XRState> _xrState;
+        osg::observer_ptr<osg::MatrixTransform> _visMaskTransform;
+};
+
+class SceneViewUpdateSlaveCallback : public osg::View::Slave::UpdateSlaveCallback
+{
+    public:
+
+        SceneViewUpdateSlaveCallback(osg::ref_ptr<XRState> xrState,
+                                     osg::ref_ptr<osg::MatrixTransform> visMaskTransform) :
+            _xrState(xrState),
+            _visMaskTransform(visMaskTransform)
+        {
+        }
+
+        void updateSlave(osg::View& view, osg::View::Slave& slave) override
+        {
+            if (_visMaskTransform.valid())
+                _xrState->updateVisibilityMaskTransform(slave._camera,
+                                                        _visMaskTransform.get());
+        }
+
+    protected:
+
+        osg::observer_ptr<XRState> _xrState;
+        osg::observer_ptr<osg::MatrixTransform> _visMaskTransform;
+};
+
+class ComputeStereoMatricesCallback : public osgUtil::SceneView::ComputeStereoMatricesCallback
+{
+    public:
+
+        ComputeStereoMatricesCallback(XRState *xrState,
+                                      osgUtil::SceneView *sceneView) :
+            _xrState(xrState),
+            _sceneView(sceneView)
+        {
+        }
+
+        osg::Matrixd computeLeftEyeProjection(const osg::Matrixd& projection) const override
+        {
+            return _xrState->getEyeProjection(_sceneView->getFrameStamp(),
+                                              0, projection);
+        }
+
+        osg::Matrixd computeLeftEyeView(const osg::Matrixd& view) const override
+        {
+            return _xrState->getEyeView(_sceneView->getFrameStamp(),
+                                        0, view);
+        }
+
+        osg::Matrixd computeRightEyeProjection(const osg::Matrixd& projection) const override
+        {
+            return _xrState->getEyeProjection(_sceneView->getFrameStamp(),
+                                              1, projection);
+        }
+
+        osg::Matrixd computeRightEyeView(const osg::Matrixd& view) const override
+        {
+            return _xrState->getEyeView(_sceneView->getFrameStamp(),
+                                        1, view);
+        }
+
+    protected:
+
+        osg::observer_ptr<XRState> _xrState;
+        osg::observer_ptr<osgUtil::SceneView> _sceneView;
+};
+
+class InitialDrawCallback : public osg::Camera::DrawCallback
+{
+    public:
+
+        InitialDrawCallback(osg::ref_ptr<XRState> xrState) :
+            _xrState(xrState)
+        {
+        }
+
+        void operator()(osg::RenderInfo& renderInfo) const override
+        {
+            _xrState->initialDrawCallback(renderInfo);
+        }
+
+    protected:
+
+        osg::observer_ptr<XRState> _xrState;
+};
+
+class PreDrawCallback : public osg::Camera::DrawCallback
+{
+    public:
+
+        PreDrawCallback(osg::ref_ptr<XRState::XRSwapchain> xrSwapchain) :
+            _xrSwapchain(xrSwapchain)
+        {
+        }
+
+        void operator()(osg::RenderInfo& renderInfo) const override
+        {
+            _xrSwapchain->preDrawCallback(renderInfo);
+        }
+
+    protected:
+
+        osg::observer_ptr<XRState::XRSwapchain> _xrSwapchain;
+};
+
+class PostDrawCallback : public osg::Camera::DrawCallback
+{
+    public:
+
+        PostDrawCallback(osg::ref_ptr<XRState::XRSwapchain> xrSwapchain) :
+            _xrSwapchain(xrSwapchain)
+        {
+        }
+
+        void operator()(osg::RenderInfo& renderInfo) const override
+        {
+            _xrSwapchain->postDrawCallback(renderInfo);
+        }
+
+    protected:
+
+        osg::observer_ptr<XRState::XRSwapchain> _xrSwapchain;
+};
+
+class SwapCallback : public osg::GraphicsContext::SwapCallback
+{
+    public:
+
+        explicit SwapCallback(osg::ref_ptr<XRState> xrState) :
+            _xrState(xrState),
+            _frameIndex(0)
+        {
+        }
+
+        void swapBuffersImplementation(osg::GraphicsContext* gc)
+        {
+            _xrState->swapBuffersImplementation(gc);
+        }
+
+        int frameIndex() const
+        {
+            return _frameIndex;
+        }
+
+    private:
+
+        osg::observer_ptr<XRState> _xrState;
+        int _frameIndex;
+};
+
+}
+
+#endif
diff --git a/3rdparty/osgXR/src/osgXR.cpp b/3rdparty/osgXR/src/osgXR.cpp
new file mode 100644
index 000000000..a31147263
--- /dev/null
+++ b/3rdparty/osgXR/src/osgXR.cpp
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#include <osgXR/MirrorSettings>
+#include <osgXR/OpenXRDisplay>
+#include <osgXR/Settings>
+#include <osgXR/osgXR>
+
+#include <osg/Notify>
+#include <osg/os_utils>
+
+using namespace osgXR;
+
+void osgXR::setupViewerDefaults(osgViewer::Viewer *viewer,
+                                const std::string &appName,
+                                uint32_t appVersion)
+{
+    unsigned int vr = 0;
+    osg::getEnvVar("OSGXR", vr);
+
+    if (vr)
+    {
+        Settings *settings = Settings::instance();
+        MirrorSettings *mirrorSettings = &settings->getMirrorSettings();
+        std::string value;
+
+        Settings::VRMode vrMode = Settings::VRMODE_AUTOMATIC;
+        if (osg::getEnvVar("OSGXR_MODE", value))
+        {
+            if (value == "SLAVE_CAMERAS")
+                vrMode = Settings::VRMODE_SLAVE_CAMERAS;
+            else if (value == "SCENE_VIEW")
+                vrMode = Settings::VRMODE_SCENE_VIEW;
+        }
+
+        Settings::SwapchainMode swapchainMode = Settings::SWAPCHAIN_AUTOMATIC;
+        if (osg::getEnvVar("OSGXR_SWAPCHAIN", value))
+        {
+            if (value == "MULTIPLE")
+                swapchainMode = Settings::SWAPCHAIN_MULTIPLE;
+            else if (value == "SINGLE")
+                swapchainMode = Settings::SWAPCHAIN_SINGLE;
+        }
+
+        float unitsPerMeter = 0.0f;
+        osg::getEnvVar("OSGXR_UNITS_PER_METER", unitsPerMeter);
+
+        int validationLayer = 0;
+        osg::getEnvVar("OSGXR_VALIDATION_LAYER", validationLayer);
+
+        int depthInfo = 0;
+        osg::getEnvVar("OSGXR_DEPTH_INFO", depthInfo);
+
+        MirrorSettings::MirrorMode mirrorMode = MirrorSettings::MIRROR_AUTOMATIC;
+        int mirrorViewIndex = -1;
+        if (osg::getEnvVar("OSGXR_MIRROR", value))
+        {
+            if (value == "NONE")
+            {
+                mirrorMode = MirrorSettings::MIRROR_NONE;
+            }
+            else if (value == "LEFT")
+            {
+                mirrorMode = MirrorSettings::MIRROR_SINGLE;
+                mirrorViewIndex = 0;
+            }
+            else if (value == "RIGHT")
+            {
+                mirrorMode = MirrorSettings::MIRROR_SINGLE;
+                mirrorViewIndex = 1;
+            }
+            else if (value == "LEFT_RIGHT")
+            {
+                mirrorMode = MirrorSettings::MIRROR_LEFT_RIGHT;
+            }
+        }
+
+        settings->setApp(appName, appVersion);
+        settings->setFormFactor(Settings::HEAD_MOUNTED_DISPLAY);
+        settings->preferEnvBlendMode(Settings::OPAQUE);
+        if (unitsPerMeter > 0.0f)
+            settings->setUnitsPerMeter(unitsPerMeter);
+        settings->setVRMode(vrMode);
+        settings->setSwapchainMode(swapchainMode);
+        settings->setValidationLayer(!!validationLayer);
+        settings->setDepthInfo(!!depthInfo);
+        mirrorSettings->setMirror(mirrorMode, mirrorViewIndex);
+
+        osg::ref_ptr<OpenXRDisplay> xr = new OpenXRDisplay(settings);
+        viewer->apply(xr);
+
+        OSG_WARN << "Setting up VR" << std::endl;
+    }
+}
diff --git a/3rdparty/osgXR/src/projection.cpp b/3rdparty/osgXR/src/projection.cpp
new file mode 100644
index 000000000..b7d15b73d
--- /dev/null
+++ b/3rdparty/osgXR/src/projection.cpp
@@ -0,0 +1,79 @@
+// =============================================================================
+// Derived from openxr-simple-example
+// Copyright 2019-2021, Collabora, Ltd.
+// Which was adapted from
+// https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/master/src/common/xr_linear.h
+// Copyright (c) 2017 The Khronos Group Inc.
+// Copyright (c) 2016 Oculus VR, LLC.
+// SPDX-License-Identifier: Apache-2.0
+// =============================================================================
+
+#include "projection.h"
+
+void osgXR::createProjectionFov(osg::Matrix& result,
+                                const XrFovf& fov,
+                                const float nearZ,
+                                const float farZ)
+{
+    const float tanAngleLeft = tanf(fov.angleLeft);
+    const float tanAngleRight = tanf(fov.angleRight);
+
+    const float tanAngleDown = tanf(fov.angleDown);
+    const float tanAngleUp = tanf(fov.angleUp);
+
+    const float tanAngleWidth = tanAngleRight - tanAngleLeft;
+
+    // Set to tanAngleDown - tanAngleUp for a clip space with positive Y
+    // down (Vulkan). Set to tanAngleUp - tanAngleDown for a clip space with
+    // positive Y up (OpenGL / D3D / Metal).
+    const float tanAngleHeight = tanAngleUp - tanAngleDown;
+
+    // Set to nearZ for a [-1,1] Z clip space (OpenGL / OpenGL ES).
+    // Set to zero for a [0,1] Z clip space (Vulkan / D3D / Metal).
+    const float offsetZ = nearZ;
+
+    if (farZ <= nearZ)
+    {
+        // place the far plane at infinity
+        result(0, 0) = 2 / tanAngleWidth;
+        result(1, 0) = 0;
+        result(2, 0) = (tanAngleRight + tanAngleLeft) / tanAngleWidth;
+        result(3, 0) = 0;
+
+        result(0, 1) = 0;
+        result(1, 1) = 2 / tanAngleHeight;
+        result(2, 1) = (tanAngleUp + tanAngleDown) / tanAngleHeight;
+        result(3, 1) = 0;
+
+        result(0, 2) = 0;
+        result(1, 2) = 0;
+        result(2, 2) = -1;
+        result(3, 2) = -(nearZ + offsetZ);
+
+        result(0, 3) = 0;
+        result(1, 3) = 0;
+        result(2, 3) = -1;
+        result(3, 3) = 0;
+    } else {
+        // normal projection
+        result(0, 0) = 2 / tanAngleWidth;
+        result(1, 0) = 0;
+        result(2, 0) = (tanAngleRight + tanAngleLeft) / tanAngleWidth;
+        result(3, 0) = 0;
+
+        result(0, 1) = 0;
+        result(1, 1) = 2 / tanAngleHeight;
+        result(2, 1) = (tanAngleUp + tanAngleDown) / tanAngleHeight;
+        result(3, 1) = 0;
+
+        result(0, 2) = 0;
+        result(1, 2) = 0;
+        result(2, 2) = -(farZ + offsetZ) / (farZ - nearZ);
+        result(3, 2) = -(farZ * (nearZ + offsetZ)) / (farZ - nearZ);
+
+        result(0, 3) = 0;
+        result(1, 3) = 0;
+        result(2, 3) = -1;
+        result(3, 3) = 0;
+    }
+}
diff --git a/3rdparty/osgXR/src/projection.h b/3rdparty/osgXR/src/projection.h
new file mode 100644
index 000000000..96a19fc4c
--- /dev/null
+++ b/3rdparty/osgXR/src/projection.h
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: LGPL-2.1-only
+// Copyright (C) 2021 James Hogan <james@albanarts.com>
+
+#ifndef OSGXR_PROJECTION
+#define OSGXR_PROJECTION 1
+
+#include <osg/Matrix>
+
+#include <openxr/openxr.h>
+
+namespace osgXR {
+
+void createProjectionFov(osg::Matrix& result,
+                         const XrFovf& fov,
+                         const float nearZ,
+                         const float farZ);
+
+} // osgXR
+
+#endif