diff --git a/doc_classes/OpenXRFbSpaceWarpExtensionWrapper.xml b/doc_classes/OpenXRFbSpaceWarpExtensionWrapper.xml
new file mode 100644
index 00000000..f2f20db4
--- /dev/null
+++ b/doc_classes/OpenXRFbSpaceWarpExtensionWrapper.xml
@@ -0,0 +1,34 @@
+
+
+
+ Wraps the [code]XR_FB_space_warp[/code] extension.
+
+
+ Wraps the [code]XR_FB_space_warp[/code] extension.
+
+
+
+
+
+
+
+ Checks if the extension is enabled or not.
+
+
+
+
+
+
+ Sets the extension as enabled or disabled.
+ Note: The Application Space Warp project setting must be enabled in order to enable Application Space Warp.
+
+
+
+
+
+ Skips space warp frame extrapolation for the next rendered frame.
+ This is useful in situations where the player camera is snapping/teleporting to a different position.
+
+
+
+
diff --git a/docs/manual/meta/application_space_warp.rst b/docs/manual/meta/application_space_warp.rst
new file mode 100644
index 00000000..c13335be
--- /dev/null
+++ b/docs/manual/meta/application_space_warp.rst
@@ -0,0 +1,20 @@
+Meta Application Space Warp
+===========================
+
+.. note::
+
+ Check out the `Meta Space Warp Sample Project `_
+ for a working demo of Application Space Warp.
+
+Application Space Warp is an extension for Meta XR devices that allows applications to render at half FPS while the runtime
+generates the in-between frames, perserving a smooth experience for users while lowering the workload of the device's CPU/GPU.
+
+For more info on how Application Space Warp works, check out Meta's `Introducing Application Space Warp `_ blog post.
+
+Project Settings
+----------------
+
+To use Application Space Warp, the feature must be enabled in your project settings. The setting can be found in **Project Settings** under the **OpenXR** section.
+The **Application Space Warp** setting should be listed under **Extensions**. The setting will not display unless **Advanced Settings** are enabled.
+
+.. image:: img/application_space_warp/space_warp_project_setting.png
diff --git a/docs/manual/meta/img/application_space_warp/space_warp_project_setting.png b/docs/manual/meta/img/application_space_warp/space_warp_project_setting.png
new file mode 100644
index 00000000..61687d0d
Binary files /dev/null and b/docs/manual/meta/img/application_space_warp/space_warp_project_setting.png differ
diff --git a/docs/manual/meta/index.rst b/docs/manual/meta/index.rst
index cc4fdf64..10b46d05 100644
--- a/docs/manual/meta/index.rst
+++ b/docs/manual/meta/index.rst
@@ -7,6 +7,7 @@ Meta
:maxdepth: 1
:name: toc-meta
+ application_space_warp
composition_layers
hand_tracking
passthrough
diff --git a/plugin/src/main/cpp/extensions/openxr_fb_space_warp_extension_wrapper.cpp b/plugin/src/main/cpp/extensions/openxr_fb_space_warp_extension_wrapper.cpp
new file mode 100644
index 00000000..d9af4eb9
--- /dev/null
+++ b/plugin/src/main/cpp/extensions/openxr_fb_space_warp_extension_wrapper.cpp
@@ -0,0 +1,293 @@
+/**************************************************************************/
+/* openxr_fb_space_warp_extension_wrapper.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT XR */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2022-present Godot XR contributors (see CONTRIBUTORS.md) */
+/* */
+/* 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 above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* 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 "extensions/openxr_fb_space_warp_extension_wrapper.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define GL_RGBA16F 0x881A
+#define GL_DEPTH24_STENCIL8 0x88F0
+
+#define VK_FORMAT_R16G16B16A16_SFLOAT 97
+#define VK_FORMAT_D24_UNORM_S8_UINT 129
+
+OpenXRFbSpaceWarpExtensionWrapper *OpenXRFbSpaceWarpExtensionWrapper::singleton = nullptr;
+
+OpenXRFbSpaceWarpExtensionWrapper *OpenXRFbSpaceWarpExtensionWrapper::get_singleton() {
+ if (singleton == nullptr) {
+ singleton = memnew(OpenXRFbSpaceWarpExtensionWrapper());
+ }
+ return singleton;
+}
+
+OpenXRFbSpaceWarpExtensionWrapper::OpenXRFbSpaceWarpExtensionWrapper() :
+ OpenXRExtensionWrapperExtension() {
+ ERR_FAIL_COND_MSG(singleton != nullptr, "An OpenXRFbSpaceWarpExtensionWrapper singleton already exists.");
+
+ request_extensions[XR_FB_SPACE_WARP_EXTENSION_NAME] = &fb_space_warp_ext;
+ singleton = this;
+}
+
+OpenXRFbSpaceWarpExtensionWrapper::~OpenXRFbSpaceWarpExtensionWrapper() {
+ cleanup();
+ singleton = nullptr;
+}
+
+godot::Dictionary OpenXRFbSpaceWarpExtensionWrapper::_get_requested_extensions() {
+ godot::Dictionary result;
+ for (auto ext : request_extensions) {
+ godot::String key = ext.first;
+ uint64_t value = reinterpret_cast(ext.second);
+ result[key] = (godot::Variant)value;
+ }
+ return result;
+}
+
+uint64_t OpenXRFbSpaceWarpExtensionWrapper::_set_system_properties_and_get_next_pointer(void *p_next_pointer) {
+ if (fb_space_warp_ext) {
+ system_space_warp_properties.next = p_next_pointer;
+ return reinterpret_cast(&system_space_warp_properties);
+ } else {
+ return reinterpret_cast(p_next_pointer);
+ }
+}
+
+uint64_t OpenXRFbSpaceWarpExtensionWrapper::_set_projection_views_and_get_next_pointer(int p_view_index, void *p_next_pointer) {
+ if (fb_space_warp_ext) {
+ space_warp_info[p_view_index].next = p_next_pointer;
+ return reinterpret_cast(&space_warp_info[p_view_index]);
+ } else {
+ return reinterpret_cast(p_next_pointer);
+ }
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::_on_instance_destroyed() {
+ cleanup();
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::_on_session_created(uint64_t p_instance) {
+ if (!fb_space_warp_ext) {
+ return;
+ }
+
+ ProjectSettings *project_settings = ProjectSettings::get_singleton();
+ bool is_project_setting_enabled = (bool)project_settings->get_setting_with_override("xr/openxr/extensions/application_space_warp");
+ if (!is_project_setting_enabled) {
+ fb_space_warp_ext = false;
+ return;
+ } else {
+ enabled = true;
+ }
+
+ String rendering_method = (String)project_settings->get_setting_with_override("rendering/renderer/rendering_method");
+ if (rendering_method == "forward_plus") {
+ UtilityFunctions::print_verbose("Disabling XR_FB_space_warp extension; this extension is not supported in the forward plus renderer");
+ cleanup();
+ return;
+ }
+
+ get_openxr_api()->register_projection_views_extension(this);
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::_on_session_destroyed() {
+ if (!fb_space_warp_ext) {
+ return;
+ }
+
+ get_openxr_api()->unregister_projection_views_extension(this);
+ memdelete_arr(space_warp_info);
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::_on_state_ready() {
+ if (!is_enabled()) {
+ return;
+ }
+
+ Ref openxr_interface = XRServer::get_singleton()->find_interface("OpenXR");
+ int view_count = openxr_interface->get_view_count();
+ int width = system_space_warp_properties.recommendedMotionVectorImageRectWidth;
+ int height = system_space_warp_properties.recommendedMotionVectorImageRectHeight;
+
+ String rendering_driver_name = RenderingServer::get_singleton()->get_current_rendering_driver_name();
+ int swapchain_format, depth_swapchain_format = 0;
+
+ if (rendering_driver_name.contains("opengl")) {
+ swapchain_format = GL_RGBA16F;
+ depth_swapchain_format = GL_DEPTH24_STENCIL8;
+ } else if (rendering_driver_name == "vulkan") {
+ swapchain_format = VK_FORMAT_R16G16B16A16_SFLOAT;
+ depth_swapchain_format = VK_FORMAT_D24_UNORM_S8_UINT;
+ } else {
+ UtilityFunctions::print_verbose("Disabling XR_FB_space_warp extension; rendering driver is not supported: ", rendering_driver_name);
+ cleanup();
+ return;
+ }
+
+ motion_vector_swapchain_info = get_openxr_api()->openxr_swapchain_create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT, swapchain_format, width, height, 1, view_count);
+ motion_vector_depth_swapchain_info = get_openxr_api()->openxr_swapchain_create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, depth_swapchain_format, width, height, 1, view_count);
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::_on_main_swapchains_created() {
+ if (!fb_space_warp_ext) {
+ return;
+ }
+
+ Ref openxr_interface = XRServer::get_singleton()->find_interface("OpenXR");
+ int view_count = openxr_interface->get_view_count();
+
+ space_warp_info = memnew_arr(XrCompositionLayerSpaceWarpInfoFB, view_count);
+ for (int i = 0; i < view_count; i++) {
+ space_warp_info[i].type = XR_TYPE_COMPOSITION_LAYER_SPACE_WARP_INFO_FB;
+
+ space_warp_info[i].next = nullptr;
+
+ space_warp_info[i].layerFlags = 0;
+
+ space_warp_info[i].motionVectorSubImage.swapchain = (XrSwapchain)get_openxr_api()->openxr_swapchain_get_swapchain(motion_vector_swapchain_info);
+ space_warp_info[i].motionVectorSubImage.imageRect.offset.x = 0;
+ space_warp_info[i].motionVectorSubImage.imageRect.offset.y = 0;
+ space_warp_info[i].motionVectorSubImage.imageRect.extent.width = system_space_warp_properties.recommendedMotionVectorImageRectWidth;
+ space_warp_info[i].motionVectorSubImage.imageRect.extent.height = system_space_warp_properties.recommendedMotionVectorImageRectHeight;
+ space_warp_info[i].motionVectorSubImage.imageArrayIndex = i;
+
+ space_warp_info[i].appSpaceDeltaPose = { { 0.0, 0.0, 0.0, 1.0 }, { 0.0, 0.0, 0.0 } };
+
+ space_warp_info[i].depthSubImage.swapchain = (XrSwapchain)get_openxr_api()->openxr_swapchain_get_swapchain(motion_vector_depth_swapchain_info);
+ space_warp_info[i].depthSubImage.imageRect.offset.x = 0;
+ space_warp_info[i].depthSubImage.imageRect.offset.y = 0;
+ space_warp_info[i].depthSubImage.imageRect.extent.width = system_space_warp_properties.recommendedMotionVectorImageRectWidth;
+ space_warp_info[i].depthSubImage.imageRect.extent.height = system_space_warp_properties.recommendedMotionVectorImageRectHeight;
+ space_warp_info[i].depthSubImage.imageArrayIndex = i;
+
+ space_warp_info[i].minDepth = 0.0;
+ space_warp_info[i].maxDepth = 1.0;
+
+ space_warp_info[i].farZ = get_openxr_api()->get_render_state_z_near();
+ space_warp_info[i].nearZ = get_openxr_api()->get_render_state_z_far();
+ }
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::_on_pre_render() {
+ if (!is_enabled()) {
+ return;
+ }
+
+ get_openxr_api()->openxr_swapchain_acquire(motion_vector_swapchain_info);
+ get_openxr_api()->openxr_swapchain_acquire(motion_vector_depth_swapchain_info);
+
+ RID motion_vector_swapchain_image = get_openxr_api()->openxr_swapchain_get_image(motion_vector_swapchain_info);
+ get_openxr_api()->set_velocity_texture(motion_vector_swapchain_image);
+ RID motion_vector_depth_swapchain_image = get_openxr_api()->openxr_swapchain_get_image(motion_vector_depth_swapchain_info);
+ get_openxr_api()->set_velocity_depth_texture(motion_vector_depth_swapchain_image);
+
+ int target_width = system_space_warp_properties.recommendedMotionVectorImageRectWidth;
+ int target_height = system_space_warp_properties.recommendedMotionVectorImageRectHeight;
+ Size2i render_target_size = { target_width, target_height };
+ get_openxr_api()->set_velocity_target_size(render_target_size);
+
+ Transform3D world_transform = XRServer::get_singleton()->get_world_origin();
+ Transform3D delta_transform = render_state.previous_transform.affine_inverse() * world_transform;
+ Quaternion delta_quat = delta_transform.basis.get_quaternion();
+ Vector3 delta_origin = delta_transform.origin;
+
+ Ref openxr_interface = XRServer::get_singleton()->find_interface("OpenXR");
+ int view_count = openxr_interface->get_view_count();
+ for (int i = 0; i < view_count; i++) {
+ space_warp_info[i].layerFlags = render_state.skip_space_warp_frame ? XR_COMPOSITION_LAYER_SPACE_WARP_INFO_FRAME_SKIP_BIT_FB : 0;
+ space_warp_info[i].appSpaceDeltaPose = { { delta_quat.x, delta_quat.y, delta_quat.z, delta_quat.w }, { delta_origin.x, delta_origin.y, delta_origin.z } };
+ space_warp_info[i].farZ = get_openxr_api()->get_render_state_z_near();
+ space_warp_info[i].nearZ = get_openxr_api()->get_render_state_z_far();
+ }
+
+ render_state.skip_space_warp_frame = false;
+ render_state.previous_transform = world_transform;
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::_on_post_draw_viewport(const RID &p_render_target) {
+ if (!is_enabled()) {
+ return;
+ }
+
+ get_openxr_api()->openxr_swapchain_release(motion_vector_swapchain_info);
+ get_openxr_api()->openxr_swapchain_release(motion_vector_depth_swapchain_info);
+}
+
+bool OpenXRFbSpaceWarpExtensionWrapper::is_enabled() {
+ return fb_space_warp_ext && enabled;
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::set_space_warp_enabled(bool p_enable) {
+ enabled = p_enable;
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::_skip_space_warp_frame() {
+ render_state.skip_space_warp_frame = true;
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::skip_space_warp_frame() {
+ if (!is_enabled()) {
+ return;
+ }
+
+ RenderingServer::get_singleton()->call_on_render_thread(callable_mp(this, &OpenXRFbSpaceWarpExtensionWrapper::_skip_space_warp_frame));
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_space_warp_enabled", "enable"), &OpenXRFbSpaceWarpExtensionWrapper::set_space_warp_enabled);
+ ClassDB::bind_method(D_METHOD("is_enabled"), &OpenXRFbSpaceWarpExtensionWrapper::is_enabled);
+ ClassDB::bind_method(D_METHOD("skip_space_warp_frame"), &OpenXRFbSpaceWarpExtensionWrapper::skip_space_warp_frame);
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::cleanup() {
+ if (fb_space_warp_ext) {
+ get_openxr_api()->unregister_projection_views_extension(this);
+ }
+
+ fb_space_warp_ext = false;
+ enabled = false;
+}
+
+void OpenXRFbSpaceWarpExtensionWrapper::add_project_setting() {
+ String p_name = "xr/openxr/extensions/application_space_warp";
+ if (!ProjectSettings::get_singleton()->has_setting(p_name)) {
+ ProjectSettings::get_singleton()->set_setting(p_name, false);
+ }
+
+ ProjectSettings::get_singleton()->set_initial_value(p_name, false);
+ Dictionary property_info;
+ property_info["name"] = p_name;
+ property_info["type"] = Variant::Type::BOOL;
+ property_info["hint"] = PROPERTY_HINT_NONE;
+ ProjectSettings::get_singleton()->add_property_info(property_info);
+}
diff --git a/plugin/src/main/cpp/include/extensions/openxr_fb_space_warp_extension_wrapper.h b/plugin/src/main/cpp/include/extensions/openxr_fb_space_warp_extension_wrapper.h
new file mode 100644
index 00000000..c3a9e623
--- /dev/null
+++ b/plugin/src/main/cpp/include/extensions/openxr_fb_space_warp_extension_wrapper.h
@@ -0,0 +1,103 @@
+/**************************************************************************/
+/* openxr_fb_space_warp_extension_wrapper.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT XR */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2022-present Godot XR contributors (see CONTRIBUTORS.md) */
+/* */
+/* 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 above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* 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. */
+/**************************************************************************/
+
+#ifndef OPENXR_FB_SPACE_WARP_EXTENSION_WRAPPER_H
+#define OPENXR_FB_SPACE_WARP_EXTENSION_WRAPPER_H
+
+#include
+#include
+#include
+#include