diff --git a/scripts/completion/fg-completion.zsh b/scripts/completion/fg-completion.zsh
index 3586fedf4..7fd0dbea7 100755
--- a/scripts/completion/fg-completion.zsh
+++ b/scripts/completion/fg-completion.zsh
@@ -60,6 +60,8 @@ _fgfs_options=(
 	'--enable-skyblend[Enable sky blending]' \
 	'--disable-textures[Disable textures]' \
 	'--enable-textures[Enable textures]' \
+        '--disable-vr[Disable VR]' \
+        '--enable-vr[Enable VR]' \
 	'--disable-wireframe[Disable wireframe drawing mode]' \
 	'--enable-wireframe[Enable wireframe drawing mode]' \
 	'--notrim[Do NOT attempt to trim the model (only with fdm=jsbsim)]' \
diff --git a/src/Include/config_cmake.h.in b/src/Include/config_cmake.h.in
index db44de3b1..b7c67d055 100644
--- a/src/Include/config_cmake.h.in
+++ b/src/Include/config_cmake.h.in
@@ -27,6 +27,7 @@
 #define FLIGHTGEAR_VERSION "@FLIGHTGEAR_VERSION@"
 #define FLIGHTGEAR_MAJOR_VERSION @FG_VERSION_MAJOR@
 #define FLIGHTGEAR_MINOR_VERSION @FG_VERSION_MINOR@
+#define FLIGHTGEAR_PATCH_VERSION @FG_VERSION_PATCH@
 
 #cmakedefine ENABLE_UIUC_MODEL
 #cmakedefine ENABLE_LARCSIM
diff --git a/src/Main/options.cxx b/src/Main/options.cxx
index f0f9c43e3..7bb02f76b 100644
--- a/src/Main/options.cxx
+++ b/src/Main/options.cxx
@@ -1900,6 +1900,10 @@ struct OptionDesc {
     {"disable-wireframe",            false, OPTION_BOOL,   "/sim/rendering/wireframe", false, "", 0 },
     {"enable-wireframe",             false, OPTION_BOOL,   "/sim/rendering/wireframe", true, "", 0 },
     {"materials-file",               true,  OPTION_STRING, "/sim/rendering/materials-file", false, "", 0 },
+#ifdef ENABLE_OSGXR
+    {"disable-vr",                   false, OPTION_BOOL,   "/sim/vr/enabled", false, "", 0 },
+    {"enable-vr",                    false, OPTION_BOOL,   "/sim/vr/enabled", true, "", 0 },
+#endif
     {"disable-terrasync",            false, OPTION_BOOL,   "/sim/terrasync/enabled", false, "", 0 },
     {"enable-terrasync",             false, OPTION_BOOL,   "/sim/terrasync/enabled", true, "", 0 },
     {"terrasync-dir",                true,  OPTION_IGNORE, "", false, "", 0 },
diff --git a/src/Viewer/CMakeLists.txt b/src/Viewer/CMakeLists.txt
index f65fee469..6646fde85 100644
--- a/src/Viewer/CMakeLists.txt
+++ b/src/Viewer/CMakeLists.txt
@@ -28,8 +28,15 @@ set(HEADERS
 	viewmgr.hxx
 	sview.hxx
 	GraphicsPresets.hxx
+	VRManager.hxx
 	)
 
+if (ENABLE_OSGXR)
+	list(APPEND SOURCES
+		VRManager.cxx
+		)
+endif()
+
 if (YES)
     list(APPEND HEADERS PUICamera.hxx)
     list(APPEND SOURCES PUICamera.cxx)
diff --git a/src/Viewer/VRManager.cxx b/src/Viewer/VRManager.cxx
new file mode 100644
index 000000000..8417df65c
--- /dev/null
+++ b/src/Viewer/VRManager.cxx
@@ -0,0 +1,221 @@
+// Copyright (C) 2021  James Hogan
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation; either version 2 of the
+// License, or (at your option) any later version.
+//
+// This program 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
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+#include "VRManager.hxx"
+#include "WindowBuilder.hxx"
+#include "renderer.hxx"
+
+#include <osgXR/Settings>
+
+#include <simgear/scene/viewer/CompositorPass.hxx>
+
+#include <Main/fg_props.hxx>
+#include <Main/globals.hxx>
+
+namespace flightgear
+{
+
+VRManager::VRManager() :
+    _reloadCompositorCallback(new ReloadCompositorCallback(this)),
+    _propXrLayersValidation("/sim/vr/openxr/layers/validation"),
+    _propXrExtensionsDepthInfo("/sim/vr/openxr/extensions/depth-info"),
+    _propXrRuntimeName("/sim/vr/openxr/runtime/name"),
+    _propXrSystemName("/sim/vr/openxr/system/name"),
+    _propStateString("/sim/vr/state-string"),
+    _propPresent("/sim/vr/present"),
+    _propRunning("/sim/vr/running"),
+    _propEnabled("/sim/vr/enabled"),
+    _propDepthInfo("/sim/vr/depth-info"),
+    _propValidationLayer("/sim/vr/validation-layer"),
+    _propMode("/sim/vr/mode"),
+    _propSwapchainMode("/sim/vr/swapchain-mode"),
+    _listenerEnabled(this, &osgXR::Manager::setEnabled),
+    _listenerDepthInfo(this, &VRManager::setDepthInfo),
+    _listenerValidationLayer(this, &VRManager::setValidationLayer),
+    _listenerMode(this, &VRManager::setVRMode),
+    _listenerSwapchainMode(this, &VRManager::setSwapchainMode)
+{
+    uint32_t fgVersion = (FLIGHTGEAR_MAJOR_VERSION << 16 |
+                          FLIGHTGEAR_MINOR_VERSION << 8 |
+                          FLIGHTGEAR_PATCH_VERSION);
+    _settings->setApp("FlightGear", fgVersion);
+    _settings->preferEnvBlendMode(osgXR::Settings::OPAQUE);
+
+    // Hook into viewer, but don't enable VR just yet
+    osgViewer::View *view = globals->get_renderer()->getView();
+    if (view) {
+        setViewer(globals->get_renderer()->getViewerBase());
+        view->apply(this);
+    }
+
+    syncReadOnlyProperties();
+
+    _propEnabled.node(true)->addChangeListener(&_listenerEnabled, true);
+    _propDepthInfo.node(true)->addChangeListener(&_listenerDepthInfo, true);
+    _propValidationLayer.node(true)->addChangeListener(&_listenerValidationLayer, true);
+    _propMode.node(true)->addChangeListener(&_listenerMode, true);
+    _propSwapchainMode.node(true)->addChangeListener(&_listenerSwapchainMode, true);
+}
+
+VRManager *VRManager::instance()
+{
+    static osg::ref_ptr<VRManager> single = new VRManager;
+    return single;
+}
+
+void VRManager::syncProperties()
+{
+    // If the state has changed, properties may need synchronising
+    if (checkAndResetStateChanged()) {
+        syncReadOnlyProperties();
+        syncSettingProperties();
+    }
+}
+
+void VRManager::syncReadOnlyProperties()
+{
+    _propXrLayersValidation = hasValidationLayer();
+    _propXrExtensionsDepthInfo = hasDepthInfoExtension();
+    _propXrRuntimeName = getRuntimeName();
+    _propXrSystemName = getSystemName();
+
+    _propStateString = getStateString();
+    _propPresent = getPresent();
+    _propRunning = isRunning();
+}
+
+void VRManager::syncSettingProperties()
+{
+    bool enabled = getEnabled();
+    if (_propEnabled != enabled)
+        _propEnabled = enabled;
+}
+
+void VRManager::setValidationLayer(bool validationLayer)
+{
+    _settings->setValidationLayer(validationLayer);
+    syncSettings();
+}
+
+void VRManager::setDepthInfo(bool depthInfo)
+{
+    _settings->setDepthInfo(depthInfo);
+    syncSettings();
+}
+
+void VRManager::setVRMode(const char * mode)
+{
+    osgXR::Settings::VRMode vrMode = osgXR::Settings::VRMODE_AUTOMATIC;
+
+    if (strcmp(mode, "AUTOMATIC") == 0) {
+        vrMode = osgXR::Settings::VRMODE_AUTOMATIC;
+    } else if (strcmp(mode, "SLAVE_CAMERAS") == 0) {
+        vrMode = osgXR::Settings::VRMODE_SLAVE_CAMERAS;
+    } else if (strcmp(mode, "SCENE_VIEW") == 0) {
+        vrMode = osgXR::Settings::VRMODE_SCENE_VIEW;
+    }
+
+    _settings->setVRMode(vrMode);
+    syncSettings();
+}
+
+void VRManager::setSwapchainMode(const char * mode)
+{
+    osgXR::Settings::SwapchainMode swapchainMode = osgXR::Settings::SWAPCHAIN_AUTOMATIC;
+
+    if (strcmp(mode, "AUTOMATIC") == 0) {
+        swapchainMode = osgXR::Settings::SWAPCHAIN_AUTOMATIC;
+    } else if (strcmp(mode,"MULTIPLE") == 0) {
+        swapchainMode = osgXR::Settings::SWAPCHAIN_MULTIPLE;
+    } else if (strcmp(mode,"SINGLE") == 0) {
+        swapchainMode = osgXR::Settings::SWAPCHAIN_SINGLE;
+    }
+
+    _settings->setSwapchainMode(swapchainMode);
+    syncSettings();
+}
+
+void VRManager::update()
+{
+    osgXR::Manager::update();
+    syncProperties();
+}
+
+void VRManager::doCreateView(osgXR::View *xrView)
+{
+    // Restarted in osgXR::Manager::update()
+    _viewer->stopThreading();
+
+    // Construct a property tree for the camera
+    SGPropertyNode_ptr camNode = new SGPropertyNode;
+    WindowBuilder *windowBuilder = WindowBuilder::getWindowBuilder();
+    setValue(camNode->getNode("window/name", true),
+             windowBuilder->getDefaultWindowName());
+
+    // Build a camera
+    CameraGroup *cgroup = CameraGroup::getDefault();
+    CameraInfo *info = cgroup->buildCamera(camNode);
+
+    // Notify osgXR about the new compositor's scene slave cameras
+    if (info) {
+        _camInfos[xrView] = info;
+        _xrViews[info] = xrView;
+        info->reloadCompositorCallback = _reloadCompositorCallback;
+
+        postReloadCompositor(cgroup, info);
+    }
+}
+
+void VRManager::doDestroyView(osgXR::View *xrView)
+{
+    // Restarted in osgXR::Manager::update()
+    _viewer->stopThreading();
+
+    CameraGroup *cgroup = CameraGroup::getDefault();
+    auto it = _camInfos.find(xrView);
+    if (it != _camInfos.end()) {
+        osg::ref_ptr<CameraInfo> info = (*it).second;
+        _camInfos.erase(it);
+
+        auto it2 = _xrViews.find(info.get());
+        if (it2 != _xrViews.end())
+            _xrViews.erase(it2);
+
+        cgroup->removeCamera(info.get());
+    }
+}
+
+void VRManager::preReloadCompositor(CameraGroup *cgroup, CameraInfo *info)
+{
+    osgXR::View *xrView = _xrViews[info];
+
+    auto& passes = info->compositor->getPassList();
+    for (auto& pass: passes)
+        if (pass->type == "scene")
+            xrView->removeSlave(pass->camera);
+}
+
+void VRManager::postReloadCompositor(CameraGroup *cgroup, CameraInfo *info)
+{
+    osgXR::View *xrView = _xrViews[info];
+
+    auto& passes = info->compositor->getPassList();
+    for (auto& pass: passes)
+        if (pass->type == "scene")
+            xrView->addSlave(pass->camera);
+}
+
+}
diff --git a/src/Viewer/VRManager.hxx b/src/Viewer/VRManager.hxx
new file mode 100644
index 000000000..db97f682d
--- /dev/null
+++ b/src/Viewer/VRManager.hxx
@@ -0,0 +1,158 @@
+// Copyright (C) 2021  James Hogan
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation; either version 2 of the
+// License, or (at your option) any later version.
+//
+// This program 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
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+#ifndef VRMANAGER_HXX
+#define VRMANAGER_HXX 1
+
+#include <config.h>
+
+#ifdef ENABLE_OSGXR
+
+#include <osg/ref_ptr>
+#include <osg/observer_ptr>
+
+#include <osgXR/Manager>
+
+#include <simgear/props/propertyObject.hxx>
+#include <simgear/scene/viewer/CompositorPass.hxx>
+
+#include "CameraGroup.hxx"
+
+#include <map>
+
+namespace flightgear
+{
+
+class VRManager : public osgXR::Manager
+{
+    public:
+
+        class ReloadCompositorCallback : public CameraInfo::ReloadCompositorCallback
+        {
+            public:
+
+                ReloadCompositorCallback(VRManager *manager) :
+                    _manager(manager)
+                {
+                };
+
+                virtual void preReloadCompositor(CameraGroup *cgroup, CameraInfo *info)
+                {
+                    _manager->preReloadCompositor(cgroup, info);
+                }
+
+                virtual void postReloadCompositor(CameraGroup *cgroup, CameraInfo *info)
+                {
+                    _manager->postReloadCompositor(cgroup, info);
+                }
+
+            protected:
+
+                osg::observer_ptr<VRManager> _manager;
+        };
+
+        VRManager();
+
+        static VRManager *instance();
+
+        void syncProperties();
+        void syncReadOnlyProperties();
+        void syncSettingProperties();
+
+        // Settings
+
+        void setValidationLayer(bool validationLayer);
+        void setDepthInfo(bool depthInfo);
+
+        void setVRMode(const char * mode);
+        void setSwapchainMode(const char * mode);
+
+        // osgXR::Manager overrides
+
+        void update() override;
+
+        void doCreateView(osgXR::View *xrView) override;
+        void doDestroyView(osgXR::View *xrView) override;
+
+        void preReloadCompositor(CameraGroup *cgroup, CameraInfo *info);
+        void postReloadCompositor(CameraGroup *cgroup, CameraInfo *info);
+
+    protected:
+
+        typedef std::map<osgXR::View *, osg::ref_ptr<CameraInfo>> XRViewToCamInfo;
+        XRViewToCamInfo _camInfos;
+
+        typedef std::map<CameraInfo *, osg::ref_ptr<osgXR::View>> CamInfoToXRView;
+        CamInfoToXRView _xrViews;
+
+        osg::ref_ptr<ReloadCompositorCallback> _reloadCompositorCallback;
+
+        // Properties
+
+        SGPropObjBool _propXrLayersValidation;
+        SGPropObjBool _propXrExtensionsDepthInfo;
+        SGPropObjString _propXrRuntimeName;
+        SGPropObjString _propXrSystemName;
+
+        SGPropObjString _propStateString;
+        SGPropObjBool _propPresent;
+        SGPropObjBool _propRunning;
+
+        SGPropObjBool _propEnabled;
+        SGPropObjBool _propDepthInfo;
+        SGPropObjBool _propValidationLayer;
+        SGPropObjString _propMode;
+        SGPropObjString _propSwapchainMode;
+
+        // Property listeners
+
+        template <typename T>
+        class Listener : public SGPropertyChangeListener
+        {
+            public:
+                typedef void (VRManager::*SetterFn)(T v);
+
+                Listener(VRManager *manager, SetterFn setter) :
+                    _manager(manager),
+                    _setter(setter)
+                {
+                }
+
+                void valueChanged(SGPropertyNode *node) override
+                {
+                    (_manager->*_setter)(node->template getValue<T>());
+                }
+
+            protected:
+
+                VRManager *_manager;
+                SetterFn _setter;
+        };
+        typedef Listener<bool> ListenerBool;
+        typedef Listener<const char *> ListenerString;
+
+        ListenerBool _listenerEnabled;
+        ListenerBool _listenerDepthInfo;
+        ListenerBool _listenerValidationLayer;
+        ListenerString _listenerMode;
+        ListenerString _listenerSwapchainMode;
+};
+
+}
+
+#endif // ENABLE_OSGXR
+
+#endif
diff --git a/src/Viewer/fg_os_osgviewer.cxx b/src/Viewer/fg_os_osgviewer.cxx
index 600fbcf73..dae44a64d 100644
--- a/src/Viewer/fg_os_osgviewer.cxx
+++ b/src/Viewer/fg_os_osgviewer.cxx
@@ -52,6 +52,7 @@
 #include "renderer.hxx"
 #include "CameraGroup.hxx"
 #include "FGEventHandler.hxx"
+#include "VRManager.hxx"
 #include "WindowBuilder.hxx"
 #include "WindowSystemAdapter.hxx"
 #include <Main/sentryIntegration.hxx>
@@ -328,6 +329,12 @@ void fgOSResetProperties()
     fgTie("/sim/rendering/osg-displaysettings/double-buffer", displaySettings, &DisplaySettings::getDoubleBuffer, &DisplaySettings::setDoubleBuffer );
     fgTie("/sim/rendering/osg-displaysettings/depth-buffer", displaySettings, &DisplaySettings::getDepthBuffer, &DisplaySettings::setDepthBuffer );
     fgTie("/sim/rendering/osg-displaysettings/rgb", displaySettings, &DisplaySettings::getRGB, &DisplaySettings::setRGB );
+
+#ifdef ENABLE_OSGXR
+    fgSetBool("/sim/vr/built", true);
+#else
+    fgSetBool("/sim/vr/built", false);
+#endif
 }
 
 
@@ -389,6 +396,9 @@ int fgOSMainLoop()
             }
         }
         globals->get_renderer()->update();
+#ifdef ENABLE_OSGXR
+        VRManager::instance()->update();
+#endif
         viewer_base->frame( globals->get_sim_time_sec() );
     }
 
@@ -437,6 +447,9 @@ void fgOSCloseWindow()
             viewer_base->stopThreading();
         }
     }
+#ifdef ENABLE_OSGXR
+    VRManager::instance()->destroyAndWait();
+#endif
     FGScenery::resetPagerSingleton();
     flightgear::addSentryBreadcrumb("fgOSCloseWindow, clearing camera group", "info");
     flightgear::CameraGroup::setDefault(NULL);